@lmzhen/dsh-evolution-ui 0.0.0 → 0.32.1

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.
Files changed (36) hide show
  1. package/README.md +6 -1
  2. package/lib/client.js +3656 -0
  3. package/lib/index.js +323 -0
  4. package/lib/types/client/index.d.ts +9 -0
  5. package/lib/types/index.d.ts +12 -0
  6. package/lib/types/panels/history/client/Panel.d.ts +27 -0
  7. package/lib/types/panels/history/client/api.d.ts +142 -0
  8. package/lib/types/panels/history/client/atoms.d.ts +73 -0
  9. package/lib/types/panels/history/client/chrome.d.ts +76 -0
  10. package/lib/types/panels/history/client/document.d.ts +71 -0
  11. package/lib/types/panels/history/client/guards.d.ts +13 -0
  12. package/lib/types/panels/history/client/ids.d.ts +3 -0
  13. package/lib/types/panels/history/client/index.d.ts +11 -0
  14. package/lib/types/panels/history/client/markdown.d.ts +56 -0
  15. package/lib/types/panels/history/client/messages.d.ts +30 -0
  16. package/lib/types/panels/history/client/styles.d.ts +5 -0
  17. package/lib/types/panels/history/host.d.ts +25 -0
  18. package/lib/types/panels/history/routes.d.ts +50 -0
  19. package/lib/types/panels/history/unit-undoable.d.ts +18 -0
  20. package/lib/types/panels/settings/DiagnosticsCard.d.ts +29 -0
  21. package/lib/types/panels/settings/ParamCard.d.ts +107 -0
  22. package/lib/types/panels/settings/PendingCard.d.ts +45 -0
  23. package/lib/types/panels/settings/RowConfigCard.d.ts +30 -0
  24. package/lib/types/panels/settings/SettingsSection.d.ts +19 -0
  25. package/lib/types/panels/settings/api.d.ts +64 -0
  26. package/lib/types/panels/settings/diagnostics-state.d.ts +28 -0
  27. package/lib/types/panels/settings/generated-about.d.ts +11 -0
  28. package/lib/types/panels/settings/generated-params.d.ts +51 -0
  29. package/lib/types/panels/settings/guards.d.ts +13 -0
  30. package/lib/types/panels/settings/index.d.ts +50 -0
  31. package/lib/types/panels/settings/messages.d.ts +134 -0
  32. package/lib/types/panels/settings/pending-state.d.ts +74 -0
  33. package/lib/types/panels/settings/seam.d.ts +178 -0
  34. package/lib/types/panels/settings/source.d.ts +88 -0
  35. package/lib/types/panels/settings/styles.d.ts +32 -0
  36. package/package.json +51 -4
package/lib/client.js ADDED
@@ -0,0 +1,3656 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@lmzhen/dsh-evolution-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
+ let _deepseek_ai_dsh_evolution_merge_ui_client = require("@lmzhen/dsh-evolution-merge-ui/client");
9
+ //#region ../evolution-client-kit/src/generated/routes.generated.ts
10
+ /**
11
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
12
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
13
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
14
+ * client copies, and the generator's --check makes a stale copy red.
15
+ */
16
+ const ROUTES = {
17
+ approval: {
18
+ pending: "/api/dsh-evolution/approval/pending",
19
+ preview: "/api/dsh-evolution/approval/preview",
20
+ approve: "/api/dsh-evolution/approval/approve",
21
+ reject: "/api/dsh-evolution/approval/reject"
22
+ },
23
+ "skill-history": {
24
+ skills: "/api/dsh-evolution/skill-history/skills",
25
+ versions: "/api/dsh-evolution/skill-history/versions",
26
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
27
+ body: "/api/dsh-evolution/skill-history/versions/body",
28
+ undo: "/api/dsh-evolution/skill-history/undo",
29
+ health: "/api/dsh-evolution/skill-history/health"
30
+ },
31
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
32
+ merge: {
33
+ view: "/api/dsh-evolution/merge/view",
34
+ rows: "/api/dsh-evolution/merge/rows",
35
+ run: "/api/dsh-evolution/merge/run",
36
+ runs: "/api/dsh-evolution/merge/runs",
37
+ body: "/api/dsh-evolution/merge/body",
38
+ history: "/api/dsh-evolution/merge/history"
39
+ },
40
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" }
41
+ };
42
+ //#endregion
43
+ //#region ../evolution-client-kit/src/generated/route-client.generated.ts
44
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
45
+ function classify(status) {
46
+ if (status === 401) return {
47
+ kind: "unauthenticated",
48
+ status: 401
49
+ };
50
+ if (status === 403) return {
51
+ kind: "forbidden",
52
+ status: 403
53
+ };
54
+ if (status === 404) return {
55
+ kind: "unmounted",
56
+ status: 404
57
+ };
58
+ return {
59
+ kind: "server",
60
+ status
61
+ };
62
+ }
63
+ /**
64
+ * One request, no retries and no body parsing for the platform's admission answers.
65
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
66
+ * @param path - the exact route path from the generated ROUTES table.
67
+ * @param init - standard RequestInit.
68
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
69
+ */
70
+ async function requestJson(doFetch, path, init) {
71
+ let response;
72
+ try {
73
+ response = await doFetch(path, init);
74
+ } catch (error) {
75
+ return {
76
+ ok: false,
77
+ failure: {
78
+ kind: "server",
79
+ status: 0,
80
+ detail: error instanceof Error ? error.message : String(error)
81
+ }
82
+ };
83
+ }
84
+ if (!response.ok) return {
85
+ ok: false,
86
+ failure: classify(response.status)
87
+ };
88
+ let body;
89
+ try {
90
+ body = await response.json();
91
+ } catch {
92
+ return {
93
+ ok: false,
94
+ failure: {
95
+ kind: "server",
96
+ status: response.status
97
+ }
98
+ };
99
+ }
100
+ if (typeof body === "object" && body !== null) {
101
+ const record = body;
102
+ if (record.ok === true) return {
103
+ ok: true,
104
+ data: record.data
105
+ };
106
+ const nested = record.error;
107
+ const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
108
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
109
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
110
+ if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
111
+ ok: false,
112
+ failure: {
113
+ kind: "refused",
114
+ code,
115
+ message
116
+ }
117
+ };
118
+ }
119
+ return {
120
+ ok: false,
121
+ failure: {
122
+ kind: "server",
123
+ status: response.status
124
+ }
125
+ };
126
+ }
127
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
128
+ function failureMessage(failure) {
129
+ switch (failure.kind) {
130
+ case "refused": return failure.message;
131
+ case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
132
+ case "unauthenticated": return "HTTP 401";
133
+ case "forbidden": return "HTTP 403";
134
+ case "unmounted": return "HTTP 404";
135
+ }
136
+ }
137
+ //#endregion
138
+ //#region src/panels/settings/api.ts
139
+ /**
140
+ * The approval routes this bundle calls, and the ONE place a request is made.
141
+ *
142
+ * Structural types on purpose: the browser half cannot import the host package at runtime, so the row
143
+ * shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
144
+ * same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
145
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/api
146
+ */
147
+ /**
148
+ * Build the approval face over one fetch implementation.
149
+ * @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
150
+ * @returns the three calls the pending card makes.
151
+ */
152
+ function createApprovalApi(doFetch) {
153
+ const post = async (path, id) => await requestJson(doFetch, path, {
154
+ method: "POST",
155
+ headers: { "content-type": "application/json" },
156
+ body: JSON.stringify({ id })
157
+ });
158
+ return {
159
+ pending: async (status) => await requestJson(doFetch, status === void 0 ? ROUTES.approval.pending : ROUTES.approval.pending + "?status=" + status),
160
+ approve: async (id) => await post(ROUTES.approval.approve, id),
161
+ reject: async (id) => await post(ROUTES.approval.reject, id),
162
+ preview: async (id) => await requestJson(doFetch, ROUTES.approval.preview + "?id=" + encodeURIComponent(id))
163
+ };
164
+ }
165
+ //#endregion
166
+ //#region src/panels/settings/generated-params.ts
167
+ const CLIENT_PARAM_SECTIONS = [
168
+ {
169
+ namespace: "evolution-approval",
170
+ seatKey: "evolution-approval",
171
+ fields: [
172
+ {
173
+ id: "evolution-approval.enabled",
174
+ group: "deployment",
175
+ doc: "Master approval switch: run the approval pipeline for foreground writes.",
176
+ label: "启用批准流水线",
177
+ hint: "关闭后前台写入不再经批准。",
178
+ control: "switch",
179
+ unit: "",
180
+ values: [],
181
+ valueLabels: [],
182
+ atLeast: ""
183
+ },
184
+ {
185
+ id: "evolution-approval.pendingTtlMs",
186
+ group: "deployment",
187
+ doc: "How long a staged write may wait before it expires: expired writes are closed as rejected and are NEVER executed; 0 (the default) means the window never expires.",
188
+ label: "待批过期时间",
189
+ hint: "一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。",
190
+ control: "number",
191
+ unit: "毫秒",
192
+ values: [],
193
+ valueLabels: [],
194
+ atLeast: ""
195
+ },
196
+ {
197
+ id: "approvalConfirm",
198
+ group: "write-caps",
199
+ doc: "What the confirmation before an APPROVE does: write straight through, ask first, or treat an unanswered prompt as a cancel. The same three words skillWriteConfirm uses for a skill write.",
200
+ label: "批准前先确认",
201
+ hint: "点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。",
202
+ control: "select",
203
+ unit: "",
204
+ values: [
205
+ "auto",
206
+ "ask",
207
+ "timeout"
208
+ ],
209
+ valueLabels: [
210
+ "直接执行",
211
+ "先确认",
212
+ "等待超时即取消"
213
+ ],
214
+ atLeast: ""
215
+ }
216
+ ]
217
+ },
218
+ {
219
+ namespace: "evolution-commands",
220
+ seatKey: "evolution-commands",
221
+ fields: [{
222
+ id: "evolution-commands.maintainHandoffOnSettle",
223
+ group: "deployment",
224
+ doc: "Hand the maintenance report to the model automatically once a scan settles successfully (the same injection as maintain handoff); off by default — enabling it costs one extra model call per successful run.",
225
+ label: "扫描完成后自动交接",
226
+ hint: "维护扫描成功结束后,自动把报告交给模型(等同 maintain handoff);默认关闭,每次成功扫描会多花一次模型调用。",
227
+ control: "switch",
228
+ unit: "",
229
+ values: [],
230
+ valueLabels: [],
231
+ atLeast: ""
232
+ }]
233
+ },
234
+ {
235
+ namespace: "evolution-curator",
236
+ seatKey: "evolution-curator",
237
+ fields: [
238
+ {
239
+ id: "curatorIntervalHours",
240
+ group: "curator",
241
+ doc: "Minimum hours between deterministic curation passes.",
242
+ label: "自动整理间隔",
243
+ hint: "两次自动整理之间至少间隔多少小时。",
244
+ control: "number",
245
+ unit: "小时",
246
+ values: [],
247
+ valueLabels: [],
248
+ atLeast: ""
249
+ },
250
+ {
251
+ id: "staleAfterDays",
252
+ group: "curator",
253
+ doc: "Inactive days before a skill counts as stale.",
254
+ label: "多久没用算过时",
255
+ hint: "技能连续多少天没被用到就算过时。",
256
+ control: "number",
257
+ unit: "天",
258
+ values: [],
259
+ valueLabels: [],
260
+ atLeast: ""
261
+ },
262
+ {
263
+ id: "archiveAfterDays",
264
+ group: "curator",
265
+ doc: "Inactive days before a stale skill is archived (must be >= staleAfterDays).",
266
+ label: "过时后归档",
267
+ hint: "过时技能再过多少天移入归档(不能小于上一项)。",
268
+ control: "number",
269
+ unit: "天",
270
+ values: [],
271
+ valueLabels: [],
272
+ atLeast: "staleAfterDays"
273
+ },
274
+ {
275
+ id: "minIdleHours",
276
+ group: "curator",
277
+ doc: "Idle hours required before an automatic curation pass runs.",
278
+ label: "最少空闲时长",
279
+ hint: "你至少多少小时没用,才运行自动整理。",
280
+ control: "number",
281
+ unit: "小时",
282
+ values: [],
283
+ valueLabels: [],
284
+ atLeast: ""
285
+ },
286
+ {
287
+ id: "minIdleFailOpen",
288
+ group: "curator",
289
+ doc: "Let the idle gate open when the activity probe is unavailable.",
290
+ label: "检测不到活动也运行",
291
+ hint: "无法判断你是否空闲时,仍然运行自动整理。",
292
+ control: "switch",
293
+ unit: "",
294
+ values: [],
295
+ valueLabels: [],
296
+ atLeast: ""
297
+ },
298
+ {
299
+ id: "llmReview",
300
+ group: "curator",
301
+ doc: "Enable the LLM nomination pass on top of the deterministic lifecycle.",
302
+ label: "用模型挑选",
303
+ hint: "在固定规则之外,再用模型挑一遍候选项。",
304
+ control: "switch",
305
+ unit: "",
306
+ values: [],
307
+ valueLabels: [],
308
+ atLeast: ""
309
+ },
310
+ {
311
+ id: "curatorReviewMaxTokens",
312
+ group: "curator",
313
+ doc: "Token budget of the curator LLM review.",
314
+ label: "模型处理上限",
315
+ hint: "每次用模型挑选时最多消耗多少 token。",
316
+ control: "number",
317
+ unit: "token",
318
+ values: [],
319
+ valueLabels: [],
320
+ atLeast: ""
321
+ },
322
+ {
323
+ id: "curatorReviewTimeoutMs",
324
+ group: "curator",
325
+ doc: "Wall-clock bound of the curator LLM review.",
326
+ label: "模型处理超时",
327
+ hint: "一次模型挑选最长允许运行多久。",
328
+ control: "number",
329
+ unit: "毫秒",
330
+ values: [],
331
+ valueLabels: [],
332
+ atLeast: ""
333
+ },
334
+ {
335
+ id: "healthSoftBodyChars",
336
+ group: "curator",
337
+ doc: "Body character line the health view judges against.",
338
+ label: "正文长度参考值",
339
+ hint: "技能正文超过多少字符算偏长(只用于健康提示)。",
340
+ control: "number",
341
+ unit: "字符",
342
+ values: [],
343
+ valueLabels: [],
344
+ atLeast: ""
345
+ },
346
+ {
347
+ id: "healthStampDensityPerKb",
348
+ group: "curator",
349
+ doc: "Stamp density per KB that flags log-like content in a body.",
350
+ label: "日志化判定阈值",
351
+ hint: "每 KB 出现多少个时间标记,就判为「像日志」。",
352
+ control: "number",
353
+ unit: "处/KB",
354
+ values: [],
355
+ valueLabels: [],
356
+ atLeast: ""
357
+ },
358
+ {
359
+ id: "healthChurnMinPatches",
360
+ group: "curator",
361
+ doc: "Patches without a read that flag a write-ghost skill.",
362
+ label: "只写不读判定次数",
363
+ hint: "连续多少次改动都没有被读取,就判为「只写不读」。",
364
+ control: "number",
365
+ unit: "处",
366
+ values: [],
367
+ valueLabels: [],
368
+ atLeast: ""
369
+ }
370
+ ]
371
+ },
372
+ {
373
+ namespace: "evolution-merge",
374
+ seatKey: "evolution-merge",
375
+ fields: [
376
+ {
377
+ id: "mergeApproval",
378
+ group: "merge",
379
+ doc: "Pause after round 1 and put the pair in the pending queue instead of authoring round 2 automatically.",
380
+ label: "合并审批",
381
+ hint: "开启后,第一轮判定完先停下、进入待批准清单;关闭则直接进入第二轮合并。",
382
+ control: "switch",
383
+ unit: "",
384
+ values: [],
385
+ valueLabels: [],
386
+ atLeast: ""
387
+ },
388
+ {
389
+ id: "mergeSkippedTtlDays",
390
+ group: "merge",
391
+ doc: "Days a skipped pair stays out of screening; 0 means it never expires.",
392
+ label: "无需合并保留天数",
393
+ hint: "判定无需合并的对,多少天内不再参与筛选;0 表示一直保留。",
394
+ control: "number",
395
+ unit: "天",
396
+ values: [],
397
+ valueLabels: [],
398
+ atLeast: ""
399
+ },
400
+ {
401
+ id: "mergeRunTimeoutMinutes",
402
+ group: "merge",
403
+ doc: "Wall-clock budget of one merge run; 0 means no limit.",
404
+ label: "单次运行上限",
405
+ hint: "一次合并运行最多持续多少分钟;0 表示不设上限。",
406
+ control: "number",
407
+ unit: "分钟",
408
+ values: [],
409
+ valueLabels: [],
410
+ atLeast: ""
411
+ }
412
+ ]
413
+ },
414
+ {
415
+ namespace: "evolution-review",
416
+ seatKey: "evolution-review",
417
+ fields: [
418
+ {
419
+ id: "reviewSkillInterval",
420
+ group: "review",
421
+ doc: "Tool calls between skill-review injections: a turn advances by its tool-call count (a turn with none counts as one), or by 1 when the turn itself used a skill.",
422
+ label: "技能检查间隔",
423
+ hint: "每累计多少次工具调用检查一次技能:一轮按该轮工具调用数累加(一轮没有工具调用也算 1 次),本轮本身用到技能则只算 1 次。",
424
+ control: "number",
425
+ unit: "次",
426
+ values: [],
427
+ valueLabels: [],
428
+ atLeast: ""
429
+ },
430
+ {
431
+ id: "reviewMemoryInterval",
432
+ group: "review",
433
+ doc: "Tool calls between memory-review injections: a turn advances by its tool-call count (a turn with none counts as one), or by 1 when the turn itself touched memory.",
434
+ label: "记忆检查间隔",
435
+ hint: "每累计多少次工具调用检查一次记忆:一轮按该轮工具调用数累加(一轮没有工具调用也算 1 次),本轮本身用到记忆则只算 1 次。",
436
+ control: "number",
437
+ unit: "次",
438
+ values: [],
439
+ valueLabels: [],
440
+ atLeast: ""
441
+ },
442
+ {
443
+ id: "skillReviewTrigger",
444
+ group: "review",
445
+ doc: "Which channel may inject a skill review (cadence, completion, both).",
446
+ label: "技能检查时机",
447
+ hint: "什么时候检查技能:按间隔/按任务完成/两者。",
448
+ control: "select",
449
+ unit: "",
450
+ values: [
451
+ "cadence",
452
+ "completion",
453
+ "both"
454
+ ],
455
+ valueLabels: [
456
+ "按间隔",
457
+ "按任务完成",
458
+ "两者"
459
+ ],
460
+ atLeast: ""
461
+ },
462
+ {
463
+ id: "skillReviewCompletionMinToolCalls",
464
+ group: "review",
465
+ doc: "Tool calls a task needs before the completion channel injects.",
466
+ label: "完成时的最少工具调用",
467
+ hint: "选「按任务完成」时,任务至少用了几次工具才检查。",
468
+ control: "number",
469
+ unit: "次",
470
+ values: [],
471
+ valueLabels: [],
472
+ atLeast: ""
473
+ },
474
+ {
475
+ id: "reviewEnabled",
476
+ group: "review",
477
+ doc: "Master switch for the review plugin.",
478
+ label: "启用自动检查",
479
+ hint: "关闭后不再自动回顾会话。",
480
+ control: "switch",
481
+ unit: "",
482
+ values: [],
483
+ valueLabels: [],
484
+ atLeast: ""
485
+ },
486
+ {
487
+ id: "reviewMode",
488
+ group: "review",
489
+ doc: "Run the review in the parent session (inject), on a subagent, or in a background branch.",
490
+ label: "运行位置",
491
+ hint: "在哪里运行检查:在当前会话里/交给子代理/后台分支。",
492
+ control: "select",
493
+ unit: "",
494
+ values: ["inject", "branch"],
495
+ valueLabels: ["在当前会话里", "后台分支"],
496
+ atLeast: ""
497
+ },
498
+ {
499
+ id: "reviewWakeInject",
500
+ group: "review",
501
+ doc: "Deliver the deferred review as a waking follow-up message.",
502
+ label: "延后结果发提醒",
503
+ hint: "会话空闲时,把延后的检查结果作为一条提醒发给你。",
504
+ control: "switch",
505
+ unit: "",
506
+ values: [],
507
+ valueLabels: [],
508
+ atLeast: ""
509
+ },
510
+ {
511
+ id: "reviewNotice",
512
+ group: "review",
513
+ doc: "Deliver result notices into the parent conversation.",
514
+ label: "结果通知",
515
+ hint: "关闭后检查结果不再写进对话;检查本身照常运行,结果留在审计记录里。",
516
+ control: "switch",
517
+ unit: "",
518
+ values: [],
519
+ valueLabels: [],
520
+ atLeast: ""
521
+ }
522
+ ]
523
+ },
524
+ {
525
+ namespace: "memory-files",
526
+ seatKey: "memory-files",
527
+ fields: [
528
+ {
529
+ id: "memoryChars",
530
+ group: "memory",
531
+ doc: "Character budget the memory store enforces for MEMORY.md.",
532
+ label: "记忆库上限",
533
+ hint: "长期记忆文件(MEMORY.md)最多多少字符。",
534
+ control: "number",
535
+ unit: "字符",
536
+ values: [],
537
+ valueLabels: [],
538
+ atLeast: ""
539
+ },
540
+ {
541
+ id: "userChars",
542
+ group: "memory",
543
+ doc: "Character budget the memory store enforces for USER.md.",
544
+ label: "用户画像上限",
545
+ hint: "用户画像文件(USER.md)最多多少字符。",
546
+ control: "number",
547
+ unit: "字符",
548
+ values: [],
549
+ valueLabels: [],
550
+ atLeast: ""
551
+ },
552
+ {
553
+ id: "addDatePrefix",
554
+ group: "memory",
555
+ doc: "Prefix stored memory entries with their date heading.",
556
+ label: "记忆条目加日期",
557
+ hint: "写入时在条目开头加上日期。",
558
+ control: "switch",
559
+ unit: "",
560
+ values: [],
561
+ valueLabels: [],
562
+ atLeast: ""
563
+ },
564
+ {
565
+ id: "maxConsolidationFailures",
566
+ group: "memory",
567
+ doc: "Consolidation failures one turn tolerates before the tool gives up.",
568
+ label: "合并失败重试",
569
+ hint: "合并记忆连续失败多少次后停止。",
570
+ control: "number",
571
+ unit: "次",
572
+ values: [],
573
+ valueLabels: [],
574
+ atLeast: ""
575
+ }
576
+ ]
577
+ },
578
+ {
579
+ namespace: "tool-memory",
580
+ seatKey: "tool-memory",
581
+ fields: [{
582
+ id: "entryPreviewChars",
583
+ group: "memory",
584
+ doc: "Characters of one memory entry shown in a tool result preview.",
585
+ label: "记忆预览长度",
586
+ hint: "列表里每条记忆最多显示多少字符。",
587
+ control: "number",
588
+ unit: "字符",
589
+ values: [],
590
+ valueLabels: [],
591
+ atLeast: ""
592
+ }]
593
+ },
594
+ {
595
+ namespace: "tool-skill-manage",
596
+ seatKey: "tool-skill-manage",
597
+ fields: [
598
+ {
599
+ id: "skillContentChars",
600
+ group: "write-caps",
601
+ doc: "Character cap on a SKILL.md body (tighten-only).",
602
+ label: "技能文件正文上限",
603
+ hint: "技能 Markdown 正文最多多少字符(只能调小)。",
604
+ control: "number",
605
+ unit: "字符",
606
+ values: [],
607
+ valueLabels: [],
608
+ atLeast: ""
609
+ },
610
+ {
611
+ id: "maxSkillFileBytes",
612
+ group: "write-caps",
613
+ doc: "Byte cap on one support file (tighten-only).",
614
+ label: "附带文件大小上限",
615
+ hint: "技能附带文件单个最大多少字节(只能调小)。",
616
+ control: "number",
617
+ unit: "字节",
618
+ values: [],
619
+ valueLabels: [],
620
+ atLeast: ""
621
+ },
622
+ {
623
+ id: "maxSkillNameLength",
624
+ group: "write-caps",
625
+ doc: "Character cap on a skill name (tighten-only).",
626
+ label: "技能名长度上限",
627
+ hint: "技能名最多多少字符(只能调小)。",
628
+ control: "number",
629
+ unit: "字符",
630
+ values: [],
631
+ valueLabels: [],
632
+ atLeast: ""
633
+ },
634
+ {
635
+ id: "maxDescriptionLength",
636
+ group: "write-caps",
637
+ doc: "Character cap on a skill description (tighten-only).",
638
+ label: "技能描述长度上限",
639
+ hint: "技能描述最多多少字符(只能调小)。",
640
+ control: "number",
641
+ unit: "字符",
642
+ values: [],
643
+ valueLabels: [],
644
+ atLeast: ""
645
+ },
646
+ {
647
+ id: "descriptionStrict",
648
+ group: "write-caps",
649
+ doc: "Refuse a description over the authoring bar instead of advising.",
650
+ label: "描述不合规即拒绝",
651
+ hint: "描述不符合要求时直接拒绝写入(默认只提醒)。",
652
+ control: "switch",
653
+ unit: "",
654
+ values: [],
655
+ valueLabels: [],
656
+ atLeast: ""
657
+ },
658
+ {
659
+ id: "strictCrossSource",
660
+ group: "write-caps",
661
+ doc: "Refuse writes whose catalog entry resolves outside the family.",
662
+ label: "引用外部内容即拒绝",
663
+ hint: "技能引用了本插件之外的文件时拒绝写入。",
664
+ control: "switch",
665
+ unit: "",
666
+ values: [],
667
+ valueLabels: [],
668
+ atLeast: ""
669
+ },
670
+ {
671
+ id: "citationPolicy",
672
+ group: "write-caps",
673
+ doc: "Refuse a move that would leave a dangling reference, or verify it.",
674
+ label: "引用检查方式",
675
+ hint: "移动或合并技能时怎么处理引用:先检查/直接拒绝。",
676
+ control: "select",
677
+ unit: "",
678
+ values: ["verify", "refuse"],
679
+ valueLabels: ["先检查", "直接拒绝"],
680
+ atLeast: ""
681
+ },
682
+ {
683
+ id: "supportFileCharPolicy",
684
+ group: "write-caps",
685
+ doc: "Warn about an oversize support file, or refuse the write.",
686
+ label: "附带文件超限时",
687
+ hint: "附带文件超出上限时:只提醒/拒绝写入。",
688
+ control: "select",
689
+ unit: "",
690
+ values: ["report", "enforce"],
691
+ valueLabels: ["只提醒", "拒绝写入"],
692
+ atLeast: ""
693
+ },
694
+ {
695
+ id: "skillWriteConfirm",
696
+ group: "write-caps",
697
+ doc: "What the one confirmation before a create or a bare delete does: write straight through, wait for an answer, or cancel the write when nobody answers within the timeout.",
698
+ label: "写入前是否确认",
699
+ hint: "新建或删除技能前要不要问你:直接写入/一直等你回答/超时未答就取消这次写入。",
700
+ control: "select",
701
+ unit: "",
702
+ values: [
703
+ "auto",
704
+ "ask",
705
+ "timeout"
706
+ ],
707
+ valueLabels: [
708
+ "不弹窗直接写入",
709
+ "一直等待回答",
710
+ "超时自动取消"
711
+ ],
712
+ atLeast: ""
713
+ },
714
+ {
715
+ id: "skillWriteConfirmTimeoutSeconds",
716
+ group: "write-caps",
717
+ doc: "Seconds the confirmation waits for an answer in timeout mode before the write is cancelled.",
718
+ label: "确认等待秒数",
719
+ hint: "选「超时自动取消」时,等多少秒没人回答就取消这次写入。",
720
+ control: "number",
721
+ unit: "秒",
722
+ values: [],
723
+ valueLabels: [],
724
+ atLeast: ""
725
+ }
726
+ ]
727
+ }
728
+ ];
729
+ /** Every (bundle, row) pair the Plugins page can open a configuration page for. */
730
+ const CLIENT_ROW_SEATS = [
731
+ {
732
+ bundle: "@lmzhen/dsh-evolution-all",
733
+ rows: [
734
+ "evolution-approval",
735
+ "evolution-commands",
736
+ "evolution-curator",
737
+ "evolution-merge",
738
+ "evolution-review",
739
+ "memory-files",
740
+ "tool-memory",
741
+ "tool-skill-manage"
742
+ ]
743
+ },
744
+ {
745
+ bundle: "@lmzhen/dsh-evolution-host",
746
+ rows: [
747
+ "evolution-approval",
748
+ "evolution-commands",
749
+ "evolution-curator",
750
+ "evolution-merge",
751
+ "evolution-review",
752
+ "memory-files"
753
+ ]
754
+ },
755
+ {
756
+ bundle: "@lmzhen/dsh-evolution-preset",
757
+ rows: [
758
+ "evolution-approval",
759
+ "evolution-commands",
760
+ "evolution-curator",
761
+ "evolution-merge",
762
+ "evolution-review",
763
+ "memory-files",
764
+ "tool-memory",
765
+ "tool-skill-manage"
766
+ ]
767
+ }
768
+ ];
769
+ //#endregion
770
+ //#region ../evolution-client-kit/src/generated/states.generated.ts
771
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
772
+ function Block(props) {
773
+ const tone = props.tone ?? "neutral";
774
+ return (0, react.createElement)("div", {
775
+ "data-tone": tone,
776
+ ...tone === "fail" ? { role: "alert" } : {}
777
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
778
+ }
779
+ /** One classified failure as a reader-facing sentence. */
780
+ function failureText(failure, t) {
781
+ switch (failure.kind) {
782
+ case "unauthenticated": return t("failureUnauthorized");
783
+ case "forbidden": return t("failureForbidden");
784
+ case "unmounted": return t("failureUnmounted");
785
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
786
+ case "refused": return failure.message;
787
+ }
788
+ }
789
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
790
+ function FailureBlock(props) {
791
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
792
+ return Block({
793
+ tone: "fail",
794
+ title: failureText(props.failure, props.t),
795
+ children: hint
796
+ });
797
+ }
798
+ //#endregion
799
+ //#region src/panels/settings/pending-state.ts
800
+ /** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
801
+ const INITIAL_PENDING_STATE = {
802
+ view: { kind: "loading" },
803
+ busy: [],
804
+ notice: null,
805
+ open: null,
806
+ preview: null
807
+ };
808
+ /** Open one row's preview: the row is now the open one and its answer is on the way. */
809
+ function expanded(state, id) {
810
+ return {
811
+ ...state,
812
+ open: id,
813
+ preview: { kind: "loading" }
814
+ };
815
+ }
816
+ /** Close whatever preview is open. */
817
+ function collapsed(state) {
818
+ return {
819
+ ...state,
820
+ open: null,
821
+ preview: null
822
+ };
823
+ }
824
+ /**
825
+ * A preview answer arrived. An answer for a row the reader has already left is DROPPED: it would
826
+ * otherwise paint one row's diff under another row's heading.
827
+ */
828
+ function previewArrived(state, id, view) {
829
+ return state.open === id ? {
830
+ ...state,
831
+ preview: view
832
+ } : state;
833
+ }
834
+ /** The first load, or a refresh after a decision: back to loading, keeping no stale rows. */
835
+ function loading(state) {
836
+ return {
837
+ ...state,
838
+ view: { kind: "loading" }
839
+ };
840
+ }
841
+ /**
842
+ * A list arrived. An empty list is its OWN state — the reader is told the window is clear rather than
843
+ * being shown the same line a slow answer shows.
844
+ */
845
+ function loaded(state, rows) {
846
+ return {
847
+ ...state,
848
+ view: rows.length === 0 ? { kind: "empty" } : {
849
+ kind: "ready",
850
+ rows
851
+ }
852
+ };
853
+ }
854
+ /** The list could not be read: the classified failure stays on screen next to a retry control. */
855
+ function failed(state, failure) {
856
+ return {
857
+ ...state,
858
+ view: {
859
+ kind: "failed",
860
+ failure
861
+ }
862
+ };
863
+ }
864
+ /** Mark one record as having an action in flight (its buttons are disabled until it ends). */
865
+ function beginAction(state, id) {
866
+ return state.busy.includes(id) ? state : {
867
+ ...state,
868
+ busy: [...state.busy, id]
869
+ };
870
+ }
871
+ /** The action ended, whatever it answered. */
872
+ function endAction(state, id) {
873
+ return {
874
+ ...state,
875
+ busy: state.busy.filter((candidate) => candidate !== id)
876
+ };
877
+ }
878
+ /** One refusal or failure line under the list; `null` clears it (a later success does). */
879
+ function noticed(state, message) {
880
+ return {
881
+ ...state,
882
+ notice: message
883
+ };
884
+ }
885
+ //#endregion
886
+ //#region src/panels/settings/PendingCard.ts
887
+ /**
888
+ * The "writes awaiting approval" card: the staged write window, with the two decisions on each row.
889
+ *
890
+ * It is a card in the SAME section and wears the SAME shell as the parameter cards (header with title,
891
+ * count and chevron; body of field blocks; a button row per block), so nothing here invents a second
892
+ * visual language. Two things differ on purpose: the data comes from the host's approval routes rather
893
+ * than the settings transport, and its three states are distinct — "reading", "nothing staged" and "the
894
+ * read failed" each say so in their own words (E7: merging the first two is what made an empty panel
895
+ * look like lost data).
896
+ *
897
+ * Every decision re-reads the window instead of predicting the outcome: the host owns the state, and a
898
+ * refusal is shown with the host's own sentence.
899
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/PendingCard
900
+ */
901
+ /**
902
+ * U-6: the registry's gate word, read out of a section's user layer. ONE definition, exported so the
903
+ * mapping itself is judgeable: the entry does the wiring, this does the reading. Anything that is not
904
+ * one of the two explicit words falls back to the shipped default (`auto`) — a document written by a
905
+ * future version must not silently turn the confirmation ON, and a typo must not turn it off. The
906
+ * independent review flagged that this reader had no case at all (P2-11).
907
+ */
908
+ function confirmModeFrom(user) {
909
+ const value = typeof user === "object" && user !== null ? user.approvalConfirm : void 0;
910
+ return value === "ask" || value === "timeout" ? value : "auto";
911
+ }
912
+ /** Which word one age bucket gets; an unknown bucket falls back to the count form. */
913
+ const AGE_KEYS = Object.freeze({
914
+ now: "approvalAgeNow",
915
+ minutes: "approvalAgeMinutes",
916
+ hours: "approvalAgeHours",
917
+ days: "approvalAgeDays",
918
+ months: "approvalAgeMonths"
919
+ });
920
+ /** Which word one staged kind gets. */
921
+ const KIND_KEYS = Object.freeze({
922
+ memory: "approvalKindMemory",
923
+ skill: "approvalKindSkill",
924
+ capability: "approvalKindCapability"
925
+ });
926
+ /**
927
+ * Render the pending window.
928
+ * @param props - the copy seat and the host calls.
929
+ * @returns the card.
930
+ */
931
+ function PendingCard(props) {
932
+ const { t, api } = props;
933
+ const translate = (key, values) => t(key, values);
934
+ const confirmMode = props.confirmMode?.() ?? "auto";
935
+ const [armed, setArmed] = (0, react.useState)(null);
936
+ const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
937
+ const [open, setOpen] = (0, react.useState)(true);
938
+ const latest = (0, react.useRef)(state);
939
+ const apply = (next) => {
940
+ const value = next(latest.current);
941
+ latest.current = value;
942
+ setState(value);
943
+ };
944
+ /**
945
+ * Read the window the card is about: the writes WAITING and the writes RUNNING.
946
+ *
947
+ * T4-05/A47: the route has always taken `?status=` — the CLI asks for both lists — but this card
948
+ * asked for neither, so a record whose approve died between the claim and the decision showed as
949
+ * "nothing is waiting for a decision" while `/evolution pending` and doctor listed it. The two
950
+ * answers are ordered the way the CLI orders them (waiting first); either one failing fails the
951
+ * read, because a half-list would read as a complete window.
952
+ */
953
+ const readTicket = (0, react.useRef)(0);
954
+ const load = async () => {
955
+ apply(loading);
956
+ const ticket = readTicket.current + 1;
957
+ readTicket.current = ticket;
958
+ const [waiting, running] = await Promise.all([api.pending(), api.pending("executing")]);
959
+ if (readTicket.current !== ticket) return;
960
+ if (!waiting.ok) {
961
+ apply((current) => failed(current, waiting.failure));
962
+ return;
963
+ }
964
+ if (!running.ok) {
965
+ apply((current) => failed(current, running.failure));
966
+ return;
967
+ }
968
+ apply((current) => loaded(current, [...waiting.data, ...running.data]));
969
+ };
970
+ (0, react.useEffect)(() => {
971
+ load();
972
+ const onFocus = () => {
973
+ load();
974
+ };
975
+ window.addEventListener("focus", onFocus);
976
+ return () => {
977
+ window.removeEventListener("focus", onFocus);
978
+ };
979
+ }, []);
980
+ const decide = (id, action) => {
981
+ apply((current) => beginAction(current, id));
982
+ (async () => {
983
+ const answer = action === "approve" ? await api.approve(id) : await api.reject(id);
984
+ if (!answer.ok) {
985
+ apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
986
+ return;
987
+ }
988
+ apply((current) => noticed(endAction(current, id), null));
989
+ await load();
990
+ })();
991
+ };
992
+ const age = (row) => {
993
+ const key = AGE_KEYS[row.age.unit];
994
+ if (key === void 0) return String(row.age.n);
995
+ return t(key, { n: row.age.n });
996
+ };
997
+ /** Ask the host what this record would store; the answer lands only if the row is still open. */
998
+ const loadPreview = async (id) => {
999
+ const answer = await api.preview(id);
1000
+ const view = answer.ok ? answer.data.available ? {
1001
+ kind: "ready",
1002
+ answer: answer.data
1003
+ } : {
1004
+ kind: "unavailable",
1005
+ reason: answer.data.reason
1006
+ } : {
1007
+ kind: "failed",
1008
+ failure: answer.failure
1009
+ };
1010
+ apply((current) => previewArrived(current, id, view));
1011
+ };
1012
+ const togglePreview = (id) => {
1013
+ if (state.open === id) {
1014
+ apply(collapsed);
1015
+ return;
1016
+ }
1017
+ apply((current) => expanded(current, id));
1018
+ loadPreview(id);
1019
+ };
1020
+ /** The facts line, then the source view: the same shape the history panel's diff uses. */
1021
+ const preview = (view) => {
1022
+ if (view.kind === "loading") return (0, react.createElement)("p", {
1023
+ className: "evolution-param-note",
1024
+ role: "status"
1025
+ }, t("approvalPreviewReading"));
1026
+ if (view.kind === "failed") return (0, react.createElement)("p", {
1027
+ className: "evolution-param-error",
1028
+ role: "status"
1029
+ }, failureText(view.failure, translate));
1030
+ if (view.kind === "unavailable") return (0, react.createElement)("p", {
1031
+ className: "evolution-param-error",
1032
+ role: "status"
1033
+ }, t("approvalPreviewNone") + view.reason);
1034
+ const facts = [
1035
+ (0, react.createElement)("span", {
1036
+ className: "evolution-param-fact",
1037
+ key: "added"
1038
+ }, t("approvalDiffAdded", { n: view.answer.linesAdded })),
1039
+ (0, react.createElement)("span", {
1040
+ className: "evolution-param-fact",
1041
+ key: "removed"
1042
+ }, t("approvalDiffRemoved", { n: view.answer.linesRemoved })),
1043
+ ...view.answer.truncated ? [(0, react.createElement)("span", {
1044
+ className: "evolution-param-fact",
1045
+ key: "cut"
1046
+ }, t("approvalDiffCut"))] : []
1047
+ ];
1048
+ const lines = [];
1049
+ for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
1050
+ for (const [index, line] of hunk.oldText.split("\n").entries()) {
1051
+ if (hunk.oldText === "" && line === "") continue;
1052
+ lines.push((0, react.createElement)("div", {
1053
+ key: "old-" + String(hunkIndex) + "-" + String(index),
1054
+ className: "evolution-param-line-del"
1055
+ }, "- " + line));
1056
+ }
1057
+ for (const [index, line] of hunk.newText.split("\n").entries()) {
1058
+ if (hunk.newText === "" && line === "") continue;
1059
+ lines.push((0, react.createElement)("div", {
1060
+ key: "new-" + String(hunkIndex) + "-" + String(index),
1061
+ className: "evolution-param-line-add"
1062
+ }, "+ " + line));
1063
+ }
1064
+ }
1065
+ return (0, react.createElement)("div", null, (0, react.createElement)("p", { className: "evolution-param-facts" }, ...facts), (0, react.createElement)("pre", { className: "evolution-param-diff" }, ...lines.length === 0 ? ["±"] : lines));
1066
+ };
1067
+ const row = (entry) => {
1068
+ if (entry.status === "executing") return (0, react.createElement)("div", {
1069
+ className: "evolution-param-field",
1070
+ key: entry.id
1071
+ }, (0, react.createElement)("div", { className: "evolution-param-label" }, (0, react.createElement)("span", { className: "evolution-param-source" }, t(KIND_KEYS[entry.kind])), (0, react.createElement)("span", { className: "evolution-param-value" }, entry.summary)), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), (0, react.createElement)("p", {
1072
+ className: "evolution-param-note",
1073
+ role: "status"
1074
+ }, t("approvalExecuting")), entry.claimedBy === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-hint" }, entry.claimedBy));
1075
+ const busy = state.busy.includes(entry.id);
1076
+ const button = (label, action, tone) => (0, react.createElement)("button", {
1077
+ className: "evolution-param-button",
1078
+ type: "button",
1079
+ disabled: busy,
1080
+ ...tone === "primary" ? { "data-primary": "true" } : { "data-tone": tone },
1081
+ onClick: () => {
1082
+ if (action === "approve" && confirmMode === "ask" && armed !== entry.id) {
1083
+ setArmed(entry.id);
1084
+ return;
1085
+ }
1086
+ setArmed(null);
1087
+ decide(entry.id, action);
1088
+ }
1089
+ }, busy ? t("approvalLoading") : t(label));
1090
+ return (0, react.createElement)("div", {
1091
+ className: "evolution-param-field",
1092
+ key: entry.id
1093
+ }, (0, react.createElement)("div", { className: "evolution-param-label" }, (0, react.createElement)("span", { className: "evolution-param-source" }, t(KIND_KEYS[entry.kind])), (0, react.createElement)("span", { className: "evolution-param-value" }, entry.summary)), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), armed === entry.id ? (0, react.createElement)("p", {
1094
+ className: "evolution-param-note",
1095
+ role: "status"
1096
+ }, (0, react.createElement)("span", { className: "evolution-param-value" }, t("approvalConfirmNamed", { n: entry.summary })), (0, react.createElement)("button", {
1097
+ className: "evolution-param-button",
1098
+ type: "button",
1099
+ "data-confirm": "yes",
1100
+ "data-primary": "true",
1101
+ onClick: () => {
1102
+ setArmed(null);
1103
+ decide(entry.id, "approve");
1104
+ }
1105
+ }, t("approvalConfirmYes")), (0, react.createElement)("button", {
1106
+ className: "evolution-param-button",
1107
+ type: "button",
1108
+ "data-tone": "ghost",
1109
+ onClick: () => {
1110
+ setArmed(null);
1111
+ }
1112
+ }, t("approvalConfirmNo"))) : null, (0, react.createElement)("div", { className: "evolution-param-actions" }, button("approvalApprove", "approve", "primary"), button("approvalReject", "reject", "outline"), (0, react.createElement)("button", {
1113
+ className: "evolution-param-button",
1114
+ type: "button",
1115
+ "data-tone": "ghost",
1116
+ onClick: () => {
1117
+ togglePreview(entry.id);
1118
+ }
1119
+ }, state.open === entry.id ? t("approvalPreviewClose") : t("approvalPreview"))), state.open === entry.id && state.preview !== null ? preview(state.preview) : null);
1120
+ };
1121
+ const body = () => {
1122
+ if (state.view.kind === "loading") return (0, react.createElement)("p", {
1123
+ className: "evolution-param-note",
1124
+ role: "status"
1125
+ }, t("approvalLoading"));
1126
+ if (state.view.kind === "empty") return (0, react.createElement)("p", {
1127
+ className: "evolution-param-note",
1128
+ role: "status"
1129
+ }, t("approvalEmpty"));
1130
+ if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, FailureBlock({
1131
+ failure: state.view.failure,
1132
+ t: translate,
1133
+ fallback: props.fallback
1134
+ }), (0, react.createElement)("button", {
1135
+ className: "evolution-param-button",
1136
+ type: "button",
1137
+ onClick: () => {
1138
+ load();
1139
+ }
1140
+ }, t("approvalRetry")));
1141
+ return (0, react.createElement)("div", null, ...state.view.rows.map(row), state.notice === null ? null : (0, react.createElement)("p", {
1142
+ className: "evolution-param-error",
1143
+ role: "status"
1144
+ }, state.notice));
1145
+ };
1146
+ const rows = state.view.kind === "ready" ? state.view.rows : [];
1147
+ const waiting = rows.filter((entry) => entry.status !== "executing").length;
1148
+ const running = rows.length - waiting;
1149
+ return (0, react.createElement)("section", { className: "evolution-param-card" }, (0, react.createElement)("button", {
1150
+ className: "evolution-param-head",
1151
+ type: "button",
1152
+ onClick: () => {
1153
+ setOpen(!open);
1154
+ },
1155
+ "aria-expanded": open
1156
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("approvalTitle")), waiting === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("approvalCount", { n: waiting })), running === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("approvalRunningCount", { n: running })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
1157
+ className: "evolution-param-button",
1158
+ type: "button",
1159
+ onClick: () => {
1160
+ load();
1161
+ }
1162
+ }, t("approvalRefresh"))), body()) : null);
1163
+ }
1164
+ //#endregion
1165
+ //#region src/panels/settings/DiagnosticsCard.ts
1166
+ /**
1167
+ * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
1168
+ * `/api/dsh-evolution/diagnostics` route answer.
1169
+ *
1170
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
1171
+ * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
1172
+ * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
1173
+ * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
1174
+ * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
1175
+ * sheet; the runtime window is the whole reading).
1176
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
1177
+ */
1178
+ /** Which word one status gets (the frozen W1 status set). */
1179
+ const STATUS_KEYS = Object.freeze({
1180
+ ok: "diagStatusOk",
1181
+ warn: "diagStatusWarn",
1182
+ fail: "diagStatusFail",
1183
+ unknown: "diagStatusUnknown"
1184
+ });
1185
+ /** The summary word for a badge status (the same frozen set; ok has no count). */
1186
+ const SUMMARY_KEYS = Object.freeze({
1187
+ ok: "diagSummaryOk",
1188
+ warn: "diagSummaryWarn",
1189
+ fail: "diagSummaryFail",
1190
+ unknown: "diagSummaryUnknown"
1191
+ });
1192
+ /** The one status the card leads with: fail > warn > unknown > ok. */
1193
+ function summaryOf(rows) {
1194
+ const count = (status) => rows.filter((row) => row.status === status).length;
1195
+ for (const status of [
1196
+ "fail",
1197
+ "warn",
1198
+ "unknown"
1199
+ ]) {
1200
+ const n = count(status);
1201
+ if (n > 0) return {
1202
+ status,
1203
+ count: n
1204
+ };
1205
+ }
1206
+ return {
1207
+ status: "ok",
1208
+ count: 0
1209
+ };
1210
+ }
1211
+ /**
1212
+ * Render the card.
1213
+ * @param props - the copy seat, the host call and the command fallback.
1214
+ * @returns the card.
1215
+ */
1216
+ function DiagnosticsCard(props) {
1217
+ const { t, api } = props;
1218
+ const translate = (key, values) => t(key, values);
1219
+ const [state, setState] = (0, react.useState)({ phase: "loading" });
1220
+ const [open, setOpen] = (0, react.useState)(true);
1221
+ const ticket = (0, react.useRef)(0);
1222
+ const read = (keepView) => {
1223
+ const current = ticket.current + 1;
1224
+ ticket.current = current;
1225
+ if (!keepView) setState({ phase: "loading" });
1226
+ api.view().then((answer) => {
1227
+ if (ticket.current !== current) return;
1228
+ setState(answer.ok ? {
1229
+ phase: "ready",
1230
+ view: answer.data
1231
+ } : {
1232
+ phase: "failed",
1233
+ failure: answer.failure
1234
+ });
1235
+ });
1236
+ };
1237
+ (0, react.useEffect)(() => {
1238
+ read(false);
1239
+ }, []);
1240
+ const refresh = () => {
1241
+ read(true);
1242
+ };
1243
+ const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
1244
+ const header = (0, react.createElement)("button", {
1245
+ type: "button",
1246
+ className: "evolution-param-head",
1247
+ "aria-expanded": open ? "true" : "false",
1248
+ onClick: () => {
1249
+ setOpen(!open);
1250
+ }
1251
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
1252
+ className: "evo-diag-badge",
1253
+ "data-status": summary.status
1254
+ }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
1255
+ const runtime = (view) => view.runtime.length === 0 ? null : (0, react.createElement)("div", {}, (0, react.createElement)("h3", { className: "evolution-param-group" }, t("diagRuntimeTitle")), (0, react.createElement)("div", { className: "evo-diag-list" }, ...view.runtime.map((row) => (0, react.createElement)("div", {
1256
+ key: "runtime-" + row.key,
1257
+ className: "evo-diag-item",
1258
+ "data-kind": "runtime",
1259
+ "data-status": row.status
1260
+ }, (0, react.createElement)("div", { className: "evo-diag-row-head" }, (0, react.createElement)("span", { className: "evo-diag-dot" }), (0, react.createElement)("span", { className: "evo-diag-label" }, row.label), (0, react.createElement)("span", { className: "evo-diag-status" }, t(STATUS_KEYS[row.status]))), row.detail === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-detail" }, row.detail), row.remedy === void 0 ? null : (0, react.createElement)("span", { className: "evo-diag-remedy" }, t("diagRemedy") + row.remedy)))));
1261
+ const body = () => {
1262
+ if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
1263
+ if (state.phase === "failed") return FailureBlock({
1264
+ failure: state.failure,
1265
+ t: translate,
1266
+ fallback: props.fallback
1267
+ });
1268
+ const view = state.view;
1269
+ return (0, react.createElement)("div", {}, (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagSubtitle")), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
1270
+ type: "button",
1271
+ className: "evolution-param-button",
1272
+ onClick: refresh,
1273
+ "data-action": "diag-refresh"
1274
+ }, t("diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
1275
+ };
1276
+ return (0, react.createElement)("section", {
1277
+ className: "evolution-param-card",
1278
+ "data-card": "diagnostics"
1279
+ }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
1280
+ }
1281
+ //#endregion
1282
+ //#region src/panels/settings/diagnostics-state.ts
1283
+ /**
1284
+ * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
1285
+ * GENERATED route client, and the four-state answer the card renders.
1286
+ *
1287
+ * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
1288
+ * and no computation — it only renders.
1289
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
1290
+ */
1291
+ /**
1292
+ * Build the diagnostics face over one fetch implementation.
1293
+ * @param doFetch - the browser fetch, or a spec stub.
1294
+ */
1295
+ function createDiagnosticsApi(doFetch = fetch) {
1296
+ return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
1297
+ }
1298
+ //#endregion
1299
+ //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
1300
+ /**
1301
+ * The command-face fallback for every Host surface, generated from evolution-commands'
1302
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1303
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
1304
+ */
1305
+ const FALLBACKS = {
1306
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1307
+ "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
1308
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1309
+ merge: { command: "/evolution merge status / pending / approve <id> / report" },
1310
+ diagnostics: {
1311
+ command: "/evolution doctor",
1312
+ platformCli: "dsh --json /evolution doctor"
1313
+ }
1314
+ };
1315
+ //#endregion
1316
+ //#region src/panels/settings/messages.ts
1317
+ /**
1318
+ * Copy for the evolution settings section and its parameter cards.
1319
+ *
1320
+ * The section is registered through the platform's locale seat (`ctx.locale`),
1321
+ * which is the same route the other out-of-repo client plugins take. The earlier
1322
+ * note in this package's README — that an outside package cannot bind the locale
1323
+ * seat — is wrong: `locale.register(namespace, { zh, en })` is the documented
1324
+ * call, and it is what makes the section title and its copy follow the UI
1325
+ * language instead of shipping one hard-coded dictionary.
1326
+ */
1327
+ /** Locale namespace this bundle registers. */
1328
+ const NS$1 = "evolution-settings";
1329
+ /** Chinese copy (the family's primary language). */
1330
+ const zh$1 = {
1331
+ title: "自进化",
1332
+ subtitle: "待你决定的写入在最上面;下面按功能列出你能改的参数,没有列出的由安装时的配置决定。",
1333
+ overridden: "已覆盖",
1334
+ deployment: "默认",
1335
+ loading: "正在读取设置…",
1336
+ unavailable: "这个功能没有启动(对应的插件没有安装)",
1337
+ seatMissing: "这个界面没有提供设置服务,参数只能看不能改",
1338
+ projectionFailed: "这一块读设置时出错,已记入日志;其它参数不受影响",
1339
+ readonly: "本次会话不允许写入设置(临时运行模式)",
1340
+ apply: "保存",
1341
+ reset: "恢复默认值",
1342
+ empty: "这里没有可改项",
1343
+ changed: "{n} 项已覆盖",
1344
+ discard: "放弃修改",
1345
+ saving: "保存中…",
1346
+ resetting: "恢复中…",
1347
+ writeTargetHost: "保存去向:宿主配置(对所有会话生效)",
1348
+ invalidNumber: "需要一个数字",
1349
+ invalidSelect: "不在允许的取值里",
1350
+ invalidAtLeast: "不能小于同组的 {n}",
1351
+ approvalConfirmNamed: "即将批准:{n}",
1352
+ approvalConfirmYes: "确认批准",
1353
+ approvalConfirmNo: "取消",
1354
+ resetTo: "复位为 {value}",
1355
+ refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1356
+ resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
1357
+ cardReview: "会话回顾",
1358
+ cardMemory: "长期记忆",
1359
+ cardToolMemory: "记忆写入",
1360
+ cardCurator: "技能整理",
1361
+ cardSkills: "技能写入规则",
1362
+ cardApproval: "写入审批",
1363
+ cardCommands: "维护扫描",
1364
+ cardMerge: "技能自动合并",
1365
+ approvalCardHint: "写入审批=使用中生成/修改;合并审批=两轮工作流。",
1366
+ mergeCardHint: "写入审批=使用中生成/修改;合并审批=两轮工作流。",
1367
+ approvalTitle: "待批写入",
1368
+ approvalLoading: "正在读取待批项…",
1369
+ approvalEmpty: "没有待批的写入",
1370
+ approvalFailed: "待批项读取失败",
1371
+ approvalRetry: "重试",
1372
+ approvalApprove: "批准",
1373
+ approvalReject: "拒绝",
1374
+ approvalCount: "{n} 项待批",
1375
+ approvalRunningCount: "{n} 项执行中",
1376
+ approvalRefresh: "刷新",
1377
+ approvalExecuting: "执行中:这条的写效果可能已经落地,不要重复批准;要清掉它请用拒绝。",
1378
+ approvalKindMemory: "记忆",
1379
+ approvalKindSkill: "技能",
1380
+ approvalKindCapability: "能力",
1381
+ approvalAgeNow: "刚刚",
1382
+ approvalAgeMinutes: "{n} 分钟前",
1383
+ approvalAgeHours: "{n} 小时前",
1384
+ approvalAgeDays: "{n} 天前",
1385
+ approvalAgeMonths: "{n} 个月前",
1386
+ approvalPreview: "预览",
1387
+ approvalPreviewClose: "收起",
1388
+ approvalPreviewReading: "正在读取预览…",
1389
+ approvalPreviewNone: "看不了预览:",
1390
+ approvalDiffAdded: "{n} 行新增",
1391
+ approvalDiffRemoved: "{n} 行删除",
1392
+ approvalDiffCut: "改动区域已截断",
1393
+ diagTitle: "诊断",
1394
+ diagSubtitle: "运行状态的只读自查;不会改动任何东西。",
1395
+ diagRuntimeTitle: "运行状态",
1396
+ diagSummaryOk: "运行正常",
1397
+ diagSummaryWarn: "{n} 项需要注意",
1398
+ diagSummaryFail: "{n} 项异常",
1399
+ diagSummaryUnknown: "{n} 项待确认",
1400
+ diagRefresh: "刷新",
1401
+ diagRefreshing: "刷新中…",
1402
+ diagStatusOk: "正常",
1403
+ diagStatusWarn: "注意",
1404
+ diagStatusFail: "异常",
1405
+ diagStatusUnknown: "未知",
1406
+ diagRemedy: "建议:",
1407
+ diagNoRows: "没有可显示的运行状态",
1408
+ stateLoading: "正在读取…",
1409
+ failureUnauthorized: "需要登录:重新打开这个页面完成登录后再试。",
1410
+ failureForbidden: "平台拒绝了这个请求(身份不是 owner)。",
1411
+ failureUnmounted: "该功能没有挂载。",
1412
+ failureServer: "读取失败,可以手动重试。",
1413
+ fallbackHint: "可以改用命令:{command}"
1414
+ };
1415
+ /** English copy, key for key with {@link zh}. */
1416
+ const en$1 = {
1417
+ title: "Self-evolution",
1418
+ subtitle: "Writes waiting for your decision come first; the changeable parameters are listed by feature below, and anything not listed comes from the deployment configuration.",
1419
+ overridden: "Overridden",
1420
+ deployment: "default",
1421
+ loading: "reading settings…",
1422
+ unavailable: "this feature is not running (its plugin is not installed)",
1423
+ seatMissing: "this shell serves no settings surface, so the parameters cannot be edited here",
1424
+ projectionFailed: "reading this row failed and the error is in the log; the other parameters are unaffected",
1425
+ readonly: "settings cannot be written in this session (temporary mode)",
1426
+ apply: "Save",
1427
+ reset: "Reset to default",
1428
+ empty: "nothing to change here",
1429
+ changed: "{n} overridden",
1430
+ discard: "Discard",
1431
+ saving: "Saving…",
1432
+ resetting: "Resetting…",
1433
+ writeTargetHost: "Saved to: host configuration (applies to every session)",
1434
+ invalidNumber: "Needs a number",
1435
+ invalidSelect: "Not one of the allowed values",
1436
+ invalidAtLeast: "must not be below {n}",
1437
+ approvalConfirmNamed: "About to approve: {n}",
1438
+ approvalConfirmYes: "Approve",
1439
+ approvalConfirmNo: "Cancel",
1440
+ resetTo: "Restores {value}",
1441
+ refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1442
+ resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
1443
+ cardReview: "Session review",
1444
+ cardMemory: "Long-term memory",
1445
+ cardToolMemory: "Writing memory",
1446
+ cardCurator: "Skill tidy-up",
1447
+ cardSkills: "Skill write rules",
1448
+ cardApproval: "Write approval",
1449
+ cardCommands: "Maintenance scans",
1450
+ cardMerge: "Skill auto-merge",
1451
+ approvalCardHint: "Write approval = generate/modify while in use; merge approval = the two-round workflow.",
1452
+ mergeCardHint: "Write approval = generate/modify while in use; merge approval = the two-round workflow.",
1453
+ approvalTitle: "Writes awaiting approval",
1454
+ approvalLoading: "reading the pending window…",
1455
+ approvalEmpty: "nothing is waiting for a decision",
1456
+ approvalFailed: "the pending window could not be read",
1457
+ approvalRetry: "Retry",
1458
+ approvalApprove: "Approve",
1459
+ approvalReject: "Reject",
1460
+ approvalCount: "{n} waiting",
1461
+ approvalRunningCount: "{n} running",
1462
+ approvalRefresh: "Refresh",
1463
+ approvalExecuting: "Running: this write may already have landed — do not approve it twice; reject it to clear it.",
1464
+ approvalKindMemory: "memory",
1465
+ approvalKindSkill: "skill",
1466
+ approvalKindCapability: "capability",
1467
+ approvalAgeNow: "just now",
1468
+ approvalAgeMinutes: "{n} min ago",
1469
+ approvalAgeHours: "{n} h ago",
1470
+ approvalAgeDays: "{n} d ago",
1471
+ approvalAgeMonths: "{n} mo ago",
1472
+ approvalPreview: "Preview",
1473
+ approvalPreviewClose: "Hide",
1474
+ approvalPreviewReading: "reading the preview…",
1475
+ approvalPreviewNone: "no preview: ",
1476
+ approvalDiffAdded: "{n} lines added",
1477
+ approvalDiffRemoved: "{n} lines removed",
1478
+ approvalDiffCut: "the changed region is truncated",
1479
+ diagTitle: "Diagnostics",
1480
+ diagSubtitle: "A read-only check of the runtime state; nothing here changes anything.",
1481
+ diagRuntimeTitle: "Runtime status",
1482
+ diagSummaryOk: "All healthy",
1483
+ diagSummaryWarn: "{n} need attention",
1484
+ diagSummaryFail: "{n} failing",
1485
+ diagSummaryUnknown: "{n} unconfirmed",
1486
+ diagRefresh: "Refresh",
1487
+ diagRefreshing: "refreshing…",
1488
+ diagStatusOk: "healthy",
1489
+ diagStatusWarn: "needs attention",
1490
+ diagStatusFail: "failing",
1491
+ diagStatusUnknown: "unknown",
1492
+ diagRemedy: "Suggested: ",
1493
+ diagNoRows: "no runtime status to show",
1494
+ stateLoading: "reading…",
1495
+ failureUnauthorized: "Sign-in is required: reopen this page and sign in, then retry.",
1496
+ failureForbidden: "The platform refused this request (not the owner principal).",
1497
+ failureUnmounted: "This feature is not mounted.",
1498
+ failureServer: "The read failed; you can retry manually.",
1499
+ fallbackHint: "Command fallback: {command}"
1500
+ };
1501
+ /**
1502
+ * Card titles per settings namespace. A namespace without an entry falls back to
1503
+ * its raw name, so a Host that registers a new namespace still renders — the
1504
+ * title is presentation, the namespace is the join key.
1505
+ */
1506
+ const NAMESPACE_TITLES = Object.freeze({
1507
+ "evolution-review": "cardReview",
1508
+ "memory-files": "cardMemory",
1509
+ "tool-memory": "cardToolMemory",
1510
+ "evolution-curator": "cardCurator",
1511
+ "tool-skill-manage": "cardSkills",
1512
+ "evolution-approval": "cardApproval",
1513
+ "evolution-commands": "cardCommands",
1514
+ "evolution-merge": "cardMerge"
1515
+ });
1516
+ /**
1517
+ * Resolve one message without the locale seat (tests and the fallback path).
1518
+ * @param key - the message key.
1519
+ * @param language - which dictionary to read; defaults to English (the platform FALLBACK_LOCALE).
1520
+ * @returns the copy this bundle carries.
1521
+ */
1522
+ function message(key, language = "en") {
1523
+ return language === "zh" ? zh$1[key] : en$1[key];
1524
+ }
1525
+ /**
1526
+ * Fill one TEMPLATE's `{slot}` placeholders (G5 C4). The seat's translator takes the values, so the
1527
+ * rule lives here instead of at every call site — `t('changed').replace('{n}', …)` used to be
1528
+ * written seven times across three components. An UNKNOWN slot is left standing: a missing value
1529
+ * must not print "undefined" into the operator's UI.
1530
+ * @param template - the resolved copy, with `{slot}` placeholders.
1531
+ * @param values - substitutions keyed by the slot name inside the braces.
1532
+ * @returns the filled copy.
1533
+ */
1534
+ function fill$1(template, values) {
1535
+ return template.replace(/\{(\w+)\}/g, (match, slot) => {
1536
+ const value = values[slot];
1537
+ return value === void 0 ? match : String(value);
1538
+ });
1539
+ }
1540
+ //#endregion
1541
+ //#region src/panels/settings/guards.ts
1542
+ /**
1543
+ * Is this a non-null, non-array object (what JSON calls an object)?
1544
+ * @param value - the value to test.
1545
+ * @returns whether it is a plain object.
1546
+ */
1547
+ function isRecord(value) {
1548
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1549
+ }
1550
+ //#endregion
1551
+ //#region src/panels/settings/ParamCard.ts
1552
+ /**
1553
+ * One namespace's parameter card.
1554
+ *
1555
+ * Structural local types on purpose: this package is distributed outside the
1556
+ * platform repository and a value import of another plugin is forbidden, so the
1557
+ * settings scope and its snapshot are declared by the shape the renderer hands
1558
+ * over (\`getSnapshot\`/\`subscribe\`, status/value/user/writable).
1559
+ *
1560
+ * header (namespace title, change count, chevron), then one block per field —
1561
+ * header (namespace title, change count, chevron), then one block per field —
1562
+ * label with its unit and source chip, a control HOLDING THE CURRENT VALUE, the
1563
+ * Chinese hint — and one 放弃修改/保存 pair at the card's foot. The control is
1564
+ * typed from the registry (\`control\`), so booleans are switches, enums are
1565
+ * selects and numbers are numeric inputs with the unit shown beside the label.
1566
+ */
1567
+ /**
1568
+ * The NAMESPACE-RELATIVE key of one registry field.
1569
+ *
1570
+ * The E3 registry writes family ids in canonical form (`evolution-approval.pendingTtlMs`),
1571
+ * while the platform settings seat speaks a namespace's own field keys (`pendingTtlMs`) —
1572
+ * its snapshot user layer, its base and its op paths are all relative to the namespace. Sending
1573
+ * the canonical id as an op path makes the Host refuse the write
1574
+ * ("Config field evolution-approval.pendingTtlMs is not volatile", measured 2026-10-19), so the
1575
+ * card translates once, here, and every read/write goes through it.
1576
+ * @param namespace - the settings namespace (Loader row id) the card writes.
1577
+ * @param field - one generated field.
1578
+ * @returns the field's key inside that namespace.
1579
+ */
1580
+ function fieldKeyOf(namespace, field) {
1581
+ const prefix = namespace + ".";
1582
+ return field.id.startsWith(prefix) ? field.id.slice(prefix.length) : field.id;
1583
+ }
1584
+ /** One cell's text: absent reads as an empty control, containers as JSON. */
1585
+ function format(value) {
1586
+ if (value === void 0) return "";
1587
+ if (typeof value === "string") return value;
1588
+ return JSON.stringify(value);
1589
+ }
1590
+ /**
1591
+ * Parse one edited value into what the scope should receive.
1592
+ * @param control - the registry's control kind for the field.
1593
+ * @param text - the text the operator left in the control.
1594
+ * @returns the value handed to \`set\`; a string stays raw so the Host's own
1595
+ * validation produces the error message the operator reads.
1596
+ */
1597
+ function parseFor(control, text) {
1598
+ const trimmed = text.trim();
1599
+ if (control === "switch") return trimmed === "true";
1600
+ if (control === "number") {
1601
+ const parsed = Number(trimmed);
1602
+ return trimmed !== "" && Number.isFinite(parsed) ? parsed : trimmed;
1603
+ }
1604
+ try {
1605
+ return JSON.parse(trimmed);
1606
+ } catch {
1607
+ return trimmed;
1608
+ }
1609
+ }
1610
+ /**
1611
+ * U-4: whether one field's CURRENT text is a value the Host can take. The controls constrain their own
1612
+ * domains (a number input sanitizes, a select only offers listed values), so the reachable case is a
1613
+ * value the registry no longer lists — carried in the user layer and re-staged by any other edit.
1614
+ * @param field - the generated field.
1615
+ * @param text - the text the control holds right now.
1616
+ * @param peer - U-4: the text the row this field declares `atLeast` against currently holds (absent
1617
+ * when the field declares no relation, or when that row is not on this card).
1618
+ * @returns the message key explaining the refusal, or null when the value is acceptable.
1619
+ */
1620
+ function invalidReason(field, text, peer) {
1621
+ const trimmed = text.trim();
1622
+ if (trimmed === "") return null;
1623
+ if (field.control === "number") {
1624
+ if (!Number.isFinite(Number(trimmed))) return "invalidNumber";
1625
+ if (field.atLeast !== "" && peer !== void 0 && peer.trim() !== "" && Number.isFinite(Number(peer))) {
1626
+ if (Number(trimmed) < Number(peer)) return "invalidAtLeast";
1627
+ }
1628
+ return null;
1629
+ }
1630
+ if (field.control === "select") return field.values.includes(trimmed) ? null : "invalidSelect";
1631
+ return null;
1632
+ }
1633
+ /**
1634
+ * U-5: the value a reset would restore, spelled for a reader. Only primitives are shown: the seat's
1635
+ * `base` may be a nested object, and `[object Object]` is worse than saying nothing.
1636
+ * @param value - the seat's base value for one field.
1637
+ * @returns its display string, or null when it is not a primitive.
1638
+ */
1639
+ function resetToOf(value) {
1640
+ if (value === null || value === void 0) return null;
1641
+ if (typeof value === "string") return value;
1642
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1643
+ return null;
1644
+ }
1645
+ /**
1646
+ * The DOM id a field's control, its label and its explanation node all share. ONE definition: the
1647
+ * three call sites used to spell the string out, and the third one (the explanation node) referred to
1648
+ * a local that only existed inside the control — a ReferenceError on every invalid value, which the
1649
+ * unit-level fixture could not see because it never rendered one (independent review, P1-1).
1650
+ */
1651
+ function controlId(field) {
1652
+ return "evolution-param-" + field.id;
1653
+ }
1654
+ /** One field's control, typed from the registry. */
1655
+ function FieldControl(props) {
1656
+ const { field, text } = props;
1657
+ const id = controlId(field);
1658
+ const common = {
1659
+ id,
1660
+ disabled: props.disabled,
1661
+ className: "evolution-param-input",
1662
+ ...props.invalid === null ? {} : {
1663
+ "aria-invalid": "true",
1664
+ "aria-describedby": id + "-invalid"
1665
+ }
1666
+ };
1667
+ if (field.control === "switch") return (0, react.createElement)("input", {
1668
+ ...common,
1669
+ className: "evolution-param-check",
1670
+ type: "checkbox",
1671
+ checked: text === "true",
1672
+ onChange: (event) => {
1673
+ props.onChange(field.id, String(event.target.checked));
1674
+ }
1675
+ });
1676
+ if (field.control === "select") return (0, react.createElement)("select", {
1677
+ ...common,
1678
+ className: "evolution-param-select",
1679
+ value: text,
1680
+ onChange: (event) => {
1681
+ props.onChange(field.id, event.target.value);
1682
+ }
1683
+ }, [field.values.includes(text) || text === "" ? null : (0, react.createElement)("option", {
1684
+ key: text,
1685
+ value: text
1686
+ }, text), ...field.values.map((value, index) => (0, react.createElement)("option", {
1687
+ key: value,
1688
+ value
1689
+ }, field.valueLabels[index] ?? value))]);
1690
+ return (0, react.createElement)("input", {
1691
+ ...common,
1692
+ type: field.control === "number" ? "number" : "text",
1693
+ value: text,
1694
+ onChange: (event) => {
1695
+ props.onChange(field.id, event.target.value);
1696
+ }
1697
+ });
1698
+ }
1699
+ /** One parameter field: label, control, hint, and its reset-to-deployment action. */
1700
+ function FieldBlock(props) {
1701
+ const { field, t } = props;
1702
+ const unit = field.unit === "" ? null : (0, react.createElement)("span", { className: "evolution-param-unit" }, "(" + field.unit + ")");
1703
+ return (0, react.createElement)("div", { className: "evolution-param-field" }, (0, react.createElement)("div", { className: "evolution-param-label" }, (0, react.createElement)("label", { htmlFor: controlId(field) }, field.label === "" ? field.id : field.label), unit, props.overridden ? (0, react.createElement)("span", {
1704
+ className: "evolution-param-source",
1705
+ "data-user": "true"
1706
+ }, t("overridden")) : null), FieldControl(props), (0, react.createElement)("p", { className: "evolution-param-hint" }, field.hint === "" ? field.doc : field.hint), props.invalid === null ? null : (0, react.createElement)("p", {
1707
+ id: controlId(field) + "-invalid",
1708
+ className: "evolution-param-error",
1709
+ role: "status"
1710
+ }, t(props.invalid)), props.overridden ? (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
1711
+ type: "button",
1712
+ className: "evolution-param-button",
1713
+ disabled: props.disabled || props.clearing,
1714
+ onClick: () => {
1715
+ props.onClear(field.id);
1716
+ }
1717
+ }, props.clearing ? t("resetting") : t("reset")), props.resetTo === null ? null : (0, react.createElement)("span", { className: "evolution-param-hint" }, t("resetTo", { value: props.resetTo }))) : null);
1718
+ }
1719
+ /**
1720
+ * Render one row's card body from a supplied snapshot.
1721
+ * @param props - the card data, its callbacks, and the row state to render.
1722
+ * @returns the card.
1723
+ */
1724
+ function ParamCardView(props) {
1725
+ const { t, fields, mutateOnce, clear, namespace, snapshot } = props;
1726
+ const [open, setOpen] = (0, react.useState)(false);
1727
+ const [draft, setDraft] = (0, react.useState)({});
1728
+ const [busy, setBusy] = (0, react.useState)(false);
1729
+ const [error, setError] = (0, react.useState)("");
1730
+ const [pending, setPending] = (0, react.useState)(null);
1731
+ const [clearing, setClearing] = (0, react.useState)(null);
1732
+ /**
1733
+ * Settle a staged write against the Host's answer.
1734
+ *
1735
+ * A resolved write promise is not the verdict: both settings scopes this bundle runs
1736
+ * on (the shell's own controller and the bridge variant) finish their recovery read
1737
+ * BEFORE it resolves, so a value the Host refused is simply still the previous one.
1738
+ * Presence in the user layer therefore cannot answer the question — on a field the
1739
+ * operator had already overridden, a refused value's key is present as well. The
1740
+ * verdict asks whether each staged id now holds the value this card requested.
1741
+ * The check lives in an effect, not in the save callback, because the raw snapshot is
1742
+ * reachable only through the render-time seat — the face's `hooks` compartment never
1743
+ * reaches the component.
1744
+ */
1745
+ (0, react.useEffect)(() => {
1746
+ if (pending === null) return;
1747
+ setPending(null);
1748
+ setBusy(false);
1749
+ setClearing(null);
1750
+ if (!pending.accepted) {
1751
+ setError(t(pending.resets ? "resetRefused" : "refused"));
1752
+ return;
1753
+ }
1754
+ if (!pending.resets) setDraft({});
1755
+ }, [
1756
+ pending,
1757
+ snapshot.user,
1758
+ t
1759
+ ]);
1760
+ if (snapshot.status === "loading") return (0, react.createElement)("p", {
1761
+ className: "evolution-param-note",
1762
+ role: "status"
1763
+ }, t("loading"));
1764
+ if (snapshot.status === "unavailable") return (0, react.createElement)("p", {
1765
+ className: "evolution-param-note",
1766
+ role: "status"
1767
+ }, t(snapshot.reason === "seat-missing" ? "seatMissing" : snapshot.reason === "projection-failed" ? "projectionFailed" : "unavailable"));
1768
+ const user = isRecord(snapshot.user) ? snapshot.user : {};
1769
+ const value = isRecord(snapshot.value) ? snapshot.value : {};
1770
+ const disabled = !snapshot.writable;
1771
+ const locked = disabled || busy || clearing !== null;
1772
+ const titleKey = NAMESPACE_TITLES[namespace];
1773
+ const title = titleKey === void 0 ? namespace : t(titleKey);
1774
+ const hintKey = namespace === "evolution-merge" ? "mergeCardHint" : namespace === "evolution-approval" ? "approvalCardHint" : void 0;
1775
+ const keyOf = (field) => fieldKeyOf(namespace, field);
1776
+ const changed = fields.filter((field) => Object.hasOwn(user, keyOf(field))).length;
1777
+ const textOf = (field) => draft[field.id] ?? format(value[keyOf(field)]);
1778
+ const dirty = fields.filter((field) => draft[field.id] !== void 0 && draft[field.id] !== format(value[keyOf(field)]));
1779
+ const peerTextOf = (field) => {
1780
+ if (field.atLeast === "") return void 0;
1781
+ const related = fields.find((candidate) => candidate.id === field.atLeast);
1782
+ return related === void 0 ? void 0 : textOf(related);
1783
+ };
1784
+ const blocked = fields.some((field) => invalidReason(field, textOf(field), peerTextOf(field)) !== null);
1785
+ const change = (id, next) => {
1786
+ setDraft({
1787
+ ...draft,
1788
+ [id]: next
1789
+ });
1790
+ };
1791
+ /**
1792
+ * Restore one field's deployment default (T5-05/A60).
1793
+ *
1794
+ * The scope's own boolean is NOT the verdict — same reason the save path judges the settled
1795
+ * snapshot instead: a host refusal leaves the user layer untouched, which is precisely what a
1796
+ * structural check sees and a dropped return value does not. A throw (the settings service
1797
+ * rejecting a non-volatile path) used to escape as an unhandled rejection.
1798
+ */
1799
+ const reset = (field) => {
1800
+ setClearing(field);
1801
+ setError("");
1802
+ (async () => {
1803
+ let accepted;
1804
+ try {
1805
+ accepted = await clear(fieldKeyOf(namespace, { id: field }));
1806
+ } catch (caught) {
1807
+ setClearing(null);
1808
+ setError(caught instanceof Error && caught.message !== "" ? caught.message : t("resetRefused"));
1809
+ return;
1810
+ }
1811
+ setPending({
1812
+ accepted,
1813
+ resets: true
1814
+ });
1815
+ })();
1816
+ };
1817
+ const save = () => {
1818
+ setBusy(true);
1819
+ setError("");
1820
+ (async () => {
1821
+ try {
1822
+ setPending({
1823
+ accepted: await mutateOnce(dirty.map((field) => ({
1824
+ op: "set",
1825
+ path: [keyOf(field)],
1826
+ value: parseFor(field.control, textOf(field))
1827
+ }))),
1828
+ resets: false
1829
+ });
1830
+ } catch (caught) {
1831
+ setBusy(false);
1832
+ setError(caught instanceof Error && caught.message !== "" ? caught.message : t("refused"));
1833
+ }
1834
+ })();
1835
+ };
1836
+ return (0, react.createElement)("section", { className: "evolution-param-card" }, (0, react.createElement)("button", {
1837
+ type: "button",
1838
+ className: "evolution-param-head",
1839
+ "aria-expanded": open ? "true" : "false",
1840
+ onClick: () => {
1841
+ setOpen(!open);
1842
+ }
1843
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, title), changed === 0 ? null : (0, react.createElement)("span", { className: "evolution-param-count" }, t("changed", { n: changed })), (0, react.createElement)("span", { className: "evolution-param-dest" }, t("writeTargetHost")), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼")), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, hintKey === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-note" }, t(hintKey)), disabled ? (0, react.createElement)("p", {
1844
+ className: "evolution-param-note",
1845
+ role: "status"
1846
+ }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
1847
+ className: "evolution-param-note",
1848
+ role: "status"
1849
+ }, t("empty")) : fields.flatMap((field, index) => [index === 0 || fields[index - 1]?.group !== field.group ? (0, react.createElement)("h3", {
1850
+ key: "group-" + field.group,
1851
+ className: "evolution-param-group"
1852
+ }, field.group) : null, (0, react.createElement)(FieldBlock, {
1853
+ key: field.id,
1854
+ field,
1855
+ text: textOf(field),
1856
+ overridden: Object.hasOwn(user, keyOf(field)),
1857
+ disabled: locked,
1858
+ clearing: clearing === field.id,
1859
+ resetTo: resetToOf(snapshot.base?.[keyOf(field)]),
1860
+ invalid: invalidReason(field, textOf(field), peerTextOf(field)),
1861
+ t,
1862
+ onChange: change,
1863
+ onClear: reset
1864
+ })]), error === "" ? null : (0, react.createElement)("p", {
1865
+ className: "evolution-param-error",
1866
+ role: "status"
1867
+ }, error), fields.length === 0 ? null : (0, react.createElement)("div", { className: "evolution-param-footer" }, (0, react.createElement)("button", {
1868
+ type: "button",
1869
+ className: "evolution-param-button",
1870
+ disabled: busy || dirty.length === 0 || blocked,
1871
+ onClick: () => {
1872
+ setDraft({});
1873
+ setError("");
1874
+ }
1875
+ }, t("discard")), (0, react.createElement)("button", {
1876
+ type: "button",
1877
+ className: "evolution-param-button",
1878
+ "data-primary": "true",
1879
+ disabled: locked || blocked || dirty.length === 0,
1880
+ onClick: save
1881
+ }, busy ? t("saving") : t("apply")))) : null);
1882
+ }
1883
+ /**
1884
+ * The card as our own section renders it: the row state comes from the hook seat the
1885
+ * renderer binds out of the inject face's `hooks` compartment (components never hold a
1886
+ * subscription of their own).
1887
+ * @param props - the card data, its callbacks, and the bound row hook.
1888
+ * @returns the card.
1889
+ */
1890
+ function ParamCard(props) {
1891
+ return ParamCardView({
1892
+ namespace: props.namespace,
1893
+ fields: props.fields,
1894
+ t: props.t,
1895
+ mutateOnce: props.mutateOnce,
1896
+ clear: props.clear,
1897
+ snapshot: props.useParamSection((state) => state)
1898
+ });
1899
+ }
1900
+ //#endregion
1901
+ //#region src/panels/settings/source.ts
1902
+ /** What a card shows when this UI found no settings seat at all. */
1903
+ const SEAT_MISSING = Object.freeze({
1904
+ status: "unavailable",
1905
+ value: void 0,
1906
+ user: void 0,
1907
+ writable: false,
1908
+ base: void 0,
1909
+ reason: "seat-missing"
1910
+ });
1911
+ /** What a card shows when the seat served the row but projecting it failed. A frozen constant
1912
+ * so a failing row keeps ONE reference: a fresh object per call would reintroduce the
1913
+ * render loop this module exists to prevent. */
1914
+ const PROJECTION_FAILED = Object.freeze({
1915
+ status: "unavailable",
1916
+ value: void 0,
1917
+ user: void 0,
1918
+ writable: false,
1919
+ base: void 0,
1920
+ reason: "projection-failed"
1921
+ });
1922
+ /**
1923
+ * Project one platform snapshot into the card's own shape.
1924
+ * @param snapshot - the seat's snapshot.
1925
+ * @returns the card's snapshot, with the reason an unavailable one is unavailable.
1926
+ */
1927
+ function sectionSnapshotOf(snapshot) {
1928
+ return {
1929
+ status: snapshot.status,
1930
+ value: isRecord(snapshot.value) ? snapshot.value : void 0,
1931
+ user: snapshot.user,
1932
+ writable: snapshot.writable,
1933
+ base: isRecord(snapshot.base) ? snapshot.base : void 0,
1934
+ ...snapshot.status === "unavailable" ? { reason: "not-served" } : {}
1935
+ };
1936
+ }
1937
+ /**
1938
+ * Bind one row's form to a projection that is stable per raw snapshot and never throws.
1939
+ * @param form - the row's client form, as the settings seat hands it over.
1940
+ * @returns the projection the card's hook reads.
1941
+ */
1942
+ function stableProjection(form) {
1943
+ let lastRaw;
1944
+ let lastOut;
1945
+ let reported = false;
1946
+ const failed = (error) => {
1947
+ if (!reported) {
1948
+ reported = true;
1949
+ console.error("[evolution-settings] projecting one row failed:", error);
1950
+ }
1951
+ return PROJECTION_FAILED;
1952
+ };
1953
+ return () => {
1954
+ let raw;
1955
+ try {
1956
+ raw = form.getSnapshot();
1957
+ } catch (error) {
1958
+ return failed(error);
1959
+ }
1960
+ if (lastOut !== void 0 && raw === lastRaw) return lastOut;
1961
+ try {
1962
+ const next = sectionSnapshotOf(raw);
1963
+ lastRaw = raw;
1964
+ lastOut = next;
1965
+ return next;
1966
+ } catch (error) {
1967
+ return failed(error);
1968
+ }
1969
+ };
1970
+ }
1971
+ /**
1972
+ * The live source one row's card binds.
1973
+ * @param probe - the lazily probed settings seat.
1974
+ * @param namespace - the row's entry id.
1975
+ * @returns the source; without a seat it reports the stated empty state, and it re-probes on
1976
+ * every read so a seat that appears later is picked up without a timer.
1977
+ */
1978
+ function sourceFor(probe, namespace) {
1979
+ const listeners = /* @__PURE__ */ new Set();
1980
+ let form;
1981
+ let projected;
1982
+ let offForm;
1983
+ /**
1984
+ * Bind the seat's form for this row.
1985
+ * @param force - re-read the seat even when one is already bound, so a REPLACED seat (the shell
1986
+ * re-composed, a different form object for the same row) is picked up instead of kept stale.
1987
+ */
1988
+ const bind = (force = false) => {
1989
+ if (form !== void 0 && !force) return;
1990
+ const seat = probe();
1991
+ if (seat === void 0) return;
1992
+ const next = seat.get(namespace);
1993
+ if (next === form) return;
1994
+ offForm?.();
1995
+ offForm = void 0;
1996
+ form = next;
1997
+ projected = stableProjection(form);
1998
+ };
1999
+ const attach = () => {
2000
+ if (form === void 0 || offForm !== void 0) return;
2001
+ offForm = form.subscribe(() => {
2002
+ for (const listener of listeners) listener();
2003
+ });
2004
+ };
2005
+ return {
2006
+ getSnapshot: () => {
2007
+ bind();
2008
+ return projected === void 0 ? SEAT_MISSING : projected();
2009
+ },
2010
+ revive: () => {
2011
+ bind(true);
2012
+ attach();
2013
+ for (const listener of listeners) listener();
2014
+ },
2015
+ subscribe: (listener) => {
2016
+ listeners.add(listener);
2017
+ bind();
2018
+ attach();
2019
+ return () => {
2020
+ listeners.delete(listener);
2021
+ if (listeners.size === 0) {
2022
+ offForm?.();
2023
+ offForm = void 0;
2024
+ }
2025
+ };
2026
+ }
2027
+ };
2028
+ }
2029
+ /**
2030
+ * The write pair for one row.
2031
+ * @param probe - the lazily probed settings seat.
2032
+ * @param namespace - the row's entry id.
2033
+ * @returns the two calls a card's save/reset path uses; without a seat they resolve without writing.
2034
+ */
2035
+ function writeFaceFor(probe, namespace) {
2036
+ const form = () => probe()?.get(namespace);
2037
+ return {
2038
+ mutateOnce: async (ops) => {
2039
+ const seat = form();
2040
+ if (seat === void 0) return false;
2041
+ return await seat.mutate(ops);
2042
+ },
2043
+ clear: async (field) => {
2044
+ const seat = form();
2045
+ if (seat === void 0) return false;
2046
+ return await seat.unset(field);
2047
+ }
2048
+ };
2049
+ }
2050
+ /**
2051
+ * One source object per namespace, for the life of the plugin fiber.
2052
+ *
2053
+ * The renderer caches a hook binding per SOURCE OBJECT, so building a source per render
2054
+ * would re-bind the hook and remount the card. The cache is owned by the caller's fiber and
2055
+ * cleared on dispose.
2056
+ * @param probe - the lazily probed settings seat.
2057
+ * @returns the per-namespace source cache and its disposer.
2058
+ */
2059
+ function createSourceCache(probe) {
2060
+ const sources = /* @__PURE__ */ new Map();
2061
+ return {
2062
+ sourceFor: (namespace) => {
2063
+ const existing = sources.get(namespace);
2064
+ if (existing !== void 0) return existing;
2065
+ const source = sourceFor(probe, namespace);
2066
+ sources.set(namespace, source);
2067
+ return source;
2068
+ },
2069
+ revive: () => {
2070
+ for (const source of sources.values()) source.revive();
2071
+ },
2072
+ dispose: () => {
2073
+ sources.clear();
2074
+ }
2075
+ };
2076
+ }
2077
+ //#endregion
2078
+ //#region src/panels/settings/RowConfigCard.ts
2079
+ /**
2080
+ * The Plugins page's per-row configuration entry for a family row.
2081
+ *
2082
+ * The page owns the settings subscription — it re-renders the entry — and passes the row's
2083
+ * form in, so this component renders the SAME card body as our own section, fed by the
2084
+ * page's state instead of the bound hook and therefore holding no subscription of its own.
2085
+ * `summary` answers the page's one-line slot: a row's description fallback, which stays
2086
+ * empty until the row actually overrides something, exactly as the page expects.
2087
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client
2088
+ */
2089
+ /**
2090
+ * Count the keys this row's user layer holds.
2091
+ * @param user - the raw user section, as the page reports it.
2092
+ * @param fields - the row's fields.
2093
+ * @returns how many of them the user overrode.
2094
+ */
2095
+ function overridden(user, fields, namespace) {
2096
+ const layer = isRecord(user) ? user : {};
2097
+ return fields.filter((field) => Object.hasOwn(layer, fieldKeyOf(namespace, field))).length;
2098
+ }
2099
+ /**
2100
+ * Render one row's configuration entry.
2101
+ * @param props - the page's view/form seat plus this row's face.
2102
+ * @returns the entry; `summary` renders nothing for an untouched row (the page then keeps the row's own description).
2103
+ */
2104
+ function RowConfigCard(props) {
2105
+ const { t, fields, namespace, view, form } = props;
2106
+ if (form === void 0) return view === "summary" ? null : (0, react.createElement)("p", {
2107
+ className: "evolution-param-note",
2108
+ role: "status"
2109
+ }, t("seatMissing"));
2110
+ const changed = overridden(form.state.user, fields, namespace);
2111
+ if (view === "summary") return changed === 0 ? null : (0, react.createElement)("span", null, t("changed", { n: changed }));
2112
+ return ParamCardView({
2113
+ namespace,
2114
+ fields,
2115
+ t,
2116
+ snapshot: sectionSnapshotOf(form.state),
2117
+ mutateOnce: async (ops) => form.mutate(ops, form.state.revision),
2118
+ clear: async (field) => form.mutate([{
2119
+ op: "unset",
2120
+ path: [field]
2121
+ }], form.state.revision)
2122
+ });
2123
+ }
2124
+ //#endregion
2125
+ //#region src/panels/settings/seam.ts
2126
+ /** Our card slot, hosted by the section this bundle registers. */
2127
+ const CARD_SLOT = "evolution.namespace.card";
2128
+ /** The Plugins page's per-row configuration seat (declared by `ui-plugin-manager`). */
2129
+ const ROW_CONFIG_SLOT = "plugins.row.config";
2130
+ /**
2131
+ * The key the Plugins page files a row's configuration under.
2132
+ *
2133
+ * Spelled here because the page's own helper cannot be imported (a value import from
2134
+ * another plugin's client half is forbidden):
2135
+ * `ui-plugin-manager`'s config-ledger module is the definition this mirrors.
2136
+ * @param bundle - the bundle package name that declares the row.
2137
+ * @param rowId - the row id that bundle's patch declares.
2138
+ * @returns the `plugins.row.config` key.
2139
+ */
2140
+ function rowConfigKey(bundle, rowId) {
2141
+ return bundle + "#" + rowId;
2142
+ }
2143
+ /**
2144
+ * The pending-window card's slot key.
2145
+ *
2146
+ * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
2147
+ * namespace (its data comes from the host's approval routes), so it carries a key of its own. The constant
2148
+ * lives HERE and not in `index.ts` because the SECTION has to name it too, and `index.ts` already imports
2149
+ * the section — one definition in the module both sides already share is what keeps that pair acyclic.
2150
+ */
2151
+ const PENDING_CARD_KEY = "approval-pending";
2152
+ /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
2153
+ const DIAGNOSTICS_CARD_KEY = "diagnostics";
2154
+ //#endregion
2155
+ //#region src/panels/settings/SettingsSection.ts
2156
+ /**
2157
+ * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
2158
+ * window's card ahead of them all, because it is the only card here that waits on a decision.
2159
+ *
2160
+ * This is the shape the platform's own settings sections use — a heading plus the
2161
+ * child slots it declared in its registration. The cards themselves are separate
2162
+ * registrations keyed by namespace, so a namespace the Host does not serve simply
2163
+ * renders nothing and a deployment that drops this row loses the whole section.
2164
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client
2165
+ */
2166
+ /**
2167
+ * Render the section.
2168
+ * @param face - the shell's inject face: translator and child-slot renderer.
2169
+ * @returns the section element.
2170
+ */
2171
+ function SettingsSection(face) {
2172
+ return (0, react.createElement)("div", { className: "evolution-params" }, (0, react.createElement)("h2", { className: "evolution-params-title" }, face.t("title")), (0, react.createElement)("p", { className: "evolution-params-subtitle" }, face.t("subtitle")), (0, react.createElement)("p", { className: "evolution-params-about" }, "0.32.1 · Web UI umbrella for the evolution family: settings, merge, skill history and skill health panels"), (0, react.createElement)("div", {
2173
+ key: PENDING_CARD_KEY,
2174
+ className: "evolution-params-group"
2175
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: PENDING_CARD_KEY })), (0, react.createElement)("div", {
2176
+ key: DIAGNOSTICS_CARD_KEY,
2177
+ className: "evolution-params-group"
2178
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: DIAGNOSTICS_CARD_KEY })), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
2179
+ key: section_.namespace,
2180
+ className: "evolution-params-group"
2181
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: section_.namespace }))));
2182
+ }
2183
+ //#endregion
2184
+ //#region ../evolution-client-kit/src/tokens.ts
2185
+ /**
2186
+ * The scale, declared on one element so it never leaks into the host shell.
2187
+ * @param scope - the selector the custom properties are declared on.
2188
+ * @returns the declaration block to put in the stylesheet.
2189
+ */
2190
+ function tokenVars(scope) {
2191
+ return scope + "{--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}";
2192
+ }
2193
+ //#endregion
2194
+ //#region src/panels/settings/styles.ts
2195
+ /**
2196
+ * The section's stylesheet.
2197
+ *
2198
+ * The platform's own client bundles compile CSS Modules into a string and inject
2199
+ * it once behind a `<style data-plugin-css="<pkg>/<file>">` tag; this bundle is
2200
+ * built outside that pipeline, so it carries its own string and injects it the
2201
+ * same way. Every colour reads the platform's design tokens (`--dsw-alias-*`), so
2202
+ * the section follows the light and dark themes without owning a colour; every
2203
+ * type step and geometry value reads the family scale (`./tokens.ts`, generated
2204
+ * from `packages/scripts/client-tokens.json`), the same one the history panel
2205
+ * draws from — before it existed this card rooted its type in
2206
+ * `--dsh-content-font-size*` (the CONVERSATION content size, which the platform's
2207
+ * own surfaces never read) and wrote its own paddings, so the family's two client
2208
+ * faces drifted apart (W2/W5/W6).
2209
+ *
2210
+ * The field metrics (8px/10px input padding, 8px radius, 12px/6px paddings and
2211
+ * gaps) follow the platform's own settings fields: `ui-primitives/Input.module.css`
2212
+ * and `ui-settings-plugins/.../fields.module.css` both use an 8px field radius.
2213
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client
2214
+ */
2215
+ /** Tag id that makes the injection idempotent. */
2216
+ const CSS_TAG_ID$1 = "@lmzhen/dsh-evolution-ui/settings.css";
2217
+ /** The stylesheet the section injects once. */
2218
+ const CSS$1 = [
2219
+ tokenVars(".evolution-params,.evolution-param-card"),
2220
+ ".evolution-params{display:flex;flex-direction:column}",
2221
+ ".evolution-params-title{margin:0 0 var(--evo-space-4);color:var(--dsw-alias-label-primary);font-size:var(--evo-type-lg);font-weight:600;line-height:var(--evo-leading-base)}",
2222
+ ".evolution-params-subtitle{margin:0 0 var(--evo-space-16);color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2223
+ ".evolution-params-about{margin:0 0 var(--evo-space-16);color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs)}",
2224
+ ".evolution-param-card{border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-radius:var(--evo-radius-control);background:var(--dsw-alias-bg-layer-1);margin-bottom:var(--evo-space-12)}",
2225
+ ".evolution-param-head{display:flex;align-items:center;gap:var(--evo-space-8);width:100%;padding:var(--evo-space-12) var(--evo-space-14);background:none;border:0;text-align:left;cursor:pointer;color:inherit;font:inherit}",
2226
+ ".evolution-param-head:hover{background:var(--dsw-alias-interactive-bg-hover)}",
2227
+ ".evolution-param-card-title{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:var(--evo-type-sm);font-weight:500;line-height:var(--evo-leading-base)}",
2228
+ ".evolution-param-count{color:var(--dsw-alias-brand-primary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2229
+ ".evolution-param-chevron{color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2230
+ ".evolution-param-body{padding:0 var(--evo-space-14) var(--evo-space-12);border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2231
+ ".evolution-param-dest{color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs)}",
2232
+ ".evolution-param-group{margin:var(--evo-space-12) 0 0;font-size:var(--evo-type-sm);font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:none}",
2233
+ ".evolution-param-field{display:flex;flex-direction:column;gap:var(--evo-space-6);padding:var(--evo-space-12) 0}",
2234
+ ".evolution-param-field+.evolution-param-field{border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2235
+ ".evolution-param-label{display:flex;align-items:center;gap:var(--evo-space-6);color:var(--dsw-alias-label-primary);font-size:var(--evo-type-body);font-weight:500;line-height:var(--evo-leading-base)}",
2236
+ ".evolution-param-unit{color:var(--dsw-alias-label-tertiary);font-weight:400}",
2237
+ ".evolution-param-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-sm);line-height:var(--evo-leading-base)}",
2238
+ ".evolution-param-value{color:var(--dsw-alias-label-secondary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2239
+ ".evolution-param-input{width:100%;box-sizing:border-box;padding:var(--evo-space-8) 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-2);color:var(--dsw-alias-label-primary);font-size:var(--evo-type-body);line-height:var(--evo-leading-base)}",
2240
+ ".evolution-param-input:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
2241
+ ".evolution-param-input:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}",
2242
+ ".evolution-param-source{display:inline-flex;align-items:center;height:var(--evo-space-20);padding:0 var(--evo-space-6);border-radius:var(--evo-radius-chip);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:var(--evo-type-xs);line-height:var(--evo-space-20)}",
2243
+ ".evolution-param-source[data-user=\"true\"]{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground)}",
2244
+ ".evolution-param-actions{display:flex;justify-content:flex-end;gap:var(--evo-space-8);padding-top:var(--evo-space-12)}",
2245
+ ".evolution-param-button{padding:var(--evo-space-6) 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);color:var(--dsw-alias-label-primary);font-size:var(--evo-type-sm);line-height:var(--evo-leading-base);cursor:pointer}",
2246
+ ".evolution-param-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
2247
+ ".evolution-param-button[data-tone=\"outline\"]{border-color:var(--dsw-alias-border-l2)}",
2248
+ ".evolution-param-button[data-tone=\"ghost\"]{border-color:transparent;background:transparent}",
2249
+ ".evolution-param-button[data-primary=\"true\"]{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:transparent}",
2250
+ ".evolution-param-button[data-primary=\"true\"]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}",
2251
+ ".evolution-param-button:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}",
2252
+ ".evolution-param-select{width:100%;box-sizing:border-box;padding:var(--evo-space-8) 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-2);color:var(--dsw-alias-label-primary);font-size:var(--evo-type-sm);line-height:var(--evo-leading-base)}",
2253
+ ".evolution-param-select:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
2254
+ ".evolution-param-select:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}",
2255
+ ".evolution-param-check{width:var(--evo-space-16);height:var(--evo-space-16);margin:0;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}",
2256
+ ".evolution-param-check:disabled{cursor:default;opacity:.5}",
2257
+ ".evolution-param-footer{display:flex;justify-content:flex-end;gap:var(--evo-space-8);padding:var(--evo-space-12) 0 0;border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2258
+ ".evolution-param-note{margin:0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2259
+ ".evolution-param-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2260
+ ".evolution-param-facts{display:flex;flex-wrap:wrap;align-items:baseline;margin:0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2261
+ ".evolution-param-fact+.evolution-param-fact::before{content:\"·\";margin:0 var(--evo-space-6);color:var(--dsw-alias-label-tertiary)}",
2262
+ ".evolution-param-diff{margin:0;padding:var(--evo-space-12) var(--evo-space-16);max-height:var(--evo-cap-block);overflow:auto;border:var(--evo-hairline-width) solid var(--dsw-alias-border-l1);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
2263
+ ".evolution-param-line-add{color:var(--evo-tone-success)}",
2264
+ ".evolution-param-line-del{color:var(--evo-tone-danger)}",
2265
+ ".evolution-param-loading{margin:var(--evo-space-12) 0 0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2266
+ ".evo-diag-badge{display:inline-flex;align-items:center;height:var(--evo-space-20);padding:0 var(--evo-space-8);border-radius:var(--evo-radius-chip);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:var(--evo-type-xs);line-height:var(--evo-space-20);flex:none}",
2267
+ ".evo-diag-badge[data-status=\"ok\"]{color:var(--dsw-alias-state-success-primary)}",
2268
+ ".evo-diag-badge[data-status=\"warn\"]{color:var(--dsw-alias-state-warn-primary)}",
2269
+ ".evo-diag-badge[data-status=\"fail\"]{color:var(--dsw-alias-state-error-primary)}",
2270
+ ".evo-diag-badge[data-status=\"unknown\"]{color:var(--dsw-alias-state-idle-primary)}",
2271
+ ".evo-diag-list{display:flex;flex-direction:column}",
2272
+ ".evo-diag-item{padding:var(--evo-space-12) 0}",
2273
+ ".evo-diag-item+.evo-diag-item{border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2274
+ ".evo-diag-label{display:block;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2275
+ ".evo-diag-row-head{display:flex;align-items:baseline;gap:var(--evo-space-6);flex-wrap:wrap}",
2276
+ ".evo-diag-dot{width:var(--evo-space-6);height:var(--evo-space-6);border-radius:var(--evo-radius-pill);background:var(--evo-diag-tone,var(--dsw-alias-state-idle-primary));flex:none}",
2277
+ ".evo-diag-status{color:var(--evo-diag-tone,var(--dsw-alias-state-idle-primary));font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2278
+ ".evo-diag-item[data-status=\"ok\"]{--evo-diag-tone:var(--dsw-alias-state-success-primary)}",
2279
+ ".evo-diag-item[data-status=\"warn\"]{--evo-diag-tone:var(--dsw-alias-state-warn-primary)}",
2280
+ ".evo-diag-item[data-status=\"fail\"]{--evo-diag-tone:var(--dsw-alias-state-error-primary)}",
2281
+ ".evo-diag-item[data-status=\"unknown\"]{--evo-diag-tone:var(--dsw-alias-state-idle-primary)}",
2282
+ ".evo-diag-detail{display:block;margin:var(--evo-space-2) 0 0;color:var(--dsw-alias-label-secondary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base);overflow-wrap:anywhere}",
2283
+ ".evo-diag-remedy{display:block;margin:var(--evo-space-4) 0 0;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}"
2284
+ ].join("\n");
2285
+ /**
2286
+ * Inject the stylesheet once per document.
2287
+ *
2288
+ * S5-08: it RETURNS the disposer. The tag check alone answered only half the question - "is it
2289
+ * there" - and left "who takes it away" to nobody, so a hot reload (unmount, remount) found the old
2290
+ * tag, skipped the injection, and kept serving the PREVIOUS bundle CSS. skill-history hands its
2291
+ * disposer back to the fiber; this is the same shape for the same concern.
2292
+ * @param host - the document to inject into; defaults to the global one.
2293
+ * @returns the disposal; a no-op when there is no document or the tag was already present.
2294
+ */
2295
+ function injectStyles$1(host) {
2296
+ const doc = host ?? globalThis.document;
2297
+ if (doc === void 0) return () => {};
2298
+ if (doc.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-ui/settings.css\"]") !== null) return () => {};
2299
+ const tag = doc.createElement("style");
2300
+ tag.dataset.plugin = "@lmzhen/dsh-evolution-ui";
2301
+ tag.dataset.pluginCss = CSS_TAG_ID$1;
2302
+ tag.textContent = CSS$1;
2303
+ doc.head.appendChild(tag);
2304
+ return () => {
2305
+ tag.remove();
2306
+ };
2307
+ }
2308
+ //#endregion
2309
+ //#region src/panels/settings/index.ts
2310
+ /** Section id and nav order (the platform's own sections sit at 0/10/15/20). */
2311
+ const SECTION_ID = "evolution";
2312
+ /**
2313
+ * Register the locale namespace, the section, its cards, and the per-row configuration entries.
2314
+ * @param ctx - client cordis context.
2315
+ */
2316
+ function apply$2(ctx) {
2317
+ ctx.effect(() => injectStyles$1(), "evolution-settings-ui: styles");
2318
+ const seam = ctx;
2319
+ let registered = false;
2320
+ ctx.effect(() => {
2321
+ try {
2322
+ const dispose = seam.locale.register(NS$1, {
2323
+ zh: zh$1,
2324
+ en: en$1
2325
+ });
2326
+ registered = true;
2327
+ return dispose;
2328
+ } catch (error) {
2329
+ ctx.logger.warn(`evolution-settings-ui: the locale seat refused the "${NS$1}" dictionary registration (${error instanceof Error ? error.message : String(error)}) — the cards fall back to the bundle's own copy`);
2330
+ return () => {};
2331
+ }
2332
+ }, "evolution-settings: dictionaries");
2333
+ let bound = null;
2334
+ const translate = (key) => {
2335
+ if (!registered) return message(key);
2336
+ if (bound === null) try {
2337
+ bound = seam.locale.bind(NS$1);
2338
+ } catch {
2339
+ return message(key);
2340
+ }
2341
+ return bound(key);
2342
+ };
2343
+ const t = (key, values) => values === void 0 ? translate(key) : fill$1(translate(key), values);
2344
+ const probe = () => ctx.get("configForms");
2345
+ const sources = createSourceCache(probe);
2346
+ ctx.effect(() => () => {
2347
+ sources.dispose();
2348
+ }, "evolution-settings: sources");
2349
+ const approvalSource = sources.sourceFor("evolution-approval");
2350
+ const confirmMode = () => confirmModeFrom(approvalSource.getSnapshot().user);
2351
+ ctx.inject?.(["configForms"], () => {
2352
+ sources.revive();
2353
+ });
2354
+ seam.slots.inject("settings.section", function* () {
2355
+ yield seam.slots.register({
2356
+ name: "settings.section",
2357
+ id: SECTION_ID,
2358
+ order: 25,
2359
+ label: () => t("title"),
2360
+ locale: NS$1,
2361
+ children: { [CARD_SLOT]: {
2362
+ kind: "keyed",
2363
+ scope: "root"
2364
+ } }
2365
+ }, SettingsSection);
2366
+ });
2367
+ seam.slots.inject(CARD_SLOT, function* () {
2368
+ for (const section of CLIENT_PARAM_SECTIONS) {
2369
+ const source = sources.sourceFor(section.seatKey);
2370
+ const { mutateOnce, clear } = writeFaceFor(probe, section.namespace);
2371
+ const face = () => ({
2372
+ namespace: section.namespace,
2373
+ fields: section.fields,
2374
+ t,
2375
+ mutateOnce,
2376
+ clear,
2377
+ hooks: { paramSection: source }
2378
+ });
2379
+ yield seam.slots.register({
2380
+ name: CARD_SLOT,
2381
+ key: section.namespace,
2382
+ inject: face
2383
+ }, ParamCard);
2384
+ }
2385
+ const api = createApprovalApi((input, init) => fetch(input, init));
2386
+ yield seam.slots.register({
2387
+ name: CARD_SLOT,
2388
+ key: PENDING_CARD_KEY,
2389
+ inject: () => ({
2390
+ t,
2391
+ api,
2392
+ confirmMode,
2393
+ fallback: FALLBACKS.approval
2394
+ })
2395
+ }, PendingCard);
2396
+ const diagApi = createDiagnosticsApi((input, init) => fetch(input, init));
2397
+ yield seam.slots.register({
2398
+ name: CARD_SLOT,
2399
+ key: DIAGNOSTICS_CARD_KEY,
2400
+ inject: () => ({
2401
+ t,
2402
+ api: diagApi,
2403
+ fallback: FALLBACKS.diagnostics
2404
+ })
2405
+ }, DiagnosticsCard);
2406
+ });
2407
+ seam.slots.inject(ROW_CONFIG_SLOT, function* () {
2408
+ for (const seat of CLIENT_ROW_SEATS) for (const rowId of seat.rows) {
2409
+ const section = CLIENT_PARAM_SECTIONS.find((candidate) => candidate.namespace === rowId);
2410
+ if (section === void 0) continue;
2411
+ const face = () => ({
2412
+ namespace: rowId,
2413
+ fields: section.fields,
2414
+ t
2415
+ });
2416
+ yield seam.slots.register({
2417
+ name: ROW_CONFIG_SLOT,
2418
+ key: rowConfigKey(seat.bundle, rowId),
2419
+ inject: face
2420
+ }, RowConfigCard);
2421
+ }
2422
+ });
2423
+ }
2424
+ //#endregion
2425
+ //#region ../evolution-client-kit/src/panel.ts
2426
+ function interpolate(text, values) {
2427
+ if (values === void 0) return text;
2428
+ return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
2429
+ }
2430
+ function injectStyles(doc, css, tagId) {
2431
+ if (doc === void 0) return () => {};
2432
+ if (doc.querySelector("style[data-plugin-css=\"" + tagId + "\"]") !== null) return () => {};
2433
+ const tag = doc.createElement("style");
2434
+ tag.setAttribute("data-plugin-css", tagId);
2435
+ tag.textContent = css;
2436
+ doc.head.append(tag);
2437
+ return () => {
2438
+ tag.remove();
2439
+ };
2440
+ }
2441
+ /**
2442
+ * Register one sidebar row and its main body under one lifecycle.
2443
+ * @param ctx - the client context carrying the slots and locale seats.
2444
+ * @param spec - the feature-specific row/body descriptor.
2445
+ */
2446
+ function registerSidebarPanel(ctx, spec) {
2447
+ const seam = ctx;
2448
+ let registered = false;
2449
+ ctx.effect(() => {
2450
+ try {
2451
+ const dispose = seam.locale.register(spec.namespace, spec.dictionaries);
2452
+ registered = true;
2453
+ return dispose;
2454
+ } catch (error) {
2455
+ ctx.logger.warn(spec.namespace + ": the locale seat refused the dictionary (" + String(error) + "); the panel falls back to the bundle copy");
2456
+ return () => {};
2457
+ }
2458
+ }, spec.namespace + ": locale");
2459
+ ctx.effect(() => injectStyles(typeof document === "undefined" ? void 0 : document, spec.styles.css, spec.styles.tagId), spec.namespace + ": styles");
2460
+ const fallback = spec.dictionaries[spec.fallback ?? "zh"];
2461
+ const lookup = (key) => {
2462
+ const own = fallback[key] ?? key;
2463
+ if (!registered) return own;
2464
+ try {
2465
+ return seam.locale.bind(spec.namespace)(key);
2466
+ } catch {
2467
+ return own;
2468
+ }
2469
+ };
2470
+ const t = (key, values) => interpolate(lookup(key), values);
2471
+ const label = () => lookup("entry.label");
2472
+ seam.slots.inject("sidebar.panellist", function* () {
2473
+ yield seam.slots.register({
2474
+ name: "sidebar.panellist",
2475
+ id: spec.id,
2476
+ order: spec.order,
2477
+ label
2478
+ }, spec.icon);
2479
+ yield seam.slots.register({
2480
+ name: "main",
2481
+ key: spec.id,
2482
+ inject: () => spec.body(t)
2483
+ }, spec.component);
2484
+ });
2485
+ }
2486
+ //#endregion
2487
+ //#region src/panels/history/client/api.ts
2488
+ /**
2489
+ * The host routes this bundle calls, and the ONE place a request is made.
2490
+ *
2491
+ * G5/W7: the paths come from the generated client route table (rendered from evolution-core
2492
+ * HOST_ROUTES), and the transport classifier is the shared generated route-client — the browser half
2493
+ * can no longer drift from the host table or grow a second HTTP vocabulary.
2494
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2495
+ */
2496
+ /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
2497
+ const HOST_ROUTES = ROUTES["skill-history"];
2498
+ /**
2499
+ * A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
2500
+ * four-state failure so the panel can render the shared failure block (G5/W7 S6b).
2501
+ */
2502
+ var SkillHistoryRefusal = class extends Error {
2503
+ failure;
2504
+ constructor(failure) {
2505
+ super(failureMessage(failure));
2506
+ this.name = "SkillHistoryRefusal";
2507
+ this.failure = failure;
2508
+ }
2509
+ };
2510
+ /**
2511
+ * Build the panel's face over one fetch implementation.
2512
+ * @param fetchImpl - the fetch to use; a spec passes a stub.
2513
+ * @returns the three callbacks the component receives through its inject face.
2514
+ */
2515
+ function createSkillHistoryApi(fetchImpl = fetch) {
2516
+ const request = async (path, init) => {
2517
+ const answer = await requestJson(fetchImpl, path, init);
2518
+ if (!answer.ok) throw new SkillHistoryRefusal(answer.failure);
2519
+ return answer.data;
2520
+ };
2521
+ return {
2522
+ skills: async () => await request(HOST_ROUTES.skills),
2523
+ versions: async (name) => await request(HOST_ROUTES.versions + "?name=" + encodeURIComponent(name)),
2524
+ diff: async (name, v) => await request(HOST_ROUTES.diff + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2525
+ body: async (name, v) => await request(HOST_ROUTES.body + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2526
+ undo: async (name, v, unitId) => {
2527
+ return (await request(HOST_ROUTES.undo, {
2528
+ method: "POST",
2529
+ headers: { "content-type": "application/json" },
2530
+ body: JSON.stringify(unitId !== void 0 && unitId !== "" ? {
2531
+ name,
2532
+ unitId
2533
+ } : v === void 0 ? { name } : {
2534
+ name,
2535
+ v
2536
+ })
2537
+ })).message;
2538
+ }
2539
+ };
2540
+ }
2541
+ //#endregion
2542
+ //#region src/panels/history/client/messages.ts
2543
+ /**
2544
+ * Panel copy, in a zh/en dictionary registered through the platform’s locale seat.
2545
+ *
2546
+ * Client copy is locale-owned: the entry label, the group headings, the actions, the version facts
2547
+ * (action words, relative time, deltas) and the status lines all resolve here, and the panel carries
2548
+ * no literal string of its own. Facts come from the host; only the WORDS live here.
2549
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2550
+ */
2551
+ /** The locale namespace this bundle registers. */
2552
+ const NS = "evolution-skill-history";
2553
+ /** Chinese dictionary. */
2554
+ const zh = {
2555
+ "entry.label": "技能历史",
2556
+ title: "技能历史",
2557
+ hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。内容相同的版本会同时标「当前」。",
2558
+ search: "按名字或描述筛选",
2559
+ "search.none": "没有匹配的技能。",
2560
+ "group.content": "正文版本",
2561
+ "group.support": "文件版本(不可回退)",
2562
+ "group.support.note": "附带文件的字节与正文共用一份索引;回退不恢复它们。",
2563
+ "empty.skills": "还没有技能留下过内容版本。",
2564
+ "empty.versions": "这个技能还没有记录版本。",
2565
+ "empty.versions.failed": "版本读取失败——这个技能有没有版本,现在判断不了;点「刷新」重试。",
2566
+ "empty.pick": "从左栏选一个技能,看它记录过的版本。",
2567
+ loading: "读取中…",
2568
+ refresh: "刷新",
2569
+ undo: "回退到此版",
2570
+ "failure.unauthorized": "需要登录:重新打开这个页面完成登录后再试。",
2571
+ "failure.forbidden": "平台拒绝了这个请求(身份不是 owner)。",
2572
+ "failure.unmounted": "该功能没有挂载。",
2573
+ "failure.server": "读取失败,可以重试。",
2574
+ "fallback.hint": "可以改用命令:{command}",
2575
+ "undo.confirm": "确认回退",
2576
+ "undo.no": "取消",
2577
+ current: "当前",
2578
+ "versions.count": "{n} 个版本",
2579
+ "row.version": "v{n}",
2580
+ "row.first": "初始版本",
2581
+ "row.summary.missing": "(未生成摘要)",
2582
+ "row.delta.up": "(+{n} 字符)",
2583
+ "row.delta.down": "(-{n} 字符)",
2584
+ "diff.show": "差异",
2585
+ "diff.source": "源码",
2586
+ "preview.show": "预览",
2587
+ "preview.hide": "收起预览",
2588
+ "preview.truncated": "正文过长:只显示前 {shown} 个字符(共 {total} 个)",
2589
+ "path.unrecorded": "(路径未记录)",
2590
+ "diff.rendered": "渲染",
2591
+ "markdown.copy": "复制",
2592
+ "markdown.copied": "已复制",
2593
+ "markdown.footnotes": "脚注",
2594
+ "diff.hide": "收起差异",
2595
+ "diff.loading": "读取差异…",
2596
+ "diff.against": "与 v{n} 比较",
2597
+ "diff.first": "这是第一版,整篇都是新增",
2598
+ "diff.truncated": "差异过长,只显示前一部分",
2599
+ "diff.added": "新增 {n} 行",
2600
+ "diff.removed": "删除 {n} 行",
2601
+ "state.managed": "家族管理",
2602
+ "state.foreign": "非家族管理",
2603
+ "state.protected": "受保护",
2604
+ "state.unknown": "标记不可读",
2605
+ "action.baseline": "初始快照",
2606
+ "action.create": "新建",
2607
+ "action.patch": "定点修改",
2608
+ "action.update": "整篇更新",
2609
+ "action.restore": "回退",
2610
+ "action.delete": "删除",
2611
+ "action.delete.note": "这一版存的是被删掉的正文",
2612
+ "action.archive": "归档",
2613
+ "action.archive.note": "这一版存的是被归档的正文",
2614
+ "action.consolidate": "合并",
2615
+ "action.restructure": "结构调整",
2616
+ "action.extract": "跨技能提取",
2617
+ "action.support-write": "写入附带文件",
2618
+ "action.support-remove": "删除附带文件",
2619
+ "action.other": "其他写入",
2620
+ "time.now": "刚刚",
2621
+ "time.minutes": "{n} 分钟前",
2622
+ "time.minutes.one": "1 分钟前",
2623
+ "time.hours": "{n} 小时前",
2624
+ "time.hours.one": "1 小时前",
2625
+ "time.days": "{n} 天前",
2626
+ "time.days.one": "1 天前",
2627
+ "time.months": "{n} 个月前",
2628
+ "time.months.one": "1 个月前",
2629
+ "time.years": "{n} 年前",
2630
+ "time.years.one": "1 年前",
2631
+ error: "读取失败",
2632
+ "error.hint": "面板读不到宿主路由:通常是插件版本与宿主不匹配。请重启 DeepSeek Harness 后重试;仍不行就在对话里说明。"
2633
+ };
2634
+ /** English dictionary: the same keys, or the panel falls back to the key itself. */
2635
+ const en = {
2636
+ "entry.label": "Skill history",
2637
+ title: "Skill history",
2638
+ hint: "The content versions every skill write leaves behind. Restoring changes the SKILL.md body only: markers, usage counts and curation state stay as they are. Versions holding the same content are all marked “current”.",
2639
+ search: "Filter by name or description",
2640
+ "search.none": "No skill matches.",
2641
+ "group.content": "Body versions",
2642
+ "group.support": "File versions (not restorable)",
2643
+ "group.support.note": "Support-file bytes share this index with the body; restoring the body does not bring them back.",
2644
+ "empty.skills": "No skill has recorded a content version yet.",
2645
+ "empty.versions": "This skill has no recorded versions.",
2646
+ "empty.versions.failed": "The version read FAILED - whether this skill has versions is unknown right now; use Refresh to retry.",
2647
+ "empty.pick": "Pick a skill on the left to see the versions it recorded.",
2648
+ loading: "Loading…",
2649
+ refresh: "Refresh",
2650
+ undo: "Restore this version",
2651
+ "failure.unauthorized": "Sign-in is required: reopen this page and sign in, then retry.",
2652
+ "failure.forbidden": "The platform refused this request (not the owner principal).",
2653
+ "failure.unmounted": "This feature is not mounted.",
2654
+ "failure.server": "The read failed; you can retry.",
2655
+ "fallback.hint": "Command fallback: {command}",
2656
+ "undo.confirm": "Confirm restore",
2657
+ "undo.no": "Cancel",
2658
+ current: "Current",
2659
+ "versions.count": "{n} versions",
2660
+ "row.version": "v{n}",
2661
+ "row.first": "First version",
2662
+ "row.summary.missing": "(no summary generated)",
2663
+ "row.delta.up": "(+{n} chars)",
2664
+ "row.delta.down": "(-{n} chars)",
2665
+ "diff.show": "Diff",
2666
+ "diff.source": "Source",
2667
+ "preview.show": "Preview",
2668
+ "preview.hide": "Hide preview",
2669
+ "preview.truncated": "the body is long: showing the first {shown} of {total} characters",
2670
+ "path.unrecorded": "(path not recorded)",
2671
+ "diff.rendered": "Rendered",
2672
+ "markdown.copy": "Copy",
2673
+ "markdown.copied": "Copied",
2674
+ "markdown.footnotes": "Footnotes",
2675
+ "diff.hide": "Hide diff",
2676
+ "diff.loading": "Loading the diff…",
2677
+ "diff.against": "compared with v{n}",
2678
+ "diff.first": "first version: everything is new",
2679
+ "diff.truncated": "the change is long; only its beginning is shown",
2680
+ "diff.added": "{n} lines added",
2681
+ "diff.removed": "{n} lines removed",
2682
+ "state.managed": "family-managed",
2683
+ "state.foreign": "not family-managed",
2684
+ "state.protected": "protected",
2685
+ "state.unknown": "marker unreadable",
2686
+ "action.baseline": "initial snapshot",
2687
+ "action.create": "created",
2688
+ "action.patch": "targeted edit",
2689
+ "action.update": "whole-body update",
2690
+ "action.restore": "restored",
2691
+ "action.delete": "deleted",
2692
+ "action.delete.note": "this version holds the removed body",
2693
+ "action.archive": "archived",
2694
+ "action.archive.note": "this version holds the archived body",
2695
+ "action.consolidate": "merged",
2696
+ "action.restructure": "restructured",
2697
+ "action.extract": "extracted across skills",
2698
+ "action.support-write": "support file written",
2699
+ "action.support-remove": "support file removed",
2700
+ "action.other": "other write",
2701
+ "time.now": "just now",
2702
+ "time.minutes": "{n} minutes ago",
2703
+ "time.minutes.one": "1 minute ago",
2704
+ "time.hours": "{n} hours ago",
2705
+ "time.hours.one": "1 hour ago",
2706
+ "time.days": "{n} days ago",
2707
+ "time.days.one": "1 day ago",
2708
+ "time.months": "{n} months ago",
2709
+ "time.months.one": "1 month ago",
2710
+ "time.years": "{n} years ago",
2711
+ "time.years.one": "1 year ago",
2712
+ error: "Could not load",
2713
+ "error.hint": "The panel could not reach the host routes — usually a plugin/host version mismatch. Restart DeepSeek Harness and retry; if it persists, ask in the conversation."
2714
+ };
2715
+ /**
2716
+ * Fill one TEMPLATE’s `{n}` slots. The seat resolves the key, this fills it, so the panel needs
2717
+ * only the seat plus this rule and never learns which language it is rendering.
2718
+ * @param template - the copy, with `{slot}` placeholders.
2719
+ * @param values - the substitutions, keyed by the slot name inside the braces.
2720
+ * @returns the filled copy.
2721
+ */
2722
+ function fill(template, values) {
2723
+ return template.replace(/\{(\w+)\}/g, (match, slot) => {
2724
+ const value = values[slot];
2725
+ return value === void 0 ? match : String(value);
2726
+ });
2727
+ }
2728
+ //#endregion
2729
+ //#region src/panels/history/client/styles.ts
2730
+ /**
2731
+ * The panel’s stylesheet, injected once behind a `<style data-plugin-css=…>` tag.
2732
+ *
2733
+ * TWO FACES, ONE SCALE. The TOOL face (`.evo-hist-*`) is the compact list and its controls; the
2734
+ * READING face (`.evo-doc-*`, at the end) is a version rendered as a document. Each face owns its
2735
+ * rules and they never overlap: the list may be dense and full of controls, a document may not. That
2736
+ * is the whole reason for two prefixes — a rule that serves one face can no longer fight the other’s
2737
+ * comfort, and a third face would be another block rather than an edit here (§15.2 L2b, W22).
2738
+ *
2739
+ * Every colour is a platform design token; every type step and every geometry value is a family scale
2740
+ * token (`./tokens.ts`, generated from `packages/scripts/client-tokens.json`), so the two client
2741
+ * halves of this family cannot drift apart.
2742
+ *
2743
+ * The metrics themselves are COPIED from the platform’s own components rather than invented:
2744
+ *
2745
+ * - the code block (`--dsw-font-markdown-code-block-small`, `0.5px` border, `12px` padding, radius
2746
+ * `12px`, `max-height: 260px`) is the platform command card’s `<pre>` (client/ui-chat/…/GenericCommandCard.module.css);
2747
+ * - hover / focus (`--dsw-alias-interactive-bg-hover`, a 2px inset focus ring) follow the platform
2748
+ * sidebar row (client/ui-sidebar/…/SidebarRoot.module.css);
2749
+ * - the field radius (`8px`) is the platform’s own Input primitive (client/ui-primitives/Input.module.css);
2750
+ * - the panel’s type steps come from the family scale, which is relative to the shell’s chrome size —
2751
+ * NOT the setting behind `--dsh-content-font-size*` (that one says it affects conversation content
2752
+ * only, and the platform’s own surfaces never read it). The step is relative, so the panel scales
2753
+ * with the shell, not with the conversation: the dense list needs it — at the raw chrome size
2754
+ * `dsh-evolution-maintenance` truncates in 264px.
2755
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2756
+ */
2757
+ /** Tag id that makes the injection idempotent. */
2758
+ const CSS_TAG_ID = "@lmzhen/dsh-evolution-ui/panel.css";
2759
+ /** The stylesheet the panel injects once. */
2760
+ const CSS = [
2761
+ tokenVars(".evo-hist-root"),
2762
+ ".evo-hist-root{display:flex;height:100%;min-height:0;min-width:0;font-size:var(--evo-type-sm);color:var(--dsw-alias-label-primary)}",
2763
+ ".evo-hist-aside{display:flex;flex-direction:column;flex:0 0 var(--evo-cap-aside);width:var(--evo-cap-aside);min-height:0;border-right:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2764
+ ".evo-hist-search{padding:var(--evo-space-12) var(--evo-space-12) var(--evo-space-4)}",
2765
+ ".evo-hist-search input{width:100%;box-sizing:border-box;padding:var(--evo-space-6) 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-2);color:var(--dsw-alias-label-primary);font:inherit}",
2766
+ ".evo-hist-search input:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
2767
+ ".evo-hist-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--evo-space-4) var(--evo-space-12) var(--evo-space-10)}",
2768
+ ".evo-hist-empty{padding:var(--evo-space-6) var(--evo-space-8);margin:0;color:var(--dsw-alias-label-tertiary)}",
2769
+ ".evo-hist-skill{display:block;width:100%;box-sizing:border-box;padding:var(--evo-space-6) var(--evo-space-8);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}",
2770
+ ".evo-hist-skill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
2771
+ ".evo-hist-skill:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
2772
+ ".evo-hist-skill[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
2773
+ ".evo-hist-cell-line{display:flex;align-items:baseline;gap:var(--evo-space-6);min-width:0}",
2774
+ ".evo-hist-cell-meta{margin-top:var(--evo-space-2)}",
2775
+ ".evo-hist-cell{min-width:0;font-variant-numeric:tabular-nums}",
2776
+ ".evo-hist-slot-lead{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
2777
+ ".evo-hist-slot-meta{flex:none;white-space:nowrap}",
2778
+ ".evo-hist-slot-aside{flex:none;margin-left:auto;white-space:nowrap}",
2779
+ ".evo-hist-tone-primary{color:inherit}",
2780
+ ".evo-hist-tone-secondary{color:var(--dsw-alias-label-secondary)}",
2781
+ ".evo-hist-tone-tertiary{color:var(--dsw-alias-label-tertiary)}",
2782
+ ".evo-hist-main{flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:var(--evo-space-12) var(--evo-space-12)}",
2783
+ ".evo-hist-head{display:flex;align-items:center;gap:var(--evo-space-10);margin:0 0 var(--evo-space-4)}",
2784
+ ".evo-hist-title{flex:1 1 auto;margin:0;font-size:var(--evo-type-lg);font-weight:600}",
2785
+ ".evo-hist-hint{margin:0 0 var(--evo-space-10);color:var(--dsw-alias-label-secondary);line-height:var(--evo-leading-base)}",
2786
+ ".evo-hist-note{display:flex;align-items:flex-start;gap:var(--evo-space-6);margin:0 0 var(--evo-space-10);color:var(--dsw-alias-label-secondary);line-height:var(--evo-leading-base)}",
2787
+ ".evo-hist-note-text{min-width:0;white-space:pre-wrap}",
2788
+ ".evo-hist-note[data-error=\"true\"]{color:var(--evo-tone-danger);background:var(--dsw-alias-bg-layer-2);border:var(--evo-hairline-width) solid var(--evo-tone-danger);border-radius:var(--evo-radius-control);padding:var(--evo-space-8) var(--evo-space-10)}",
2789
+ ".evo-hist-icon{flex:none;margin-top:.15em}",
2790
+ ".evo-hist-group{margin:var(--evo-space-16) 0 var(--evo-space-4);font-size:var(--evo-type-sm);font-weight:600;color:var(--dsw-alias-label-secondary)}",
2791
+ ".evo-hist-row{display:flex;align-items:flex-start;gap:var(--evo-space-8);padding:var(--evo-space-8) 0;border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);max-width:calc(var(--evo-measure-read) * 1.5);color:var(--dsw-alias-label-primary)}",
2792
+ ".evo-hist-row:first-of-type{border-top:0}",
2793
+ ".evo-hist-row-body{flex:1 1 auto;min-width:0}",
2794
+ ".evo-hist-row-title{display:flex;align-items:baseline;gap:var(--evo-space-8);min-width:0}",
2795
+ ".evo-hist-row-summary{margin-top:var(--evo-space-2);line-height:var(--evo-leading-base)}",
2796
+ ".evo-hist-actions{display:flex;flex:none;align-items:center;gap:var(--evo-space-6)}",
2797
+ ".evo-hist-chip{flex:none;padding:var(--evo-space-2) var(--evo-space-8);border-radius:var(--evo-radius-pill);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}",
2798
+ ".evo-hist-button{display:inline-flex;flex:none;align-items:center;gap:var(--evo-space-2);font:inherit;padding:var(--evo-space-4) var(--evo-space-10);border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-radius:var(--evo-radius-control);background:transparent;color:var(--evo-tone-quiet);cursor:pointer}",
2799
+ ".evo-hist-button[data-kind=\"toggle\"],.evo-hist-button[data-kind=\"switch\"]{padding:var(--evo-space-2) var(--evo-space-4);border-color:transparent;color:var(--evo-tone-quiet)}",
2800
+ ".evo-hist-button[data-kind=\"switch\"][aria-pressed=\"true\"]{color:var(--dsw-alias-label-primary)}",
2801
+ ".evo-hist-button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
2802
+ ".evo-hist-button:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
2803
+ ".evo-hist-button[data-tone=\"primary\"]{border-color:var(--evo-tone-primary);color:var(--evo-tone-primary)}",
2804
+ ".evo-doc-preview{margin:var(--evo-space-6) 0 var(--evo-space-2);max-width:var(--evo-measure-read);line-height:var(--evo-leading-base)}",
2805
+ ".evo-doc-diff{display:flex;flex-direction:column;gap:var(--evo-space-8)}",
2806
+ ".evo-doc-head{display:flex;align-items:center;gap:var(--evo-space-8);max-width:var(--evo-measure-read)}",
2807
+ ".evo-doc-head .evo-hist-note{flex:1 1 auto;margin:0}",
2808
+ ".evo-doc-toggle{display:flex;flex:none;align-items:center;gap:var(--evo-space-6)}",
2809
+ ".evo-doc-fact+.evo-doc-fact::before{content:\"·\";margin:0 var(--evo-space-6);color:var(--dsw-alias-label-tertiary)}",
2810
+ ".evo-doc-source{margin:var(--evo-space-6) 0 var(--evo-space-2);padding:var(--evo-space-12) var(--evo-space-16);max-height:var(--evo-cap-block);overflow:auto;border:var(--evo-hairline-width) solid var(--dsw-alias-border-l1);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
2811
+ ".evo-doc-render{display:flex;flex-direction:column;gap:var(--evo-space-8);margin:var(--evo-space-6) 0 var(--evo-space-2);max-width:var(--evo-measure-read)}",
2812
+ ".evo-doc-block{position:relative;padding:var(--evo-space-8) var(--evo-space-12) var(--evo-space-8) var(--evo-space-20);border:var(--evo-hairline-width) solid var(--dsw-alias-border-l2);border-left-width:var(--evo-hairline-marker);border-radius:var(--evo-radius-surface);background:var(--dsw-alias-markdown-code-block);overflow-x:auto}",
2813
+ ".evo-doc-del{border-left-color:var(--evo-tone-danger)}",
2814
+ ".evo-doc-add{border-left-color:var(--evo-tone-success)}",
2815
+ ".evo-doc-tag{position:absolute;left:var(--evo-space-8);top:var(--evo-space-8);color:var(--dsw-alias-label-tertiary)}",
2816
+ ".evo-doc-line-add{color:var(--evo-tone-success)}",
2817
+ ".evo-doc-line-del{color:var(--evo-tone-danger)}"
2818
+ ].join("\n");
2819
+ //#endregion
2820
+ //#region src/panels/history/client/atoms.ts
2821
+ /**
2822
+ * The four atoms the panel's tool face is built from (G3, §15.2 L2a atoms).
2823
+ *
2824
+ * Why atoms at all: the row used to be assembled from five different elements that each wrote their
2825
+ * own classes, so "what a control looks like" was spread over the stylesheet and a click target, a
2826
+ * state chip, a refusal and a glyph had no vocabulary at all (W7/W9/W13/W14). Every interactive
2827
+ * element now says what it IS — a control that acts, a control that only reveals more, a state, a
2828
+ * notice — and the stylesheet answers once per kind.
2829
+ *
2830
+ * No words live here: an atom takes the copy its caller already resolved.
2831
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2832
+ */
2833
+ /** The stroked path of one glyph, in a 16x16 box. */
2834
+ const PATHS = {
2835
+ "chevron-right": ["M6.5 3.5L11 8l-4.5 4.5"],
2836
+ "chevron-down": ["M3.5 6.5L8 11l4.5-4.5"],
2837
+ alert: [
2838
+ "M8 2.5l5.5 10.5h-11z",
2839
+ "M8 6.6v3.1",
2840
+ "M8 11.4h.01"
2841
+ ]
2842
+ };
2843
+ /**
2844
+ * One drawn glyph.
2845
+ * @param name - which glyph.
2846
+ * @returns an inline SVG that takes the surrounding colour and type size.
2847
+ */
2848
+ function icon(name) {
2849
+ return (0, react.createElement)("svg", {
2850
+ className: "evo-hist-icon",
2851
+ viewBox: "0 0 16 16",
2852
+ width: "1em",
2853
+ height: "1em",
2854
+ fill: "none",
2855
+ stroke: "currentColor",
2856
+ strokeWidth: 1.5,
2857
+ strokeLinecap: "round",
2858
+ strokeLinejoin: "round",
2859
+ "aria-hidden": "true",
2860
+ focusable: "false"
2861
+ }, PATHS[name].map((d, index) => (0, react.createElement)("path", {
2862
+ key: index,
2863
+ d
2864
+ })));
2865
+ }
2866
+ /**
2867
+ * One control.
2868
+ * @param props - what it is, how it speaks, and what it does.
2869
+ * @returns the button element.
2870
+ */
2871
+ function button(props) {
2872
+ const { children, kind = "action", tone = "quiet", title, expanded, pressed, onClick } = props;
2873
+ return (0, react.createElement)("button", {
2874
+ key: props.key,
2875
+ type: "button",
2876
+ className: "evo-hist-button",
2877
+ "data-kind": kind,
2878
+ "data-tone": tone,
2879
+ title,
2880
+ "aria-expanded": expanded,
2881
+ "aria-pressed": pressed,
2882
+ onClick
2883
+ }, kind === "toggle" ? icon(expanded === true ? "chevron-down" : "chevron-right") : null, children);
2884
+ }
2885
+ /**
2886
+ * One state, spelled as a chip: never clickable, never in the primary colour (W9).
2887
+ * @param props - the copy.
2888
+ * @returns the chip element.
2889
+ */
2890
+ function chip(props) {
2891
+ return (0, react.createElement)("span", {
2892
+ key: props.key,
2893
+ className: "evo-hist-chip"
2894
+ }, props.children);
2895
+ }
2896
+ /**
2897
+ * One notice.
2898
+ * @param props - the copy, and whether it reports a failure.
2899
+ * @returns the note element.
2900
+ */
2901
+ function note(props) {
2902
+ return (0, react.createElement)("p", {
2903
+ key: props.key,
2904
+ className: "evo-hist-note",
2905
+ "data-error": props.error === true ? "true" : void 0
2906
+ }, props.error === true ? icon("alert") : null, (0, react.createElement)("span", { className: "evo-hist-note-text" }, props.children));
2907
+ }
2908
+ //#endregion
2909
+ //#region src/panels/history/client/chrome.ts
2910
+ /**
2911
+ * The TOOL face: the skill list, the version list, and the controls that act on a version.
2912
+ *
2913
+ * This file renders FACTS. What a row says, in which order and in which slot is decided in core
2914
+ * (`skill-row-cells.ts`) and travels with the row, because a browser half cannot import core at
2915
+ * runtime; the words come from the locale dictionary through the `Copy` seat. The renderer's only
2916
+ * judgements are layout: a cell's slot decides its alignment, and the `aside` slot is the one the
2917
+ * browser's ellipsis may never eat (W1).
2918
+ *
2919
+ * The reading face — a version rendered as a document, and the rendered diff — lives in
2920
+ * `document.ts`; the two faces share the scale in `tokens.ts` and nothing else (§15.2 L2b).
2921
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2922
+ */
2923
+ /** The local time an ISO timestamp names, as the operator's own clock shows it. */
2924
+ function localTime(at) {
2925
+ const parsed = Date.parse(at);
2926
+ if (Number.isNaN(parsed)) return at;
2927
+ const date = new Date(parsed);
2928
+ const pad = (value) => String(value).padStart(2, "0");
2929
+ return String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
2930
+ }
2931
+ /** The text one cell shows, resolved through the seat. */
2932
+ function cellText(copy, cell) {
2933
+ if (!("copy" in cell)) return cell.literal;
2934
+ return cell.values === void 0 ? copy.t(cell.copy) : copy.format(cell.copy, cell.values);
2935
+ }
2936
+ /**
2937
+ * One line of cells. The slot decides placement; the tone only changes the register.
2938
+ * @param cells - the cells of this line, in reading order.
2939
+ * @param copy - the dictionary seat.
2940
+ * @param className - the line's own class.
2941
+ * @param hover - hover text the host cannot express (the local clock), by cell key.
2942
+ * @returns the line element.
2943
+ */
2944
+ function cellLine(cells, copy, className, hover = {}) {
2945
+ return (0, react.createElement)("div", { className }, cells.map((cell) => (0, react.createElement)("span", {
2946
+ key: cell.key,
2947
+ className: "evo-hist-cell evo-hist-slot-" + cell.slot + " evo-hist-tone-" + cell.tone,
2948
+ title: hover[cell.key] ?? cell.note
2949
+ }, cellText(copy, cell))));
2950
+ }
2951
+ /** The search field above the list. */
2952
+ function searchField(copy, query, onQuery) {
2953
+ return (0, react.createElement)("div", { className: "evo-hist-search" }, (0, react.createElement)("input", {
2954
+ type: "search",
2955
+ value: query,
2956
+ placeholder: copy.t("search"),
2957
+ "aria-label": copy.t("search"),
2958
+ onChange: (event) => {
2959
+ onQuery(event.target.value);
2960
+ }
2961
+ }));
2962
+ }
2963
+ /**
2964
+ * One skill in the left column: its name and size, then what it is and when it last changed.
2965
+ * @param copy - the dictionary seat.
2966
+ * @param row - the skill's facts and cells.
2967
+ * @param selected - whether this is the open skill.
2968
+ * @param onOpen - what a click does.
2969
+ * @returns the row.
2970
+ */
2971
+ function skillLine(copy, row, selected, onOpen) {
2972
+ return (0, react.createElement)("button", {
2973
+ key: row.name,
2974
+ type: "button",
2975
+ className: "evo-hist-skill",
2976
+ "aria-current": selected ? "true" : void 0,
2977
+ onClick: onOpen
2978
+ }, cellLine(row.cells.title, copy, "evo-hist-cell-line"), cellLine(row.cells.meta, copy, "evo-hist-cell-line evo-hist-cell-meta"));
2979
+ }
2980
+ /**
2981
+ * One version row: what happened, when, how big — then the controls, then whatever is expanded.
2982
+ *
2983
+ * The write is the only control that can change anything, and it is the only one that ever takes the
2984
+ * primary tone; a toggle that merely reveals more is borderless and carries a drawn chevron, so the
2985
+ * two kinds cannot be mistaken for each other (W7/W14).
2986
+ * @param props - the row, the copy, and the panel's callbacks.
2987
+ * @returns the row.
2988
+ */
2989
+ function versionRow(props) {
2990
+ const { copy, row, confirming, open, expanded, onRestore, onCancel, onToggle } = props;
2991
+ const actions = [];
2992
+ if (row.undoable === false) actions.push(chip({
2993
+ key: "current",
2994
+ children: copy.t("current")
2995
+ }));
2996
+ if (row.undoable === true) {
2997
+ if (confirming) actions.push(button({
2998
+ key: "cancel",
2999
+ onClick: onCancel,
3000
+ children: copy.t("undo.no")
3001
+ }));
3002
+ actions.push(button({
3003
+ key: "undo",
3004
+ tone: confirming ? "primary" : "quiet",
3005
+ onClick: onRestore,
3006
+ children: confirming ? copy.t("undo.confirm") : copy.t("undo")
3007
+ }));
3008
+ }
3009
+ if (row.undoable === true || row.charsDelta !== void 0) actions.push(button({
3010
+ key: "diff",
3011
+ kind: "toggle",
3012
+ expanded: open === "diff",
3013
+ onClick: () => {
3014
+ onToggle("diff");
3015
+ },
3016
+ children: open === "diff" ? copy.t("diff.hide") : copy.t("diff.show")
3017
+ }));
3018
+ actions.push(button({
3019
+ key: "preview",
3020
+ kind: "toggle",
3021
+ expanded: open === "preview",
3022
+ onClick: () => {
3023
+ onToggle("preview");
3024
+ },
3025
+ children: open === "preview" ? copy.t("preview.hide") : copy.t("preview.show")
3026
+ }));
3027
+ return (0, react.createElement)("div", {
3028
+ key: String(row.v),
3029
+ className: "evo-hist-row"
3030
+ }, (0, react.createElement)("div", { className: "evo-hist-row-body" }, cellLine(row.cells.title, copy, "evo-hist-row-title", { time: localTime(row.at) }), row.cells.meta.length === 0 ? null : cellLine(row.cells.meta, copy, "evo-hist-row-summary"), expanded), (0, react.createElement)("div", { className: "evo-hist-actions" }, actions));
3031
+ }
3032
+ /**
3033
+ * One group's heading, and the note that explains the group.
3034
+ * @param title - the group's name, already resolved.
3035
+ * @param noteText - the group's note, when it has one.
3036
+ * @returns the heading and the note, for the caller to place in its own section.
3037
+ */
3038
+ function groupHeading(title, noteText) {
3039
+ return [(0, react.createElement)("h3", {
3040
+ key: "title",
3041
+ className: "evo-hist-group"
3042
+ }, title), noteText === void 0 ? null : (0, react.createElement)("p", {
3043
+ key: "note",
3044
+ className: "evo-hist-hint"
3045
+ }, noteText)];
3046
+ }
3047
+ /** What the list shows when it has nothing to show.
3048
+ * S5-07: the sites that render it as a SIBLING in an array must pass a key - a keyless sibling made
3049
+ * React warn on every render of the default view (measured: one warning per render, captured by spy).
3050
+ */
3051
+ function emptyState(text, key) {
3052
+ return (0, react.createElement)("p", {
3053
+ key,
3054
+ className: "evo-hist-empty"
3055
+ }, text);
3056
+ }
3057
+ /** One notice about what just happened. */
3058
+ function notice(text, error) {
3059
+ if (text === void 0) return null;
3060
+ return note({
3061
+ error,
3062
+ children: text
3063
+ });
3064
+ }
3065
+ /** The pane's heading and its one control. */
3066
+ function panelHead(copy, onRefresh) {
3067
+ return (0, react.createElement)("div", { className: "evo-hist-head" }, (0, react.createElement)("h2", { className: "evo-hist-title" }, copy.t("title")), button({
3068
+ key: "refresh",
3069
+ title: copy.t("refresh"),
3070
+ onClick: onRefresh,
3071
+ children: copy.t("refresh")
3072
+ }));
3073
+ }
3074
+ //#endregion
3075
+ //#region src/panels/history/client/markdown.ts
3076
+ /**
3077
+ * The one seam onto the platform's Markdown renderer.
3078
+ *
3079
+ * Everything that renders Markdown in this panel goes through here, so the implementation can be
3080
+ * swapped in one file: today it is the platform's own `MarkdownText` (reached through the client
3081
+ * module table — zero bytes shipped, styles already in the shell, the same mdast pipeline the chat
3082
+ * uses); if that ever stops being reachable, a self-drawn renderer replaces this module and nothing
3083
+ * else changes.
3084
+ *
3085
+ * The words are NOT this module's: the face passes its locale dictionary in, because the platform
3086
+ * component takes its chrome as props.
3087
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
3088
+ */
3089
+ /** The module-table specifier; kept in one place so the seam is the only reader. */
3090
+ const PLATFORM_UI_MODULE = "@deepseek-ai/dsh-client-ui-primitives";
3091
+ /**
3092
+ * The renderer a module row carries, when it carries one.
3093
+ *
3094
+ * A React component type is NOT always a function: the platform's own `MarkdownText` is
3095
+ * `memo(function MarkdownText …)`, and `memo` returns an OBJECT. A `typeof === 'function'` test
3096
+ * therefore rejected the real renderer and silently degraded every view to its source — exactly
3097
+ * what 0.14.0 shipped (measured on the installed panel 2026-09-28: `[预览]` and the rendered diff
3098
+ * both fell back). The accepted domain is React's element-type domain: a function, or a non-null
3099
+ * object it can mount. `tests/markdown.client.spec.ts` pins all four cases.
3100
+ * @param row - what the client module table handed over.
3101
+ * @returns the component, or undefined when the row carries none.
3102
+ */
3103
+ function rendererOf(row) {
3104
+ const candidate = row.MarkdownText;
3105
+ if (typeof candidate === "function") return candidate;
3106
+ if (typeof candidate === "object" && candidate !== null && candidate.$$typeof !== void 0) return candidate;
3107
+ }
3108
+ /**
3109
+ * The platform component, when this deployment's module table carries one.
3110
+ *
3111
+ * A row that IS reachable without a renderer is not a render: reporting one would move the failure
3112
+ * into the render itself, where only the slot's error boundary would catch it.
3113
+ * @returns the component, or undefined when it cannot be reached.
3114
+ */
3115
+ function markdownText() {
3116
+ try {
3117
+ return rendererOf(require(PLATFORM_UI_MODULE));
3118
+ } catch {
3119
+ return;
3120
+ }
3121
+ }
3122
+ /**
3123
+ * One document, rendered by the platform component under a labels object that survives re-renders.
3124
+ *
3125
+ * The component is memoized and the platform's own call sites memoize their labels for that reason:
3126
+ * this panel re-renders on every keystroke in its search field, and a fresh labels object per render
3127
+ * would take the whole document through the render again.
3128
+ * @param props - the resolved component, the Markdown source, and the localized words.
3129
+ * @returns the rendered document.
3130
+ */
3131
+ function MarkdownBlock(props) {
3132
+ const { component: MarkdownText, text, words } = props;
3133
+ const { copy, copied, footnotes } = words;
3134
+ return (0, react.createElement)(MarkdownText, {
3135
+ text,
3136
+ labels: (0, react.useMemo)(() => ({
3137
+ code: {
3138
+ copyLabel: copy,
3139
+ copiedLabel: copied
3140
+ },
3141
+ footnotes
3142
+ }), [
3143
+ copy,
3144
+ copied,
3145
+ footnotes
3146
+ ])
3147
+ });
3148
+ }
3149
+ /**
3150
+ * Render one Markdown document to an element tree.
3151
+ * @param text - the Markdown source (already bounded by the caller).
3152
+ * @param words - the localized Markdown chrome.
3153
+ * @returns the element, or `{ ok: false }` when the platform renderer is not reachable (the caller
3154
+ * then keeps its own source view: a missing renderer degrades the view, never the panel).
3155
+ */
3156
+ function renderMarkdown(text, words) {
3157
+ const component = markdownText();
3158
+ if (component === void 0) return { ok: false };
3159
+ return {
3160
+ ok: true,
3161
+ node: (0, react.createElement)(MarkdownBlock, {
3162
+ component,
3163
+ text,
3164
+ words
3165
+ })
3166
+ };
3167
+ }
3168
+ //#endregion
3169
+ //#region src/panels/history/client/document.ts
3170
+ /**
3171
+ * The READING face: one version rendered as a document, and the rendered diff.
3172
+ *
3173
+ * This file is the only place that decides how a document reads (G4). The tool face (`chrome.ts`)
3174
+ * picks versions and acts on them; here the rules are the reader's, and they follow the positioning
3175
+ * ruling of §14.0: a document is read, not scanned — one measure, page-like spacing, no inner scroll
3176
+ * (W18/W20/W21/W22). Nothing here is a control except the source/rendered choice, which belongs to
3177
+ * the reader rather than to the panel's actions.
3178
+ *
3179
+ * The text arrives already prepared: the host strips the frontmatter block through core's
3180
+ * `displayBodyOf`, because a browser half cannot import core. The RENDERED DIFF keeps the whole text
3181
+ * on purpose — a diff is about what CHANGED, and a metadata change is a change.
3182
+ *
3183
+ * It degrades rather than blanking: when the platform's renderer cannot be reached, the exact text is
3184
+ * shown instead, and the reader can still read the version.
3185
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
3186
+ */
3187
+ /** One `<pre>` rendering of a diff: added lines marked `+`, removed ones `-`. */
3188
+ function sourceBlock(diff) {
3189
+ const rows = [];
3190
+ for (const [hunkIndex, hunk] of diff.hunks.entries()) {
3191
+ for (const [index, line] of hunk.oldText.split("\n").entries()) {
3192
+ if (hunk.oldText === "" && line === "") continue;
3193
+ rows.push((0, react.createElement)("div", {
3194
+ key: "old-" + String(hunkIndex) + "-" + String(index),
3195
+ className: "evo-doc-line-del"
3196
+ }, "- " + line));
3197
+ }
3198
+ for (const [index, line] of hunk.newText.split("\n").entries()) {
3199
+ if (hunk.newText === "" && line === "") continue;
3200
+ rows.push((0, react.createElement)("div", {
3201
+ key: "new-" + String(hunkIndex) + "-" + String(index),
3202
+ className: "evo-doc-line-add"
3203
+ }, "+ " + line));
3204
+ }
3205
+ }
3206
+ return (0, react.createElement)("pre", { className: "evo-doc-source" }, rows.length === 0 ? "±" : rows);
3207
+ }
3208
+ /** One rendered side of a diff: an \u201cold\u201d or \u201cnew\u201d block under a tinted edge. */
3209
+ function renderedSide(face, mark, side, text) {
3210
+ const rendered = renderMarkdown(text, face.markdownWords);
3211
+ if (!rendered.ok) return null;
3212
+ return (0, react.createElement)("div", { className: "evo-doc-block evo-doc-" + side }, (0, react.createElement)("span", { className: "evo-doc-tag" }, mark), rendered.node);
3213
+ }
3214
+ /**
3215
+ * The same diff, both sides rendered as Markdown and stacked: the removed text first, then the added
3216
+ * one. Block level on purpose — line-level interleaving of two rendered documents is a different (and
3217
+ * much larger) problem, and the source view stays one click away for exact bytes.
3218
+ * @param face - the dictionary seat and the renderer's words.
3219
+ * @param diff - the windowed change the host reported.
3220
+ * @returns the stacked blocks, or null when the renderer is out.
3221
+ */
3222
+ function renderedBlocks(face, diff) {
3223
+ const blocks = [];
3224
+ for (const [index, hunk] of diff.hunks.entries()) {
3225
+ if (hunk.oldText !== "") {
3226
+ const side = renderedSide(face, "−", "del", hunk.oldText);
3227
+ if (side === null) return null;
3228
+ blocks.push((0, react.createElement)("div", { key: "del-" + String(index) }, side));
3229
+ }
3230
+ if (hunk.newText !== "") {
3231
+ const side = renderedSide(face, "+", "add", hunk.newText);
3232
+ if (side === null) return null;
3233
+ blocks.push((0, react.createElement)("div", { key: "add-" + String(index) }, side));
3234
+ }
3235
+ }
3236
+ return blocks.length === 0 ? null : blocks;
3237
+ }
3238
+ /**
3239
+ * One diff in the mode the reader asked for, degrading to the source view when the renderer is out.
3240
+ * @param face - the dictionary seat and the renderer's words.
3241
+ * @param diff - the windowed change the host reported.
3242
+ * @param mode - source or rendered.
3243
+ * @returns the element to draw.
3244
+ */
3245
+ function diffView(face, diff, mode) {
3246
+ if (mode === "rendered") {
3247
+ const blocks = renderedBlocks(face, diff);
3248
+ if (blocks !== null) return (0, react.createElement)("div", { className: "evo-doc-render" }, blocks);
3249
+ }
3250
+ return sourceBlock(diff);
3251
+ }
3252
+ /**
3253
+ * One version read as a document: the platform renderer when it is there, plain text when it is not.
3254
+ *
3255
+ * The document is the page, not a card inside it: no border, no surface of its own and no inner
3256
+ * scroll, so the pane's own scrollbar is the only one a reader meets (W20/W21). A body the host
3257
+ * bounded says how much it is not showing, with both numbers (W23).
3258
+ * @param props - the face and the body state to draw.
3259
+ * @returns the element.
3260
+ */
3261
+ function PreviewBody(props) {
3262
+ const { face, body } = props;
3263
+ if (body.kind === "loading") return note({ children: face.t("diff.loading") });
3264
+ if (body.kind === "failed") return note({
3265
+ error: true,
3266
+ children: body.message
3267
+ });
3268
+ const rendered = renderMarkdown(body.body.display, face.markdownWords);
3269
+ return (0, react.createElement)("div", { className: "evo-doc-preview" }, body.body.truncated ? note({ children: face.format("preview.truncated", {
3270
+ shown: body.body.display.length,
3271
+ total: body.body.chars
3272
+ }) }) : null, rendered.ok ? rendered.node : (0, react.createElement)("pre", { className: "evo-doc-source" }, body.body.display));
3273
+ }
3274
+ /**
3275
+ * The expanded body of one row: the diff, a loading line, or the refusal.
3276
+ *
3277
+ * The source/rendered choice is LOCAL to this view: which row is open is the panel's business, but
3278
+ * how one open diff is displayed is not, so the state does not climb into the parent.
3279
+ * @param props - the face and the diff state to draw.
3280
+ * @returns the element.
3281
+ */
3282
+ function DiffBody(props) {
3283
+ const { face, diff } = props;
3284
+ const [mode, setMode] = (0, react.useState)("source");
3285
+ if (diff.kind === "loading") return note({ children: face.t("diff.loading") });
3286
+ if (diff.kind === "failed") return note({
3287
+ error: true,
3288
+ children: diff.message
3289
+ });
3290
+ const against = diff.diff.against === null ? face.t("diff.first") : face.format("diff.against", { n: diff.diff.against });
3291
+ const toggle = (0, react.createElement)("div", { className: "evo-doc-toggle" }, button({
3292
+ key: "source",
3293
+ kind: "switch",
3294
+ pressed: mode === "source",
3295
+ onClick: () => {
3296
+ setMode("source");
3297
+ },
3298
+ children: face.t("diff.source")
3299
+ }), button({
3300
+ key: "rendered",
3301
+ kind: "switch",
3302
+ pressed: mode === "rendered",
3303
+ onClick: () => {
3304
+ setMode("rendered");
3305
+ },
3306
+ children: face.t("diff.rendered")
3307
+ }));
3308
+ return (0, react.createElement)("div", { className: "evo-doc-diff" }, (0, react.createElement)("div", { className: "evo-doc-head" }, note({ children: [
3309
+ (0, react.createElement)("span", {
3310
+ key: "against",
3311
+ className: "evo-doc-fact"
3312
+ }, against),
3313
+ (0, react.createElement)("span", {
3314
+ key: "added",
3315
+ className: "evo-doc-fact"
3316
+ }, face.format("diff.added", { n: diff.diff.linesAdded })),
3317
+ (0, react.createElement)("span", {
3318
+ key: "removed",
3319
+ className: "evo-doc-fact"
3320
+ }, face.format("diff.removed", { n: diff.diff.linesRemoved })),
3321
+ diff.diff.truncated ? (0, react.createElement)("span", {
3322
+ key: "truncated",
3323
+ className: "evo-doc-fact"
3324
+ }, face.t("diff.truncated")) : null
3325
+ ] }), toggle), diffView(face, diff.diff, mode));
3326
+ }
3327
+ //#endregion
3328
+ //#region src/panels/history/client/Panel.ts
3329
+ /**
3330
+ * The panel: one read, one open skill, one open row — and the faces that draw them.
3331
+ *
3332
+ * Everything a PERSON reads is decided elsewhere: what a row says is core's (`skill-row-cells.ts`,
3333
+ * shipped with the row), the words are the dictionary's, the tool face is `chrome.ts` and the reading
3334
+ * face is `document.ts`. What is left here is the STATE — which skill is open, which row is expanded,
3335
+ * which reads are in flight — and the two rules that only a state machine can hold: a read takes a
3336
+ * ticket so a slow reply cannot land on another skill, and a write reports its own result instead of
3337
+ * being erased by the read that follows it.
3338
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
3339
+ */
3340
+ /**
3341
+ * The panel body.
3342
+ * @param face - copy plus the data callbacks.
3343
+ * @returns the panel element.
3344
+ */
3345
+ /** Fill the generated block's `{slot}` copies with this bundle's dictionary. */
3346
+ function fillCopy(template, values) {
3347
+ return template.replace(/\{(\w+)\}/g, (match, slot) => {
3348
+ const value = values[slot];
3349
+ return value === void 0 ? match : String(value);
3350
+ });
3351
+ }
3352
+ /** The shared four-state failure block, spoken through this bundle's keys. */
3353
+ function failureBlock(t, failure) {
3354
+ const translate = (key, values) => {
3355
+ const text = t(key);
3356
+ return values === void 0 ? text : fillCopy(text, values);
3357
+ };
3358
+ return FailureBlock({
3359
+ failure,
3360
+ t: translate,
3361
+ fallback: FALLBACKS["skill-history"]
3362
+ });
3363
+ }
3364
+ function SkillHistoryPanel(face) {
3365
+ const [skills, setSkills] = (0, react.useState)([]);
3366
+ const [skillsLoaded, setSkillsLoaded] = (0, react.useState)(false);
3367
+ const [failure, setFailure] = (0, react.useState)(void 0);
3368
+ const [selected, setSelected] = (0, react.useState)(void 0);
3369
+ const [payload, setPayload] = (0, react.useState)(void 0);
3370
+ const [versionsFailed, setVersionsFailed] = (0, react.useState)(void 0);
3371
+ const [noteText, setNoteText] = (0, react.useState)(void 0);
3372
+ const [noteError, setNoteError] = (0, react.useState)(false);
3373
+ const [pending, setPending] = (0, react.useState)(void 0);
3374
+ const [query, setQuery] = (0, react.useState)("");
3375
+ const [expanded, setExpanded] = (0, react.useState)(void 0);
3376
+ const [diffs, setDiffs] = (0, react.useState)(/* @__PURE__ */ new Map());
3377
+ const [bodies, setBodies] = (0, react.useState)(/* @__PURE__ */ new Map());
3378
+ const readTicket = (0, react.useRef)(0);
3379
+ const failed = (error) => {
3380
+ setNoteError(true);
3381
+ setNoteText(face.t("error.hint") + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
3382
+ setFailure(error instanceof SkillHistoryRefusal ? error.failure : {
3383
+ kind: "server",
3384
+ status: 0
3385
+ });
3386
+ };
3387
+ const succeeded = (message) => {
3388
+ setNoteError(false);
3389
+ setNoteText(message);
3390
+ };
3391
+ /** The sentence one failed read carries, in ONE place: every reader below reports the same way. */
3392
+ const failedMessage = (error) => face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
3393
+ /** Read the skills again, keeping the sentence: a re-read is not an answer to anything. */
3394
+ const reloadSkills = () => {
3395
+ face.loadSkills().then(setSkills).catch(failed).finally(() => {
3396
+ setSkillsLoaded(true);
3397
+ });
3398
+ };
3399
+ /**
3400
+ * Show one skill's chains, keeping the sentence. A re-read that FAILS reports through the caller's
3401
+ * own handler, so the failure of a read can never erase the write it was reading after.
3402
+ * @param name - the skill to open.
3403
+ * @param onFailure - what to do with a failed read.
3404
+ */
3405
+ const showVersions = (name, onFailure = failed) => {
3406
+ const ticket = readTicket.current + 1;
3407
+ readTicket.current = ticket;
3408
+ setSelected(name);
3409
+ setPayload(void 0);
3410
+ setVersionsFailed(void 0);
3411
+ setPending(void 0);
3412
+ setExpanded(void 0);
3413
+ setDiffs(/* @__PURE__ */ new Map());
3414
+ setBodies(/* @__PURE__ */ new Map());
3415
+ face.loadVersions(name).then((next) => {
3416
+ if (readTicket.current === ticket) setPayload(next);
3417
+ }).catch((error) => {
3418
+ if (readTicket.current !== ticket) return;
3419
+ setVersionsFailed(failedMessage(error));
3420
+ onFailure(error);
3421
+ });
3422
+ };
3423
+ /** Open a skill from the list: another skill's sentence described another skill, so it goes away. */
3424
+ const open = (name) => {
3425
+ if (name !== selected) {
3426
+ setNoteText(void 0);
3427
+ setNoteError(false);
3428
+ }
3429
+ showVersions(name);
3430
+ };
3431
+ /** What the refresh control does: both reads again, and the sentence stays. */
3432
+ const refresh = () => {
3433
+ reloadSkills();
3434
+ if (selected !== void 0) showVersions(selected);
3435
+ };
3436
+ const restore = (v, unitId) => {
3437
+ if (selected === void 0) return;
3438
+ if (pending !== v) {
3439
+ setPending(v);
3440
+ return;
3441
+ }
3442
+ setPending(void 0);
3443
+ const name = selected;
3444
+ face.undo(name, unitId === void 0 ? v : void 0, unitId).then((message) => {
3445
+ showVersions(name, (error) => {
3446
+ setNoteError(true);
3447
+ setNoteText(message + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
3448
+ });
3449
+ succeeded(message);
3450
+ }).catch(failed);
3451
+ };
3452
+ /**
3453
+ * Expand one row, or collapse it: one row is open at a time, and each kind keeps its own lazy read.
3454
+ * @param v - the version whose row was clicked.
3455
+ * @param kind - which expansion the row should show.
3456
+ */
3457
+ const toggle = (v, kind) => {
3458
+ if (selected === void 0) return;
3459
+ if (expanded !== void 0 && expanded.v === v && expanded.kind === kind) {
3460
+ setExpanded(void 0);
3461
+ return;
3462
+ }
3463
+ setExpanded({
3464
+ v,
3465
+ kind
3466
+ });
3467
+ if (kind === "diff" ? diffs.has(v) : bodies.has(v)) return;
3468
+ const name = selected;
3469
+ const ticket = readTicket.current + 1;
3470
+ readTicket.current = ticket;
3471
+ if (kind === "diff") {
3472
+ setDiffs((current) => new Map(current).set(v, { kind: "loading" }));
3473
+ face.loadDiff(name, v).then((diff) => {
3474
+ if (readTicket.current !== ticket) {
3475
+ setDiffs((current) => without(current, v));
3476
+ return;
3477
+ }
3478
+ setDiffs((current) => new Map(current).set(v, {
3479
+ kind: "ready",
3480
+ diff
3481
+ }));
3482
+ }).catch((error) => {
3483
+ if (readTicket.current !== ticket) {
3484
+ setDiffs((current) => without(current, v));
3485
+ return;
3486
+ }
3487
+ setDiffs((current) => new Map(current).set(v, {
3488
+ kind: "failed",
3489
+ message: failedMessage(error)
3490
+ }));
3491
+ });
3492
+ return;
3493
+ }
3494
+ setBodies((current) => new Map(current).set(v, { kind: "loading" }));
3495
+ face.loadBody(name, v).then((body) => {
3496
+ if (readTicket.current !== ticket) {
3497
+ setBodies((current) => without(current, v));
3498
+ return;
3499
+ }
3500
+ setBodies((current) => new Map(current).set(v, {
3501
+ kind: "ready",
3502
+ body
3503
+ }));
3504
+ }).catch((error) => {
3505
+ if (readTicket.current !== ticket) {
3506
+ setBodies((current) => without(current, v));
3507
+ return;
3508
+ }
3509
+ setBodies((current) => new Map(current).set(v, {
3510
+ kind: "failed",
3511
+ message: failedMessage(error)
3512
+ }));
3513
+ });
3514
+ };
3515
+ (0, react.useEffect)(reloadSkills, []);
3516
+ const needle = query.trim().toLowerCase();
3517
+ const visible = (0, react.useMemo)(() => needle === "" ? skills : skills.filter((skill) => skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle)), [skills, needle]);
3518
+ const loaded = payload !== void 0;
3519
+ /** Drop the pending confirmation: the control that opened it must not be the only way out. */
3520
+ const cancelRestore = () => {
3521
+ setPending(void 0);
3522
+ };
3523
+ /** What one row shows when it is the open one. */
3524
+ const expandedOf = (row) => {
3525
+ if (expanded === void 0 || expanded.v !== row.v) return null;
3526
+ if (expanded.kind === "diff") {
3527
+ const state = diffs.get(row.v);
3528
+ return state === void 0 ? null : (0, react.createElement)(DiffBody, {
3529
+ face,
3530
+ diff: state
3531
+ });
3532
+ }
3533
+ const state = bodies.get(row.v);
3534
+ return state === void 0 ? null : (0, react.createElement)(PreviewBody, {
3535
+ face,
3536
+ body: state
3537
+ });
3538
+ };
3539
+ /** One group: a heading, an optional note, and its rows. */
3540
+ const group = (title, entries, noteText2) => {
3541
+ if (entries.length === 0) return null;
3542
+ return (0, react.createElement)("section", { key: title }, ...groupHeading(title, noteText2), ...entries.map((row) => versionRow({
3543
+ copy: face,
3544
+ row,
3545
+ confirming: pending === row.v,
3546
+ open: expanded !== void 0 && expanded.v === row.v ? expanded.kind : void 0,
3547
+ expanded: expandedOf(row),
3548
+ onRestore: () => {
3549
+ restore(row.v, row.unitId);
3550
+ },
3551
+ onCancel: cancelRestore,
3552
+ onToggle: (kind) => {
3553
+ toggle(row.v, kind);
3554
+ }
3555
+ })));
3556
+ };
3557
+ return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, searchField(face, query, setQuery), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? emptyState(face.t(skillsLoaded ? "empty.skills" : "loading")) : visible.length === 0 ? emptyState(face.t("search.none")) : visible.map((skill) => skillLine(face, skill, skill.name === selected, () => {
3558
+ open(skill.name);
3559
+ })))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh), notice(noteText, noteError), failure === void 0 ? null : (0, react.createElement)("div", { className: "evo-hist-failure" }, failureBlock(face.t, failure)), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? skillsLoaded ? "empty.skills" : "loading" : "empty.pick"), "empty"), (0, react.createElement)("p", {
3560
+ key: "hint",
3561
+ className: "evo-hist-hint"
3562
+ }, face.t("hint"))] : loaded ? null : emptyState(versionsFailed === void 0 ? face.t("loading") : `${face.t("empty.versions.failed")} — ${versionsFailed}`), loaded ? (0, react.createElement)("div", null, group(face.t("group.content"), payload.content, void 0), group(face.t("group.support"), payload.support, face.t("group.support.note")), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
3563
+ }
3564
+ /**
3565
+ * The lazy-read cache without one key.
3566
+ *
3567
+ * A read that a later toggle superseded must leave nothing behind: `loading` is not an answer, and
3568
+ * the cache also serves as the \u201cdo not read twice\u201d guard, so an abandoned entry would refuse every
3569
+ * later read of that row and leave it saying \u201cloading\u201d until the page is reloaded.
3570
+ * @param current - the cache.
3571
+ * @param key - the version whose entry is dropped.
3572
+ * @returns a new cache without that key.
3573
+ */
3574
+ function without(current, key) {
3575
+ const next = new Map(current);
3576
+ next.delete(key);
3577
+ return next;
3578
+ }
3579
+ //#endregion
3580
+ //#region src/panels/history/client/ids.ts
3581
+ const PANEL_ID = "skill-history";
3582
+ function PanelIcon(props = {}) {
3583
+ const size = typeof props.size === "number" && props.size > 0 ? props.size : 18;
3584
+ return (0, react.createElement)("svg", {
3585
+ "aria-hidden": "true",
3586
+ width: String(size),
3587
+ height: String(size),
3588
+ viewBox: "0 0 16 16",
3589
+ fill: "none",
3590
+ stroke: "currentColor",
3591
+ strokeWidth: "1.5",
3592
+ strokeLinecap: "round",
3593
+ strokeLinejoin: "round"
3594
+ }, (0, react.createElement)("circle", {
3595
+ cx: "8",
3596
+ cy: "8",
3597
+ r: "5.75"
3598
+ }), (0, react.createElement)("path", { d: "M8 4.75V8l2.25 1.5" }));
3599
+ }
3600
+ /**
3601
+ * Register the locale namespace, the panel row and the panel body.
3602
+ * @param ctx - the client cordis context.
3603
+ */
3604
+ function apply$1(ctx) {
3605
+ const api = createSkillHistoryApi();
3606
+ registerSidebarPanel(ctx, {
3607
+ id: PANEL_ID,
3608
+ order: 35,
3609
+ namespace: NS,
3610
+ dictionaries: {
3611
+ zh,
3612
+ en
3613
+ },
3614
+ fallback: "en",
3615
+ styles: {
3616
+ css: CSS,
3617
+ tagId: CSS_TAG_ID
3618
+ },
3619
+ icon: PanelIcon,
3620
+ component: SkillHistoryPanel,
3621
+ body: (t) => ({
3622
+ t,
3623
+ format: (key, values) => fill(t(key), values),
3624
+ loadSkills: api.skills,
3625
+ loadVersions: api.versions,
3626
+ loadDiff: api.diff,
3627
+ loadBody: api.body,
3628
+ undo: api.undo,
3629
+ markdownWords: {
3630
+ copy: t("markdown.copy"),
3631
+ copied: t("markdown.copied"),
3632
+ footnotes: t("markdown.footnotes")
3633
+ }
3634
+ })
3635
+ });
3636
+ }
3637
+ //#endregion
3638
+ //#region src/client/index.ts
3639
+ const inject = [
3640
+ "slots",
3641
+ "locale",
3642
+ "layout"
3643
+ ];
3644
+ /** Register every panel through the shared client-kit lifecycle. */
3645
+ function apply(ctx) {
3646
+ apply$2(ctx);
3647
+ (0, _deepseek_ai_dsh_evolution_merge_ui_client.apply)(ctx);
3648
+ apply$1(ctx);
3649
+ }
3650
+ //#endregion
3651
+ exports.apply = apply;
3652
+ exports.inject = inject;
3653
+
3654
+ return module.exports;
3655
+ }
3656
+ });