@lmzhen/dsh-evolution-ui 0.32.4 → 0.32.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -5,163 +5,6 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region ../evolution-client-kit/src/generated/routes.generated.ts
9
- /**
10
- * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
11
- * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
12
- * external), so the table is mirrored here mechanically; a rename in core regenerates all three
13
- * client copies, and the generator's --check makes a stale copy red.
14
- */
15
- const ROUTES = {
16
- approval: {
17
- pending: "/api/dsh-evolution/approval/pending",
18
- preview: "/api/dsh-evolution/approval/preview",
19
- approve: "/api/dsh-evolution/approval/approve",
20
- reject: "/api/dsh-evolution/approval/reject"
21
- },
22
- "skill-history": {
23
- skills: "/api/dsh-evolution/skill-history/skills",
24
- versions: "/api/dsh-evolution/skill-history/versions",
25
- diff: "/api/dsh-evolution/skill-history/versions/diff",
26
- body: "/api/dsh-evolution/skill-history/versions/body",
27
- undo: "/api/dsh-evolution/skill-history/undo",
28
- health: "/api/dsh-evolution/skill-history/health"
29
- },
30
- "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
31
- merge: {
32
- view: "/api/dsh-evolution/merge/view",
33
- rows: "/api/dsh-evolution/merge/rows",
34
- run: "/api/dsh-evolution/merge/run",
35
- runs: "/api/dsh-evolution/merge/runs",
36
- body: "/api/dsh-evolution/merge/body",
37
- history: "/api/dsh-evolution/merge/history"
38
- },
39
- diagnostics: { view: "/api/dsh-evolution/diagnostics" }
40
- };
41
- //#endregion
42
- //#region ../evolution-client-kit/src/generated/route-client.generated.ts
43
- /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
44
- function classify(status) {
45
- if (status === 401) return {
46
- kind: "unauthenticated",
47
- status: 401
48
- };
49
- if (status === 403) return {
50
- kind: "forbidden",
51
- status: 403
52
- };
53
- if (status === 404) return {
54
- kind: "unmounted",
55
- status: 404
56
- };
57
- return {
58
- kind: "server",
59
- status
60
- };
61
- }
62
- /**
63
- * One request, no retries and no body parsing for the platform's admission answers.
64
- * @param doFetch - the fetch in use (the browser's, or a spec's stub).
65
- * @param path - the exact route path from the generated ROUTES table.
66
- * @param init - standard RequestInit.
67
- * @returns the data, or the classified failure (network/parse failures read as 'server').
68
- */
69
- async function requestJson(doFetch, path, init) {
70
- let response;
71
- try {
72
- response = await doFetch(path, init);
73
- } catch (error) {
74
- return {
75
- ok: false,
76
- failure: {
77
- kind: "server",
78
- status: 0,
79
- detail: error instanceof Error ? error.message : String(error)
80
- }
81
- };
82
- }
83
- if (!response.ok) return {
84
- ok: false,
85
- failure: classify(response.status)
86
- };
87
- let body;
88
- try {
89
- body = await response.json();
90
- } catch {
91
- return {
92
- ok: false,
93
- failure: {
94
- kind: "server",
95
- status: response.status
96
- }
97
- };
98
- }
99
- if (typeof body === "object" && body !== null) {
100
- const record = body;
101
- if (record.ok === true) return {
102
- ok: true,
103
- data: record.data
104
- };
105
- const nested = record.error;
106
- const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
107
- const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
108
- const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
109
- if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
110
- ok: false,
111
- failure: {
112
- kind: "refused",
113
- code,
114
- message
115
- }
116
- };
117
- }
118
- return {
119
- ok: false,
120
- failure: {
121
- kind: "server",
122
- status: response.status
123
- }
124
- };
125
- }
126
- /** The one-sentence rendering legacy callers attach to a classified failure. */
127
- function failureMessage(failure) {
128
- switch (failure.kind) {
129
- case "refused": return failure.message;
130
- case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
131
- case "unauthenticated": return "HTTP 401";
132
- case "forbidden": return "HTTP 403";
133
- case "unmounted": return "HTTP 404";
134
- }
135
- }
136
- //#endregion
137
- //#region src/panels/settings/api.ts
138
- /**
139
- * The approval routes this bundle calls, and the ONE place a request is made.
140
- *
141
- * Structural types on purpose: the browser half cannot import the host package at runtime, so the row
142
- * shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
143
- * same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
144
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/api
145
- */
146
- /**
147
- * Build the approval face over one fetch implementation.
148
- * @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
149
- * @returns the three calls the pending card makes.
150
- */
151
- function createApprovalApi(doFetch) {
152
- const post = async (path, id) => await requestJson(doFetch, path, {
153
- method: "POST",
154
- headers: { "content-type": "application/json" },
155
- body: JSON.stringify({ id })
156
- });
157
- return {
158
- pending: async (status) => await requestJson(doFetch, status === void 0 ? ROUTES.approval.pending : ROUTES.approval.pending + "?status=" + status),
159
- approve: async (id) => await post(ROUTES.approval.approve, id),
160
- reject: async (id) => await post(ROUTES.approval.reject, id),
161
- preview: async (id) => await requestJson(doFetch, ROUTES.approval.preview + "?id=" + encodeURIComponent(id))
162
- };
163
- }
164
- //#endregion
165
8
  //#region src/panels/settings/generated-params.ts
166
9
  const CLIENT_PARAM_SECTIONS = [
167
10
  {
@@ -171,37 +14,55 @@ window.__ModuleLoader__.load({
171
14
  {
172
15
  id: "evolution-approval.enabled",
173
16
  group: "deployment",
17
+ groupLabel: "默认",
18
+ groupLabelEn: "Defaults",
174
19
  doc: "Master approval switch: run the approval pipeline for foreground writes.",
175
20
  label: "启用批准流水线",
21
+ labelEn: "Enable approval pipeline",
176
22
  hint: "关闭后前台写入不再经批准。",
23
+ hintEn: "Master approval switch: run the approval pipeline for foreground writes.",
177
24
  control: "switch",
178
25
  unit: "",
26
+ unitEn: "",
179
27
  values: [],
180
28
  valueLabels: [],
29
+ valueLabelsEn: [],
181
30
  atLeast: ""
182
31
  },
183
32
  {
184
33
  id: "evolution-approval.pendingTtlMs",
185
34
  group: "deployment",
35
+ groupLabel: "默认",
36
+ groupLabelEn: "Defaults",
186
37
  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.",
187
38
  label: "待批过期时间",
39
+ labelEn: "Pending expiry",
188
40
  hint: "一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。",
41
+ hintEn: "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.",
189
42
  control: "number",
190
43
  unit: "毫秒",
44
+ unitEn: "ms",
191
45
  values: [],
192
46
  valueLabels: [],
47
+ valueLabelsEn: [],
193
48
  atLeast: ""
194
49
  },
195
50
  {
196
51
  id: "approvalConfirm",
197
52
  group: "write-caps",
53
+ groupLabel: "写入上限",
54
+ groupLabelEn: "Write limits",
198
55
  doc: "What the confirmation before an APPROVE does: execute immediately, or ask first.",
199
56
  label: "批准前先确认",
57
+ labelEn: "Confirm before approving",
200
58
  hint: "点「批准」时要不要再确认一次:直接执行 / 先确认。默认直接执行(不打断批量批准)。",
59
+ hintEn: "What the confirmation before an APPROVE does: execute immediately, or ask first.",
201
60
  control: "select",
202
61
  unit: "",
62
+ unitEn: "",
203
63
  values: ["auto", "ask"],
204
64
  valueLabels: ["直接执行", "先确认"],
65
+ valueLabelsEn: ["Execute directly", "Ask first"],
205
66
  atLeast: ""
206
67
  }
207
68
  ]
@@ -212,13 +73,19 @@ window.__ModuleLoader__.load({
212
73
  fields: [{
213
74
  id: "evolution-commands.maintainHandoffOnSettle",
214
75
  group: "deployment",
76
+ groupLabel: "默认",
77
+ groupLabelEn: "Defaults",
215
78
  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.",
216
79
  label: "扫描完成后自动交接",
80
+ labelEn: "Hand off after a scan",
217
81
  hint: "维护扫描成功结束后,自动把报告交给模型(等同 maintain handoff);默认关闭,每次成功扫描会多花一次模型调用。",
82
+ hintEn: "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.",
218
83
  control: "switch",
219
84
  unit: "",
85
+ unitEn: "",
220
86
  values: [],
221
87
  valueLabels: [],
88
+ valueLabelsEn: [],
222
89
  atLeast: ""
223
90
  }]
224
91
  },
@@ -229,133 +96,199 @@ window.__ModuleLoader__.load({
229
96
  {
230
97
  id: "curatorIntervalHours",
231
98
  group: "curator",
99
+ groupLabel: "技能整理",
100
+ groupLabelEn: "Skill tidy-up",
232
101
  doc: "Minimum hours between deterministic curation passes.",
233
102
  label: "自动整理间隔",
103
+ labelEn: "Tidy-up interval",
234
104
  hint: "两次自动整理之间至少间隔多少小时。",
105
+ hintEn: "Minimum hours between deterministic curation passes.",
235
106
  control: "number",
236
107
  unit: "小时",
108
+ unitEn: "hours",
237
109
  values: [],
238
110
  valueLabels: [],
111
+ valueLabelsEn: [],
239
112
  atLeast: ""
240
113
  },
241
114
  {
242
115
  id: "staleAfterDays",
243
116
  group: "curator",
117
+ groupLabel: "技能整理",
118
+ groupLabelEn: "Skill tidy-up",
244
119
  doc: "Inactive days before a skill counts as stale.",
245
120
  label: "多久没用算过时",
121
+ labelEn: "Days until stale",
246
122
  hint: "技能连续多少天没被用到就算过时。",
123
+ hintEn: "Inactive days before a skill counts as stale.",
247
124
  control: "number",
248
125
  unit: "天",
126
+ unitEn: "days",
249
127
  values: [],
250
128
  valueLabels: [],
129
+ valueLabelsEn: [],
251
130
  atLeast: ""
252
131
  },
253
132
  {
254
133
  id: "archiveAfterDays",
255
134
  group: "curator",
135
+ groupLabel: "技能整理",
136
+ groupLabelEn: "Skill tidy-up",
256
137
  doc: "Inactive days before a stale skill is archived (must be >= staleAfterDays).",
257
138
  label: "过时后归档",
139
+ labelEn: "Archive after stale",
258
140
  hint: "过时技能再过多少天移入归档(不能小于上一项)。",
141
+ hintEn: "Inactive days before a stale skill is archived (must be >= staleAfterDays).",
259
142
  control: "number",
260
143
  unit: "天",
144
+ unitEn: "days",
261
145
  values: [],
262
146
  valueLabels: [],
147
+ valueLabelsEn: [],
263
148
  atLeast: "staleAfterDays"
264
149
  },
265
150
  {
266
151
  id: "minIdleHours",
267
152
  group: "curator",
153
+ groupLabel: "技能整理",
154
+ groupLabelEn: "Skill tidy-up",
268
155
  doc: "Idle hours required before an automatic curation pass runs.",
269
156
  label: "最少空闲时长",
157
+ labelEn: "Minimum idle time",
270
158
  hint: "你至少多少小时没用,才运行自动整理。",
159
+ hintEn: "Idle hours required before an automatic curation pass runs.",
271
160
  control: "number",
272
161
  unit: "小时",
162
+ unitEn: "hours",
273
163
  values: [],
274
164
  valueLabels: [],
165
+ valueLabelsEn: [],
275
166
  atLeast: ""
276
167
  },
277
168
  {
278
169
  id: "minIdleFailOpen",
279
170
  group: "curator",
171
+ groupLabel: "技能整理",
172
+ groupLabelEn: "Skill tidy-up",
280
173
  doc: "Let the idle gate open when the activity probe is unavailable.",
281
174
  label: "检测不到活动也运行",
175
+ labelEn: "Run without activity signal",
282
176
  hint: "无法判断你是否空闲时,仍然运行自动整理。",
177
+ hintEn: "Let the idle gate open when the activity probe is unavailable.",
283
178
  control: "switch",
284
179
  unit: "",
180
+ unitEn: "",
285
181
  values: [],
286
182
  valueLabels: [],
183
+ valueLabelsEn: [],
287
184
  atLeast: ""
288
185
  },
289
186
  {
290
187
  id: "llmReview",
291
188
  group: "curator",
189
+ groupLabel: "技能整理",
190
+ groupLabelEn: "Skill tidy-up",
292
191
  doc: "Enable the LLM nomination pass on top of the deterministic lifecycle.",
293
192
  label: "用模型挑选",
193
+ labelEn: "Let the model nominate",
294
194
  hint: "在固定规则之外,再用模型挑一遍候选项。",
195
+ hintEn: "Enable the LLM nomination pass on top of the deterministic lifecycle.",
295
196
  control: "switch",
296
197
  unit: "",
198
+ unitEn: "",
297
199
  values: [],
298
200
  valueLabels: [],
201
+ valueLabelsEn: [],
299
202
  atLeast: ""
300
203
  },
301
204
  {
302
205
  id: "curatorReviewMaxTokens",
303
206
  group: "curator",
207
+ groupLabel: "技能整理",
208
+ groupLabelEn: "Skill tidy-up",
304
209
  doc: "Token budget of the curator LLM review.",
305
210
  label: "模型处理上限",
211
+ labelEn: "Model token budget",
306
212
  hint: "每次用模型挑选时最多消耗多少 token。",
213
+ hintEn: "Token budget of the curator LLM review.",
307
214
  control: "number",
308
215
  unit: "token",
216
+ unitEn: "tokens",
309
217
  values: [],
310
218
  valueLabels: [],
219
+ valueLabelsEn: [],
311
220
  atLeast: ""
312
221
  },
313
222
  {
314
223
  id: "curatorReviewTimeoutMs",
315
224
  group: "curator",
225
+ groupLabel: "技能整理",
226
+ groupLabelEn: "Skill tidy-up",
316
227
  doc: "Wall-clock bound of the curator LLM review.",
317
228
  label: "模型处理超时",
229
+ labelEn: "Model review timeout",
318
230
  hint: "一次模型挑选最长允许运行多久。",
231
+ hintEn: "Wall-clock bound of the curator LLM review.",
319
232
  control: "number",
320
233
  unit: "毫秒",
234
+ unitEn: "ms",
321
235
  values: [],
322
236
  valueLabels: [],
237
+ valueLabelsEn: [],
323
238
  atLeast: ""
324
239
  },
325
240
  {
326
241
  id: "healthSoftBodyChars",
327
242
  group: "curator",
243
+ groupLabel: "技能整理",
244
+ groupLabelEn: "Skill tidy-up",
328
245
  doc: "Body character line the health view judges against.",
329
246
  label: "正文长度参考值",
247
+ labelEn: "Body length reference",
330
248
  hint: "技能正文超过多少字符算偏长(只用于健康提示)。",
249
+ hintEn: "Body character line the health view judges against.",
331
250
  control: "number",
332
251
  unit: "字符",
252
+ unitEn: "chars",
333
253
  values: [],
334
254
  valueLabels: [],
255
+ valueLabelsEn: [],
335
256
  atLeast: ""
336
257
  },
337
258
  {
338
259
  id: "healthStampDensityPerKb",
339
260
  group: "curator",
261
+ groupLabel: "技能整理",
262
+ groupLabelEn: "Skill tidy-up",
340
263
  doc: "Stamp density per KB that flags log-like content in a body.",
341
264
  label: "日志化判定阈值",
265
+ labelEn: "Log-like density threshold",
342
266
  hint: "每 KB 出现多少个时间标记,就判为「像日志」。",
267
+ hintEn: "Stamp density per KB that flags log-like content in a body.",
343
268
  control: "number",
344
269
  unit: "处/KB",
270
+ unitEn: "per KB",
345
271
  values: [],
346
272
  valueLabels: [],
273
+ valueLabelsEn: [],
347
274
  atLeast: ""
348
275
  },
349
276
  {
350
277
  id: "healthChurnMinPatches",
351
278
  group: "curator",
279
+ groupLabel: "技能整理",
280
+ groupLabelEn: "Skill tidy-up",
352
281
  doc: "Patches without a read that flag a write-ghost skill.",
353
282
  label: "只写不读判定次数",
283
+ labelEn: "Write-only threshold",
354
284
  hint: "连续多少次改动都没有被读取,就判为「只写不读」。",
285
+ hintEn: "Patches without a read that flag a write-ghost skill.",
355
286
  control: "number",
356
287
  unit: "处",
288
+ unitEn: "patches",
357
289
  values: [],
358
290
  valueLabels: [],
291
+ valueLabelsEn: [],
359
292
  atLeast: ""
360
293
  }
361
294
  ]
@@ -367,37 +300,55 @@ window.__ModuleLoader__.load({
367
300
  {
368
301
  id: "mergeApproval",
369
302
  group: "merge",
303
+ groupLabel: "合并",
304
+ groupLabelEn: "Merge",
370
305
  doc: "Pause after round 1 and put the pair in the pending queue instead of authoring round 2 automatically.",
371
306
  label: "合并审批",
307
+ labelEn: "Merge approval",
372
308
  hint: "开启后,第一轮判定完先停下、进入待批准清单;关闭则直接进入第二轮合并。",
309
+ hintEn: "Pause after round 1 and put the pair in the pending queue instead of authoring round 2 automatically.",
373
310
  control: "switch",
374
311
  unit: "",
312
+ unitEn: "",
375
313
  values: [],
376
314
  valueLabels: [],
315
+ valueLabelsEn: [],
377
316
  atLeast: ""
378
317
  },
379
318
  {
380
319
  id: "mergeSkippedTtlDays",
381
320
  group: "merge",
321
+ groupLabel: "合并",
322
+ groupLabelEn: "Merge",
382
323
  doc: "Days a skipped pair stays out of screening; 0 means it never expires.",
383
324
  label: "无需合并保留天数",
325
+ labelEn: "Keep skipped pairs",
384
326
  hint: "判定无需合并的对,多少天内不再参与筛选;0 表示一直保留。",
327
+ hintEn: "Days a skipped pair stays out of screening; 0 means it never expires.",
385
328
  control: "number",
386
329
  unit: "天",
330
+ unitEn: "days",
387
331
  values: [],
388
332
  valueLabels: [],
333
+ valueLabelsEn: [],
389
334
  atLeast: ""
390
335
  },
391
336
  {
392
337
  id: "mergeRunTimeoutMinutes",
393
338
  group: "merge",
339
+ groupLabel: "合并",
340
+ groupLabelEn: "Merge",
394
341
  doc: "Wall-clock budget of one merge run; 0 means no limit.",
395
342
  label: "单次运行上限",
343
+ labelEn: "Run time limit",
396
344
  hint: "一次合并运行最多持续多少分钟;0 表示不设上限。",
345
+ hintEn: "Wall-clock budget of one merge run; 0 means no limit.",
397
346
  control: "number",
398
347
  unit: "分钟",
348
+ unitEn: "min",
399
349
  values: [],
400
350
  valueLabels: [],
351
+ valueLabelsEn: [],
401
352
  atLeast: ""
402
353
  }
403
354
  ]
@@ -409,35 +360,52 @@ window.__ModuleLoader__.load({
409
360
  {
410
361
  id: "reviewSkillInterval",
411
362
  group: "review",
363
+ groupLabel: "审查",
364
+ groupLabelEn: "Review",
412
365
  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.",
413
366
  label: "技能检查间隔",
367
+ labelEn: "Skill review interval",
414
368
  hint: "每累计多少次工具调用检查一次技能:一轮按该轮工具调用数累加(一轮没有工具调用也算 1 次),本轮本身用到技能则只算 1 次。",
369
+ hintEn: "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.",
415
370
  control: "number",
416
371
  unit: "次",
372
+ unitEn: "calls",
417
373
  values: [],
418
374
  valueLabels: [],
375
+ valueLabelsEn: [],
419
376
  atLeast: ""
420
377
  },
421
378
  {
422
379
  id: "reviewMemoryInterval",
423
380
  group: "review",
381
+ groupLabel: "审查",
382
+ groupLabelEn: "Review",
424
383
  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.",
425
384
  label: "记忆检查间隔",
385
+ labelEn: "Memory review interval",
426
386
  hint: "每累计多少次工具调用检查一次记忆:一轮按该轮工具调用数累加(一轮没有工具调用也算 1 次),本轮本身用到记忆则只算 1 次。",
387
+ hintEn: "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.",
427
388
  control: "number",
428
389
  unit: "次",
390
+ unitEn: "calls",
429
391
  values: [],
430
392
  valueLabels: [],
393
+ valueLabelsEn: [],
431
394
  atLeast: ""
432
395
  },
433
396
  {
434
397
  id: "skillReviewTrigger",
435
398
  group: "review",
399
+ groupLabel: "审查",
400
+ groupLabelEn: "Review",
436
401
  doc: "Which channel may inject a skill review (cadence, completion, both).",
437
402
  label: "技能检查时机",
403
+ labelEn: "Skill review trigger",
438
404
  hint: "什么时候检查技能:按间隔/按任务完成/两者。",
405
+ hintEn: "Which channel may inject a skill review (cadence, completion, both).",
439
406
  control: "select",
440
407
  unit: "",
408
+ unitEn: "",
441
409
  values: [
442
410
  "cadence",
443
411
  "completion",
@@ -448,66 +416,101 @@ window.__ModuleLoader__.load({
448
416
  "按任务完成",
449
417
  "两者"
450
418
  ],
419
+ valueLabelsEn: [
420
+ "By interval",
421
+ "On completion",
422
+ "Both"
423
+ ],
451
424
  atLeast: ""
452
425
  },
453
426
  {
454
427
  id: "skillReviewCompletionMinToolCalls",
455
428
  group: "review",
429
+ groupLabel: "审查",
430
+ groupLabelEn: "Review",
456
431
  doc: "Tool calls a task needs before the completion channel injects.",
457
432
  label: "完成时的最少工具调用",
433
+ labelEn: "Minimum tool calls on completion",
458
434
  hint: "选「按任务完成」时,任务至少用了几次工具才检查。",
435
+ hintEn: "Tool calls a task needs before the completion channel injects.",
459
436
  control: "number",
460
437
  unit: "次",
438
+ unitEn: "calls",
461
439
  values: [],
462
440
  valueLabels: [],
441
+ valueLabelsEn: [],
463
442
  atLeast: ""
464
443
  },
465
444
  {
466
445
  id: "reviewEnabled",
467
446
  group: "review",
447
+ groupLabel: "审查",
448
+ groupLabelEn: "Review",
468
449
  doc: "Master switch for the review plugin.",
469
450
  label: "启用自动检查",
451
+ labelEn: "Enable automatic review",
470
452
  hint: "关闭后不再自动回顾会话。",
453
+ hintEn: "Master switch for the review plugin.",
471
454
  control: "switch",
472
455
  unit: "",
456
+ unitEn: "",
473
457
  values: [],
474
458
  valueLabels: [],
459
+ valueLabelsEn: [],
475
460
  atLeast: ""
476
461
  },
477
462
  {
478
463
  id: "reviewMode",
479
464
  group: "review",
465
+ groupLabel: "审查",
466
+ groupLabelEn: "Review",
480
467
  doc: "Run the review in the parent session (inject), on a subagent, or in a background branch.",
481
468
  label: "运行位置",
469
+ labelEn: "Review location",
482
470
  hint: "在哪里运行检查:在当前会话里/交给子代理/后台分支。",
471
+ hintEn: "Run the review in the parent session (inject), on a subagent, or in a background branch.",
483
472
  control: "select",
484
473
  unit: "",
474
+ unitEn: "",
485
475
  values: ["inject", "branch"],
486
476
  valueLabels: ["在当前会话里", "后台分支"],
477
+ valueLabelsEn: ["In this session", "Background branch"],
487
478
  atLeast: ""
488
479
  },
489
480
  {
490
481
  id: "reviewWakeInject",
491
482
  group: "review",
483
+ groupLabel: "审查",
484
+ groupLabelEn: "Review",
492
485
  doc: "Deliver the deferred review as a waking follow-up message.",
493
486
  label: "延后结果发提醒",
487
+ labelEn: "Notify on deferred result",
494
488
  hint: "会话空闲时,把延后的检查结果作为一条提醒发给你。",
489
+ hintEn: "Deliver the deferred review as a waking follow-up message.",
495
490
  control: "switch",
496
491
  unit: "",
492
+ unitEn: "",
497
493
  values: [],
498
494
  valueLabels: [],
495
+ valueLabelsEn: [],
499
496
  atLeast: ""
500
497
  },
501
498
  {
502
499
  id: "reviewNotice",
503
500
  group: "review",
501
+ groupLabel: "审查",
502
+ groupLabelEn: "Review",
504
503
  doc: "Deliver result notices into the parent conversation.",
505
504
  label: "结果通知",
505
+ labelEn: "Result notice",
506
506
  hint: "关闭后检查结果不再写进对话;检查本身照常运行,结果留在审计记录里。",
507
+ hintEn: "Deliver result notices into the parent conversation.",
507
508
  control: "switch",
508
509
  unit: "",
510
+ unitEn: "",
509
511
  values: [],
510
512
  valueLabels: [],
513
+ valueLabelsEn: [],
511
514
  atLeast: ""
512
515
  }
513
516
  ]
@@ -519,49 +522,73 @@ window.__ModuleLoader__.load({
519
522
  {
520
523
  id: "memoryChars",
521
524
  group: "memory",
525
+ groupLabel: "记忆",
526
+ groupLabelEn: "Memory",
522
527
  doc: "Character budget the memory store enforces for MEMORY.md.",
523
528
  label: "记忆库上限",
529
+ labelEn: "Memory store limit",
524
530
  hint: "长期记忆文件(MEMORY.md)最多多少字符。",
531
+ hintEn: "Character budget the memory store enforces for MEMORY.md.",
525
532
  control: "number",
526
533
  unit: "字符",
534
+ unitEn: "chars",
527
535
  values: [],
528
536
  valueLabels: [],
537
+ valueLabelsEn: [],
529
538
  atLeast: ""
530
539
  },
531
540
  {
532
541
  id: "userChars",
533
542
  group: "memory",
543
+ groupLabel: "记忆",
544
+ groupLabelEn: "Memory",
534
545
  doc: "Character budget the memory store enforces for USER.md.",
535
546
  label: "用户画像上限",
547
+ labelEn: "User profile limit",
536
548
  hint: "用户画像文件(USER.md)最多多少字符。",
549
+ hintEn: "Character budget the memory store enforces for USER.md.",
537
550
  control: "number",
538
551
  unit: "字符",
552
+ unitEn: "chars",
539
553
  values: [],
540
554
  valueLabels: [],
555
+ valueLabelsEn: [],
541
556
  atLeast: ""
542
557
  },
543
558
  {
544
559
  id: "addDatePrefix",
545
560
  group: "memory",
561
+ groupLabel: "记忆",
562
+ groupLabelEn: "Memory",
546
563
  doc: "Prefix stored memory entries with their date heading.",
547
564
  label: "记忆条目加日期",
565
+ labelEn: "Prefix memory entries with date",
548
566
  hint: "写入时在条目开头加上日期。",
567
+ hintEn: "Prefix stored memory entries with their date heading.",
549
568
  control: "switch",
550
569
  unit: "",
570
+ unitEn: "",
551
571
  values: [],
552
572
  valueLabels: [],
573
+ valueLabelsEn: [],
553
574
  atLeast: ""
554
575
  },
555
576
  {
556
577
  id: "maxConsolidationFailures",
557
578
  group: "memory",
579
+ groupLabel: "记忆",
580
+ groupLabelEn: "Memory",
558
581
  doc: "Consolidation failures one turn tolerates before the tool gives up.",
559
582
  label: "合并失败重试",
583
+ labelEn: "Consolidation retries",
560
584
  hint: "合并记忆连续失败多少次后停止。",
585
+ hintEn: "Consolidation failures one turn tolerates before the tool gives up.",
561
586
  control: "number",
562
587
  unit: "次",
588
+ unitEn: "calls",
563
589
  values: [],
564
590
  valueLabels: [],
591
+ valueLabelsEn: [],
565
592
  atLeast: ""
566
593
  }
567
594
  ]
@@ -572,13 +599,19 @@ window.__ModuleLoader__.load({
572
599
  fields: [{
573
600
  id: "entryPreviewChars",
574
601
  group: "memory",
602
+ groupLabel: "记忆",
603
+ groupLabelEn: "Memory",
575
604
  doc: "Characters of one memory entry shown in a tool result preview.",
576
605
  label: "记忆预览长度",
606
+ labelEn: "Memory preview length",
577
607
  hint: "列表里每条记忆最多显示多少字符。",
608
+ hintEn: "Characters of one memory entry shown in a tool result preview.",
578
609
  control: "number",
579
610
  unit: "字符",
611
+ unitEn: "chars",
580
612
  values: [],
581
613
  valueLabels: [],
614
+ valueLabelsEn: [],
582
615
  atLeast: ""
583
616
  }]
584
617
  },
@@ -589,107 +622,160 @@ window.__ModuleLoader__.load({
589
622
  {
590
623
  id: "skillContentChars",
591
624
  group: "write-caps",
625
+ groupLabel: "写入上限",
626
+ groupLabelEn: "Write limits",
592
627
  doc: "Character cap on a SKILL.md body (tighten-only).",
593
628
  label: "技能文件正文上限",
629
+ labelEn: "Skill body limit",
594
630
  hint: "技能 Markdown 正文最多多少字符(只能调小)。",
631
+ hintEn: "Character cap on a SKILL.md body (tighten-only).",
595
632
  control: "number",
596
633
  unit: "字符",
634
+ unitEn: "chars",
597
635
  values: [],
598
636
  valueLabels: [],
637
+ valueLabelsEn: [],
599
638
  atLeast: ""
600
639
  },
601
640
  {
602
641
  id: "maxSkillFileBytes",
603
642
  group: "write-caps",
643
+ groupLabel: "写入上限",
644
+ groupLabelEn: "Write limits",
604
645
  doc: "Byte cap on one support file (tighten-only).",
605
646
  label: "附带文件大小上限",
647
+ labelEn: "Support file size limit",
606
648
  hint: "技能附带文件单个最大多少字节(只能调小)。",
649
+ hintEn: "Byte cap on one support file (tighten-only).",
607
650
  control: "number",
608
651
  unit: "字节",
652
+ unitEn: "bytes",
609
653
  values: [],
610
654
  valueLabels: [],
655
+ valueLabelsEn: [],
611
656
  atLeast: ""
612
657
  },
613
658
  {
614
659
  id: "maxSkillNameLength",
615
660
  group: "write-caps",
661
+ groupLabel: "写入上限",
662
+ groupLabelEn: "Write limits",
616
663
  doc: "Character cap on a skill name (tighten-only).",
617
664
  label: "技能名长度上限",
665
+ labelEn: "Skill name length limit",
618
666
  hint: "技能名最多多少字符(只能调小)。",
667
+ hintEn: "Character cap on a skill name (tighten-only).",
619
668
  control: "number",
620
669
  unit: "字符",
670
+ unitEn: "chars",
621
671
  values: [],
622
672
  valueLabels: [],
673
+ valueLabelsEn: [],
623
674
  atLeast: ""
624
675
  },
625
676
  {
626
677
  id: "maxDescriptionLength",
627
678
  group: "write-caps",
679
+ groupLabel: "写入上限",
680
+ groupLabelEn: "Write limits",
628
681
  doc: "Character cap on a skill description (tighten-only).",
629
682
  label: "技能描述长度上限",
683
+ labelEn: "Description length limit",
630
684
  hint: "技能描述最多多少字符(只能调小)。",
685
+ hintEn: "Character cap on a skill description (tighten-only).",
631
686
  control: "number",
632
687
  unit: "字符",
688
+ unitEn: "chars",
633
689
  values: [],
634
690
  valueLabels: [],
691
+ valueLabelsEn: [],
635
692
  atLeast: ""
636
693
  },
637
694
  {
638
695
  id: "descriptionStrict",
639
696
  group: "write-caps",
697
+ groupLabel: "写入上限",
698
+ groupLabelEn: "Write limits",
640
699
  doc: "Refuse a description over the authoring bar instead of advising.",
641
700
  label: "描述不合规即拒绝",
701
+ labelEn: "Reject invalid descriptions",
642
702
  hint: "描述不符合要求时直接拒绝写入(默认只提醒)。",
703
+ hintEn: "Refuse a description over the authoring bar instead of advising.",
643
704
  control: "switch",
644
705
  unit: "",
706
+ unitEn: "",
645
707
  values: [],
646
708
  valueLabels: [],
709
+ valueLabelsEn: [],
647
710
  atLeast: ""
648
711
  },
649
712
  {
650
713
  id: "strictCrossSource",
651
714
  group: "write-caps",
715
+ groupLabel: "写入上限",
716
+ groupLabelEn: "Write limits",
652
717
  doc: "Refuse writes whose catalog entry resolves outside the family.",
653
718
  label: "引用外部内容即拒绝",
719
+ labelEn: "Reject external references",
654
720
  hint: "技能引用了本插件之外的文件时拒绝写入。",
721
+ hintEn: "Refuse writes whose catalog entry resolves outside the family.",
655
722
  control: "switch",
656
723
  unit: "",
724
+ unitEn: "",
657
725
  values: [],
658
726
  valueLabels: [],
727
+ valueLabelsEn: [],
659
728
  atLeast: ""
660
729
  },
661
730
  {
662
731
  id: "citationPolicy",
663
732
  group: "write-caps",
733
+ groupLabel: "写入上限",
734
+ groupLabelEn: "Write limits",
664
735
  doc: "Refuse a move that would leave a dangling reference, or verify it.",
665
736
  label: "引用检查方式",
737
+ labelEn: "Citation check",
666
738
  hint: "移动或合并技能时怎么处理引用:先检查/直接拒绝。",
739
+ hintEn: "Refuse a move that would leave a dangling reference, or verify it.",
667
740
  control: "select",
668
741
  unit: "",
742
+ unitEn: "",
669
743
  values: ["verify", "refuse"],
670
744
  valueLabels: ["先检查", "直接拒绝"],
745
+ valueLabelsEn: ["Verify first", "Refuse"],
671
746
  atLeast: ""
672
747
  },
673
748
  {
674
749
  id: "supportFileCharPolicy",
675
750
  group: "write-caps",
751
+ groupLabel: "写入上限",
752
+ groupLabelEn: "Write limits",
676
753
  doc: "Warn about an oversize support file, or refuse the write.",
677
754
  label: "附带文件超限时",
755
+ labelEn: "Oversize support file",
678
756
  hint: "附带文件超出上限时:只提醒/拒绝写入。",
757
+ hintEn: "Warn about an oversize support file, or refuse the write.",
679
758
  control: "select",
680
759
  unit: "",
760
+ unitEn: "",
681
761
  values: ["report", "enforce"],
682
762
  valueLabels: ["只提醒", "拒绝写入"],
763
+ valueLabelsEn: ["Warn only", "Refuse write"],
683
764
  atLeast: ""
684
765
  },
685
766
  {
686
767
  id: "skillWriteConfirm",
687
768
  group: "write-caps",
769
+ groupLabel: "写入上限",
770
+ groupLabelEn: "Write limits",
688
771
  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.",
689
772
  label: "写入前是否确认",
773
+ labelEn: "Confirm before writing",
690
774
  hint: "新建或删除技能前要不要问你:直接写入/一直等你回答/超时未答就取消这次写入。",
775
+ hintEn: "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.",
691
776
  control: "select",
692
777
  unit: "",
778
+ unitEn: "",
693
779
  values: [
694
780
  "auto",
695
781
  "ask",
@@ -700,63 +786,34 @@ window.__ModuleLoader__.load({
700
786
  "一直等待回答",
701
787
  "超时自动取消"
702
788
  ],
789
+ valueLabelsEn: [
790
+ "Write directly",
791
+ "Wait for answer",
792
+ "Cancel on timeout"
793
+ ],
703
794
  atLeast: ""
704
795
  },
705
796
  {
706
797
  id: "skillWriteConfirmTimeoutSeconds",
707
798
  group: "write-caps",
799
+ groupLabel: "写入上限",
800
+ groupLabelEn: "Write limits",
708
801
  doc: "Seconds the confirmation waits for an answer in timeout mode before the write is cancelled.",
709
802
  label: "确认等待秒数",
803
+ labelEn: "Confirmation timeout",
710
804
  hint: "选「超时自动取消」时,等多少秒没人回答就取消这次写入。",
805
+ hintEn: "Seconds the confirmation waits for an answer in timeout mode before the write is cancelled.",
711
806
  control: "number",
712
807
  unit: "秒",
808
+ unitEn: "s",
713
809
  values: [],
714
810
  valueLabels: [],
811
+ valueLabelsEn: [],
715
812
  atLeast: ""
716
813
  }
717
814
  ]
718
815
  }
719
816
  ];
720
- /** Every (bundle, row) pair the Plugins page can open a configuration page for. */
721
- const CLIENT_ROW_SEATS = [
722
- {
723
- bundle: "@lmzhen/dsh-evolution-all",
724
- rows: [
725
- "evolution-approval",
726
- "evolution-commands",
727
- "evolution-curator",
728
- "evolution-merge",
729
- "evolution-review",
730
- "memory-files",
731
- "tool-memory",
732
- "tool-skill-manage"
733
- ]
734
- },
735
- {
736
- bundle: "@lmzhen/dsh-evolution-host",
737
- rows: [
738
- "evolution-approval",
739
- "evolution-commands",
740
- "evolution-curator",
741
- "evolution-merge",
742
- "evolution-review",
743
- "memory-files"
744
- ]
745
- },
746
- {
747
- bundle: "@lmzhen/dsh-evolution-preset",
748
- rows: [
749
- "evolution-approval",
750
- "evolution-commands",
751
- "evolution-curator",
752
- "evolution-merge",
753
- "evolution-review",
754
- "memory-files",
755
- "tool-memory",
756
- "tool-skill-manage"
757
- ]
758
- }
759
- ];
760
817
  //#endregion
761
818
  //#region ../evolution-client-kit/src/generated/states.generated.ts
762
819
  /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
@@ -787,521 +844,295 @@ window.__ModuleLoader__.load({
787
844
  });
788
845
  }
789
846
  //#endregion
790
- //#region src/panels/settings/pending-state.ts
791
- /** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
792
- const INITIAL_PENDING_STATE = {
793
- view: { kind: "loading" },
794
- busy: [],
795
- notice: null,
796
- open: null,
797
- preview: null
798
- };
799
- /** Open one row's preview: the row is now the open one and its answer is on the way. */
800
- function expanded(state, id) {
801
- return {
802
- ...state,
803
- open: id,
804
- preview: { kind: "loading" }
805
- };
806
- }
807
- /** Close whatever preview is open. */
808
- function collapsed(state) {
809
- return {
810
- ...state,
811
- open: null,
812
- preview: null
813
- };
814
- }
847
+ //#region src/panels/settings/DiagnosticsCard.ts
815
848
  /**
816
- * A preview answer arrived. An answer for a row the reader has already left is DROPPED: it would
817
- * otherwise paint one row's diff under another row's heading.
849
+ * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
850
+ * `/api/dsh-evolution/diagnostics` route answer.
851
+ *
852
+ * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
853
+ * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
854
+ * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
855
+ * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
856
+ * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
857
+ * sheet; the runtime window is the whole reading).
858
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
818
859
  */
819
- function previewArrived(state, id, view) {
820
- return state.open === id ? {
821
- ...state,
822
- preview: view
823
- } : state;
824
- }
825
- /** The first load, or a refresh after a decision: back to loading, keeping no stale rows. */
826
- function loading(state) {
860
+ /** Which word one status gets (the frozen W1 status set). */
861
+ const STATUS_KEYS = Object.freeze({
862
+ ok: "diagStatusOk",
863
+ warn: "diagStatusWarn",
864
+ fail: "diagStatusFail",
865
+ unknown: "diagStatusUnknown"
866
+ });
867
+ /** The summary word for a badge status (the same frozen set; ok has no count). */
868
+ const SUMMARY_KEYS = Object.freeze({
869
+ ok: "diagSummaryOk",
870
+ warn: "diagSummaryWarn",
871
+ fail: "diagSummaryFail",
872
+ unknown: "diagSummaryUnknown"
873
+ });
874
+ /** The one status the card leads with: fail > warn > unknown > ok. */
875
+ function summaryOf(rows) {
876
+ const count = (status) => rows.filter((row) => row.status === status).length;
877
+ for (const status of [
878
+ "fail",
879
+ "warn",
880
+ "unknown"
881
+ ]) {
882
+ const n = count(status);
883
+ if (n > 0) return {
884
+ status,
885
+ count: n
886
+ };
887
+ }
827
888
  return {
828
- ...state,
829
- view: { kind: "loading" }
889
+ status: "ok",
890
+ count: 0
830
891
  };
831
892
  }
832
893
  /**
833
- * A list arrived. An empty list is its OWN state — the reader is told the window is clear rather than
834
- * being shown the same line a slow answer shows.
894
+ * Render the card.
895
+ * @param props - the copy seat, the host call and the command fallback.
896
+ * @returns the card.
835
897
  */
836
- function loaded(state, rows) {
837
- return {
838
- ...state,
839
- view: rows.length === 0 ? { kind: "empty" } : {
840
- kind: "ready",
841
- rows
842
- }
898
+ function DiagnosticsCard(props) {
899
+ const { t, api } = props;
900
+ const translate = (key, values) => t(key, values);
901
+ const [state, setState] = (0, react.useState)({ phase: "loading" });
902
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
903
+ const [open, setOpen] = (0, react.useState)(false);
904
+ const ticket = (0, react.useRef)(0);
905
+ const read = (keepView) => {
906
+ const current = ticket.current + 1;
907
+ ticket.current = current;
908
+ if (keepView) setRefreshing(true);
909
+ else setState({ phase: "loading" });
910
+ api.view().then((answer) => {
911
+ if (ticket.current !== current) return;
912
+ setState(answer.ok ? {
913
+ phase: "ready",
914
+ view: answer.data
915
+ } : {
916
+ phase: "failed",
917
+ failure: answer.failure
918
+ });
919
+ }).finally(() => {
920
+ if (ticket.current === current) setRefreshing(false);
921
+ });
843
922
  };
844
- }
845
- /** The list could not be read: the classified failure stays on screen next to a retry control. */
846
- function failed(state, failure) {
847
- return {
848
- ...state,
849
- view: {
850
- kind: "failed",
851
- failure
923
+ (0, react.useEffect)(() => {
924
+ read(false);
925
+ }, []);
926
+ const refresh = () => {
927
+ read(true);
928
+ };
929
+ const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
930
+ const header = (0, react.createElement)("button", {
931
+ type: "button",
932
+ className: "evolution-param-head",
933
+ "aria-expanded": open ? "true" : "false",
934
+ onClick: () => {
935
+ setOpen(!open);
852
936
  }
937
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
938
+ className: "evo-diag-badge",
939
+ "data-status": summary.status
940
+ }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
941
+ 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", {
942
+ key: "runtime-" + row.key,
943
+ className: "evo-diag-item",
944
+ "data-kind": "runtime",
945
+ "data-status": row.status
946
+ }, (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)))));
947
+ const body = () => {
948
+ if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
949
+ if (state.phase === "failed") return FailureBlock({
950
+ failure: state.failure,
951
+ t: translate,
952
+ fallback: props.fallback
953
+ });
954
+ const view = state.view;
955
+ 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", {
956
+ type: "button",
957
+ className: "evolution-param-button",
958
+ onClick: refresh,
959
+ disabled: refreshing,
960
+ "data-action": "diag-refresh"
961
+ }, t(refreshing ? "diagRefreshing" : "diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
853
962
  };
963
+ return (0, react.createElement)("section", {
964
+ className: "evolution-param-card",
965
+ "data-card": "diagnostics"
966
+ }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
854
967
  }
855
- /** Mark one record as having an action in flight (its buttons are disabled until it ends). */
856
- function beginAction(state, id) {
857
- return state.busy.includes(id) ? state : {
858
- ...state,
859
- busy: [...state.busy, id]
968
+ //#endregion
969
+ //#region ../evolution-client-kit/src/generated/routes.generated.ts
970
+ /**
971
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
972
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
973
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
974
+ * client copies, and the generator's --check makes a stale copy red.
975
+ */
976
+ const ROUTES = {
977
+ approval: {
978
+ pending: "/api/dsh-evolution/approval/pending",
979
+ preview: "/api/dsh-evolution/approval/preview",
980
+ approve: "/api/dsh-evolution/approval/approve",
981
+ reject: "/api/dsh-evolution/approval/reject",
982
+ release: "/api/dsh-evolution/approval/release"
983
+ },
984
+ "skill-history": {
985
+ skills: "/api/dsh-evolution/skill-history/skills",
986
+ versions: "/api/dsh-evolution/skill-history/versions",
987
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
988
+ body: "/api/dsh-evolution/skill-history/versions/body",
989
+ undo: "/api/dsh-evolution/skill-history/undo",
990
+ health: "/api/dsh-evolution/skill-history/health"
991
+ },
992
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
993
+ merge: {
994
+ view: "/api/dsh-evolution/merge/view",
995
+ rows: "/api/dsh-evolution/merge/rows",
996
+ run: "/api/dsh-evolution/merge/run",
997
+ runs: "/api/dsh-evolution/merge/runs",
998
+ body: "/api/dsh-evolution/merge/body",
999
+ history: "/api/dsh-evolution/merge/history"
1000
+ },
1001
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" },
1002
+ settings: {
1003
+ describe: "/api/dsh-evolution/settings/describe",
1004
+ mutate: "/api/dsh-evolution/settings/mutate"
1005
+ }
1006
+ };
1007
+ //#endregion
1008
+ //#region ../evolution-client-kit/src/generated/route-client.generated.ts
1009
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
1010
+ function classify(status) {
1011
+ if (status === 401) return {
1012
+ kind: "unauthenticated",
1013
+ status: 401
1014
+ };
1015
+ if (status === 403) return {
1016
+ kind: "forbidden",
1017
+ status: 403
1018
+ };
1019
+ if (status === 404) return {
1020
+ kind: "unmounted",
1021
+ status: 404
860
1022
  };
861
- }
862
- /** The action ended, whatever it answered. */
863
- function endAction(state, id) {
864
1023
  return {
865
- ...state,
866
- busy: state.busy.filter((candidate) => candidate !== id)
1024
+ kind: "server",
1025
+ status
867
1026
  };
868
1027
  }
869
- /** One refusal or failure line under the list; `null` clears it (a later success does). */
870
- function noticed(state, message) {
1028
+ /**
1029
+ * One request, no retries and no body parsing for the platform's admission answers.
1030
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
1031
+ * @param path - the exact route path from the generated ROUTES table.
1032
+ * @param init - standard RequestInit.
1033
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
1034
+ */
1035
+ async function requestJson(doFetch, path, init) {
1036
+ let response;
1037
+ try {
1038
+ response = await doFetch(path, init);
1039
+ } catch (error) {
1040
+ return {
1041
+ ok: false,
1042
+ failure: {
1043
+ kind: "server",
1044
+ status: 0,
1045
+ detail: error instanceof Error ? error.message : String(error)
1046
+ }
1047
+ };
1048
+ }
1049
+ if (!response.ok) return {
1050
+ ok: false,
1051
+ failure: classify(response.status)
1052
+ };
1053
+ let body;
1054
+ try {
1055
+ body = await response.json();
1056
+ } catch {
1057
+ return {
1058
+ ok: false,
1059
+ failure: {
1060
+ kind: "server",
1061
+ status: response.status
1062
+ }
1063
+ };
1064
+ }
1065
+ if (typeof body === "object" && body !== null) {
1066
+ const record = body;
1067
+ if (record.ok === true) return {
1068
+ ok: true,
1069
+ data: record.data
1070
+ };
1071
+ const nested = record.error;
1072
+ const errorRecord = typeof nested === "object" && nested !== null ? nested : void 0;
1073
+ const code = typeof record.code === "string" && record.code !== "" ? record.code : typeof errorRecord?.code === "string" ? errorRecord.code : void 0;
1074
+ const message = typeof record.message === "string" && record.message !== "" ? record.message : typeof errorRecord?.message === "string" ? errorRecord.message : void 0;
1075
+ if (record.ok === false && code !== void 0 && message !== void 0 && message !== "") return {
1076
+ ok: false,
1077
+ failure: {
1078
+ kind: "refused",
1079
+ code,
1080
+ message
1081
+ }
1082
+ };
1083
+ }
871
1084
  return {
872
- ...state,
873
- notice: message
1085
+ ok: false,
1086
+ failure: {
1087
+ kind: "server",
1088
+ status: response.status
1089
+ }
874
1090
  };
875
1091
  }
1092
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
1093
+ function failureMessage(failure) {
1094
+ switch (failure.kind) {
1095
+ case "refused": return failure.message;
1096
+ case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
1097
+ case "unauthenticated": return "HTTP 401";
1098
+ case "forbidden": return "HTTP 403";
1099
+ case "unmounted": return "HTTP 404";
1100
+ }
1101
+ }
876
1102
  //#endregion
877
- //#region src/panels/settings/PendingCard.ts
1103
+ //#region src/panels/settings/diagnostics-state.ts
878
1104
  /**
879
- * The "writes awaiting approval" card: the staged write window, with the two decisions on each row.
880
- *
881
- * It is a card in the SAME section and wears the SAME shell as the parameter cards (header with title,
882
- * count and chevron; body of field blocks; a button row per block), so nothing here invents a second
883
- * visual language. Two things differ on purpose: the data comes from the host's approval routes rather
884
- * than the settings transport, and its three states are distinct — "reading", "nothing staged" and "the
885
- * read failed" each say so in their own words (E7: merging the first two is what made an empty panel
886
- * look like lost data).
1105
+ * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
1106
+ * GENERATED route client, and the four-state answer the card renders.
887
1107
  *
888
- * Every decision re-reads the window instead of predicting the outcome: the host owns the state, and a
889
- * refusal is shown with the host's own sentence.
890
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/PendingCard
1108
+ * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
1109
+ * and no computation — it only renders.
1110
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
891
1111
  */
892
1112
  /**
893
- * U-6: the registry's gate word, read out of a section's user layer. ONE definition, exported so the
894
- * mapping itself is judgeable: the entry does the wiring, this does the reading. Anything that is not
895
- * one of the two explicit words falls back to the shipped default (`auto`) — a document written by a
896
- * future version must not silently turn the confirmation ON, and a typo must not turn it off. The
897
- * independent review flagged that this reader had no case at all (P2-11).
1113
+ * Build the diagnostics face over one fetch implementation.
1114
+ * @param doFetch - the browser fetch, or a spec stub.
898
1115
  */
899
- function confirmModeFrom(user) {
900
- const value = typeof user === "object" && user !== null ? user.approvalConfirm : void 0;
901
- return value === "ask" || value === "timeout" ? "ask" : "auto";
1116
+ function createDiagnosticsApi(doFetch = fetch) {
1117
+ return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
902
1118
  }
903
- /** Which word one age bucket gets; an unknown bucket falls back to the count form. */
904
- const AGE_KEYS = Object.freeze({
905
- now: "approvalAgeNow",
906
- minutes: "approvalAgeMinutes",
907
- hours: "approvalAgeHours",
908
- days: "approvalAgeDays",
909
- months: "approvalAgeMonths"
910
- });
911
- /** Which word one staged kind gets. */
912
- const KIND_KEYS = Object.freeze({
913
- memory: "approvalKindMemory",
914
- skill: "approvalKindSkill",
915
- capability: "approvalKindCapability"
916
- });
1119
+ //#endregion
1120
+ //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
917
1121
  /**
918
- * Render the pending window.
919
- * @param props - the copy seat and the host calls.
920
- * @returns the card.
921
- */
922
- function PendingCard(props) {
923
- const { t, api } = props;
924
- const translate = (key, values) => t(key, values);
925
- const confirmMode = props.confirmMode?.() ?? "auto";
926
- const [armed, setArmed] = (0, react.useState)(null);
927
- const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
928
- const [open, setOpen] = (0, react.useState)(true);
929
- const latest = (0, react.useRef)(state);
930
- const apply = (next) => {
931
- const value = next(latest.current);
932
- latest.current = value;
933
- setState(value);
934
- };
935
- /**
936
- * Read the window the card is about: the writes WAITING and the writes RUNNING.
937
- *
938
- * T4-05/A47: the route has always taken `?status=` — the CLI asks for both lists — but this card
939
- * asked for neither, so a record whose approve died between the claim and the decision showed as
940
- * "nothing is waiting for a decision" while `/evolution pending` and doctor listed it. The two
941
- * answers are ordered the way the CLI orders them (waiting first); either one failing fails the
942
- * read, because a half-list would read as a complete window.
943
- */
944
- const readTicket = (0, react.useRef)(0);
945
- const load = async () => {
946
- apply(loading);
947
- const ticket = readTicket.current + 1;
948
- readTicket.current = ticket;
949
- const [waiting, running] = await Promise.all([api.pending(), api.pending("executing")]);
950
- if (readTicket.current !== ticket) return;
951
- if (!waiting.ok) {
952
- apply((current) => failed(current, waiting.failure));
953
- return;
954
- }
955
- if (!running.ok) {
956
- apply((current) => failed(current, running.failure));
957
- return;
958
- }
959
- apply((current) => loaded(current, [...waiting.data, ...running.data]));
960
- };
961
- (0, react.useEffect)(() => {
962
- load();
963
- const onFocus = () => {
964
- load();
965
- };
966
- window.addEventListener("focus", onFocus);
967
- return () => {
968
- window.removeEventListener("focus", onFocus);
969
- };
970
- }, []);
971
- const decide = (id, action) => {
972
- apply((current) => beginAction(current, id));
973
- (async () => {
974
- const answer = action === "approve" ? await api.approve(id) : await api.reject(id);
975
- if (!answer.ok) {
976
- apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
977
- return;
978
- }
979
- apply((current) => noticed(endAction(current, id), null));
980
- await load();
981
- })();
982
- };
983
- const age = (row) => {
984
- const key = AGE_KEYS[row.age.unit];
985
- if (key === void 0) return String(row.age.n);
986
- return t(key, { n: row.age.n });
987
- };
988
- /** Ask the host what this record would store; the answer lands only if the row is still open. */
989
- const loadPreview = async (id) => {
990
- const answer = await api.preview(id);
991
- const view = answer.ok ? answer.data.available ? {
992
- kind: "ready",
993
- answer: answer.data
994
- } : {
995
- kind: "unavailable",
996
- reason: answer.data.reason
997
- } : {
998
- kind: "failed",
999
- failure: answer.failure
1000
- };
1001
- apply((current) => previewArrived(current, id, view));
1002
- };
1003
- const togglePreview = (id) => {
1004
- if (state.open === id) {
1005
- apply(collapsed);
1006
- return;
1007
- }
1008
- apply((current) => expanded(current, id));
1009
- loadPreview(id);
1010
- };
1011
- /** The facts line, then the source view: the same shape the history panel's diff uses. */
1012
- const preview = (view) => {
1013
- if (view.kind === "loading") return (0, react.createElement)("p", {
1014
- className: "evolution-param-note",
1015
- role: "status"
1016
- }, t("approvalPreviewReading"));
1017
- if (view.kind === "failed") return (0, react.createElement)("p", {
1018
- className: "evolution-param-error",
1019
- role: "status"
1020
- }, failureText(view.failure, translate));
1021
- if (view.kind === "unavailable") return (0, react.createElement)("p", {
1022
- className: "evolution-param-error",
1023
- role: "status"
1024
- }, t("approvalPreviewNone") + view.reason);
1025
- const facts = [
1026
- (0, react.createElement)("span", {
1027
- className: "evolution-param-fact",
1028
- key: "added"
1029
- }, t("approvalDiffAdded", { n: view.answer.linesAdded })),
1030
- (0, react.createElement)("span", {
1031
- className: "evolution-param-fact",
1032
- key: "removed"
1033
- }, t("approvalDiffRemoved", { n: view.answer.linesRemoved })),
1034
- ...view.answer.truncated ? [(0, react.createElement)("span", {
1035
- className: "evolution-param-fact",
1036
- key: "cut"
1037
- }, t("approvalDiffCut"))] : []
1038
- ];
1039
- const lines = [];
1040
- for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
1041
- for (const [index, line] of hunk.oldText.split("\n").entries()) {
1042
- if (hunk.oldText === "" && line === "") continue;
1043
- lines.push((0, react.createElement)("div", {
1044
- key: "old-" + String(hunkIndex) + "-" + String(index),
1045
- className: "evolution-param-line-del"
1046
- }, "- " + line));
1047
- }
1048
- for (const [index, line] of hunk.newText.split("\n").entries()) {
1049
- if (hunk.newText === "" && line === "") continue;
1050
- lines.push((0, react.createElement)("div", {
1051
- key: "new-" + String(hunkIndex) + "-" + String(index),
1052
- className: "evolution-param-line-add"
1053
- }, "+ " + line));
1054
- }
1055
- }
1056
- 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));
1057
- };
1058
- const row = (entry) => {
1059
- if (entry.status === "executing") return (0, react.createElement)("div", {
1060
- className: "evolution-param-field",
1061
- key: entry.id
1062
- }, (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", {
1063
- className: "evolution-param-note",
1064
- role: "status"
1065
- }, t("approvalExecuting")), entry.claimedBy === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-hint" }, entry.claimedBy));
1066
- const busy = state.busy.includes(entry.id);
1067
- const button = (label, action, tone) => (0, react.createElement)("button", {
1068
- className: "evolution-param-button",
1069
- type: "button",
1070
- disabled: busy,
1071
- ...tone === "primary" ? { "data-primary": "true" } : { "data-tone": tone },
1072
- onClick: () => {
1073
- if (action === "approve" && confirmMode === "ask" && armed !== entry.id) {
1074
- setArmed(entry.id);
1075
- return;
1076
- }
1077
- setArmed(null);
1078
- decide(entry.id, action);
1079
- }
1080
- }, busy ? t("approvalLoading") : t(label));
1081
- return (0, react.createElement)("div", {
1082
- className: "evolution-param-field",
1083
- key: entry.id
1084
- }, (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", {
1085
- className: "evolution-param-note",
1086
- role: "status"
1087
- }, (0, react.createElement)("span", { className: "evolution-param-value" }, t("approvalConfirmNamed", { n: entry.summary })), (0, react.createElement)("button", {
1088
- className: "evolution-param-button",
1089
- type: "button",
1090
- "data-confirm": "yes",
1091
- "data-primary": "true",
1092
- onClick: () => {
1093
- setArmed(null);
1094
- decide(entry.id, "approve");
1095
- }
1096
- }, t("approvalConfirmYes")), (0, react.createElement)("button", {
1097
- className: "evolution-param-button",
1098
- type: "button",
1099
- "data-tone": "ghost",
1100
- onClick: () => {
1101
- setArmed(null);
1102
- }
1103
- }, t("approvalConfirmNo"))) : null, (0, react.createElement)("div", { className: "evolution-param-actions" }, button("approvalApprove", "approve", "primary"), button("approvalReject", "reject", "outline"), (0, react.createElement)("button", {
1104
- className: "evolution-param-button",
1105
- type: "button",
1106
- "data-tone": "ghost",
1107
- onClick: () => {
1108
- togglePreview(entry.id);
1109
- }
1110
- }, state.open === entry.id ? t("approvalPreviewClose") : t("approvalPreview"))), state.open === entry.id && state.preview !== null ? preview(state.preview) : null);
1111
- };
1112
- const body = () => {
1113
- if (state.view.kind === "loading") return (0, react.createElement)("p", {
1114
- className: "evolution-param-note",
1115
- role: "status"
1116
- }, t("approvalLoading"));
1117
- if (state.view.kind === "empty") return (0, react.createElement)("p", {
1118
- className: "evolution-param-note",
1119
- role: "status"
1120
- }, t("approvalEmpty"));
1121
- if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, FailureBlock({
1122
- failure: state.view.failure,
1123
- t: translate,
1124
- fallback: props.fallback
1125
- }), (0, react.createElement)("button", {
1126
- className: "evolution-param-button",
1127
- type: "button",
1128
- onClick: () => {
1129
- load();
1130
- }
1131
- }, t("approvalRetry")));
1132
- return (0, react.createElement)("div", null, ...state.view.rows.map(row), state.notice === null ? null : (0, react.createElement)("p", {
1133
- className: "evolution-param-error",
1134
- role: "status"
1135
- }, state.notice));
1136
- };
1137
- const rows = state.view.kind === "ready" ? state.view.rows : [];
1138
- const waiting = rows.filter((entry) => entry.status !== "executing").length;
1139
- const running = rows.length - waiting;
1140
- return (0, react.createElement)("section", { className: "evolution-param-card" }, (0, react.createElement)("button", {
1141
- className: "evolution-param-head",
1142
- type: "button",
1143
- onClick: () => {
1144
- setOpen(!open);
1145
- },
1146
- "aria-expanded": open
1147
- }, (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", {
1148
- className: "evolution-param-button",
1149
- type: "button",
1150
- onClick: () => {
1151
- load();
1152
- }
1153
- }, t("approvalRefresh"))), body()) : null);
1154
- }
1155
- //#endregion
1156
- //#region src/panels/settings/DiagnosticsCard.ts
1157
- /**
1158
- * The diagnostics card (W7 S7): the settings face of the SAME DiagnosticsView the CLI and the
1159
- * `/api/dsh-evolution/diagnostics` route answer.
1160
- *
1161
- * Read-only by construction: it fetches once on mount and only refetches on an explicit Refresh; a
1162
- * failed read renders the shared FailureBlock with the command-face fallback. The client owns no
1163
- * key list and no status vocabulary: it renders the host view's frozen runtime rows and leads with
1164
- * a plain-language summary. The view's documentation facts are a CLI/JSON concern and are
1165
- * deliberately NOT rendered here (0.27.2 real-machine feedback: a settings page is not a spec
1166
- * sheet; the runtime window is the whole reading).
1167
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/DiagnosticsCard
1168
- */
1169
- /** Which word one status gets (the frozen W1 status set). */
1170
- const STATUS_KEYS = Object.freeze({
1171
- ok: "diagStatusOk",
1172
- warn: "diagStatusWarn",
1173
- fail: "diagStatusFail",
1174
- unknown: "diagStatusUnknown"
1175
- });
1176
- /** The summary word for a badge status (the same frozen set; ok has no count). */
1177
- const SUMMARY_KEYS = Object.freeze({
1178
- ok: "diagSummaryOk",
1179
- warn: "diagSummaryWarn",
1180
- fail: "diagSummaryFail",
1181
- unknown: "diagSummaryUnknown"
1182
- });
1183
- /** The one status the card leads with: fail > warn > unknown > ok. */
1184
- function summaryOf(rows) {
1185
- const count = (status) => rows.filter((row) => row.status === status).length;
1186
- for (const status of [
1187
- "fail",
1188
- "warn",
1189
- "unknown"
1190
- ]) {
1191
- const n = count(status);
1192
- if (n > 0) return {
1193
- status,
1194
- count: n
1195
- };
1196
- }
1197
- return {
1198
- status: "ok",
1199
- count: 0
1200
- };
1201
- }
1202
- /**
1203
- * Render the card.
1204
- * @param props - the copy seat, the host call and the command fallback.
1205
- * @returns the card.
1206
- */
1207
- function DiagnosticsCard(props) {
1208
- const { t, api } = props;
1209
- const translate = (key, values) => t(key, values);
1210
- const [state, setState] = (0, react.useState)({ phase: "loading" });
1211
- const [open, setOpen] = (0, react.useState)(true);
1212
- const ticket = (0, react.useRef)(0);
1213
- const read = (keepView) => {
1214
- const current = ticket.current + 1;
1215
- ticket.current = current;
1216
- if (!keepView) setState({ phase: "loading" });
1217
- api.view().then((answer) => {
1218
- if (ticket.current !== current) return;
1219
- setState(answer.ok ? {
1220
- phase: "ready",
1221
- view: answer.data
1222
- } : {
1223
- phase: "failed",
1224
- failure: answer.failure
1225
- });
1226
- });
1227
- };
1228
- (0, react.useEffect)(() => {
1229
- read(false);
1230
- }, []);
1231
- const refresh = () => {
1232
- read(true);
1233
- };
1234
- const summary = state.phase === "ready" ? summaryOf(state.view.runtime) : void 0;
1235
- const header = (0, react.createElement)("button", {
1236
- type: "button",
1237
- className: "evolution-param-head",
1238
- "aria-expanded": open ? "true" : "false",
1239
- onClick: () => {
1240
- setOpen(!open);
1241
- }
1242
- }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("diagTitle")), summary === void 0 ? null : (0, react.createElement)("span", {
1243
- className: "evo-diag-badge",
1244
- "data-status": summary.status
1245
- }, t(SUMMARY_KEYS[summary.status], summary.count === 0 ? void 0 : { n: summary.count })), (0, react.createElement)("span", { className: "evolution-param-chevron" }, open ? "▲" : "▼"));
1246
- 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", {
1247
- key: "runtime-" + row.key,
1248
- className: "evo-diag-item",
1249
- "data-kind": "runtime",
1250
- "data-status": row.status
1251
- }, (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)))));
1252
- const body = () => {
1253
- if (state.phase === "loading") return (0, react.createElement)("p", { className: "evolution-param-loading" }, t("stateLoading"));
1254
- if (state.phase === "failed") return FailureBlock({
1255
- failure: state.failure,
1256
- t: translate,
1257
- fallback: props.fallback
1258
- });
1259
- const view = state.view;
1260
- 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", {
1261
- type: "button",
1262
- className: "evolution-param-button",
1263
- onClick: refresh,
1264
- "data-action": "diag-refresh"
1265
- }, t("diagRefresh"))), runtime(view), view.runtime.length === 0 ? (0, react.createElement)("p", { className: "evolution-param-note" }, t("diagNoRows")) : null);
1266
- };
1267
- return (0, react.createElement)("section", {
1268
- className: "evolution-param-card",
1269
- "data-card": "diagnostics"
1270
- }, header, open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
1271
- }
1272
- //#endregion
1273
- //#region src/panels/settings/diagnostics-state.ts
1274
- /**
1275
- * The diagnostics card's data face (W7 S7): ONE request to `HOST_ROUTES.diagnostics.view` through the
1276
- * GENERATED route client, and the four-state answer the card renders.
1277
- *
1278
- * The Host route is the SAME `buildDoctorView` the CLI prints (W6/W8), so this client owns no facts
1279
- * and no computation — it only renders.
1280
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client/diagnostics-state
1281
- */
1282
- /**
1283
- * Build the diagnostics face over one fetch implementation.
1284
- * @param doFetch - the browser fetch, or a spec stub.
1285
- */
1286
- function createDiagnosticsApi(doFetch = fetch) {
1287
- return { view: async () => await requestJson(doFetch, ROUTES.diagnostics.view, { headers: { accept: "application/json" } }) };
1288
- }
1289
- //#endregion
1290
- //#region ../evolution-client-kit/src/generated/fallbacks.generated.ts
1291
- /**
1292
- * The command-face fallback for every Host surface, generated from evolution-commands'
1293
- * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1294
- * panels render this hint instead of a bare HTTP failure — same use case, command face.
1122
+ * The command-face fallback for every Host surface, generated from evolution-commands'
1123
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
1124
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
1295
1125
  */
1296
1126
  const FALLBACKS = {
1297
1127
  approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
1298
- "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
1128
+ "skill-history": { command: "/evolution skill undo <name> [--to v<N>](查看:/evolution skill history <name>)" },
1299
1129
  "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
1300
1130
  merge: { command: "/evolution merge status / pending / approve <id> / report" },
1301
1131
  diagnostics: {
1302
1132
  command: "/evolution doctor",
1303
1133
  platformCli: "dsh --json /evolution doctor"
1304
- }
1134
+ },
1135
+ settings: { command: "/evolution policy set <id> <value>(查看:/evolution policy)" }
1305
1136
  };
1306
1137
  //#endregion
1307
1138
  //#region src/panels/settings/messages.ts
@@ -1321,6 +1152,7 @@ window.__ModuleLoader__.load({
1321
1152
  const zh$1 = {
1322
1153
  title: "自进化",
1323
1154
  subtitle: "待你决定的写入在最上面;下面按功能列出你能改的参数,没有列出的由安装时的配置决定。",
1155
+ aboutPurpose: "自进化设置与技能历史",
1324
1156
  overridden: "已覆盖",
1325
1157
  deployment: "默认",
1326
1158
  loading: "正在读取设置…",
@@ -1343,6 +1175,8 @@ window.__ModuleLoader__.load({
1343
1175
  approvalConfirmYes: "确认批准",
1344
1176
  approvalConfirmNo: "取消",
1345
1177
  resetTo: "复位为 {value}",
1178
+ saved: "已保存。",
1179
+ resetDone: "已恢复默认值。",
1346
1180
  refused: "这个值没有被接受(界面上保持原样)。想看原因:在对话里让它说明。",
1347
1181
  resetRefused: "恢复默认值没有生效(这一行还是你的值)。想看原因:在对话里让它说明。",
1348
1182
  cardReview: "会话回顾",
@@ -1365,10 +1199,22 @@ window.__ModuleLoader__.load({
1365
1199
  approvalCount: "{n} 项待批",
1366
1200
  approvalRunningCount: "{n} 项执行中",
1367
1201
  approvalRefresh: "刷新",
1368
- approvalExecuting: "执行中:这条的写效果可能已经落地,不要重复批准;要清掉它请用拒绝。",
1202
+ approvalApproving: "批准中…",
1203
+ approvalRejecting: "拒绝中…",
1204
+ approvalExecuting: "执行中:写效果可能已经落地,不要重复批准。先核验效果,再用「放回待批」释放占用;释放不会停止或撤销在途写入。",
1205
+ approvalRelease: "放回待批",
1206
+ approvalReleasing: "释放中…",
1369
1207
  approvalKindMemory: "记忆",
1370
1208
  approvalKindSkill: "技能",
1371
1209
  approvalKindCapability: "能力",
1210
+ approvalVerbAdd: "新增",
1211
+ approvalVerbCreate: "创建",
1212
+ approvalVerbUpdate: "更新",
1213
+ approvalVerbRemove: "移除",
1214
+ approvalVerbDelete: "删除",
1215
+ approvalVerbArchive: "归档",
1216
+ approvalVerbRestore: "恢复",
1217
+ approvalVerbMerge: "合并",
1372
1218
  approvalAgeNow: "刚刚",
1373
1219
  approvalAgeMinutes: "{n} 分钟前",
1374
1220
  approvalAgeHours: "{n} 小时前",
@@ -1378,6 +1224,7 @@ window.__ModuleLoader__.load({
1378
1224
  approvalPreviewClose: "收起",
1379
1225
  approvalPreviewReading: "正在读取预览…",
1380
1226
  approvalPreviewNone: "看不了预览:",
1227
+ approvalPreviewPath: "文件:{path}",
1381
1228
  approvalDiffAdded: "{n} 行新增",
1382
1229
  approvalDiffRemoved: "{n} 行删除",
1383
1230
  approvalDiffCut: "改动区域已截断",
@@ -1407,6 +1254,7 @@ window.__ModuleLoader__.load({
1407
1254
  const en$1 = {
1408
1255
  title: "Self-evolution",
1409
1256
  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.",
1257
+ aboutPurpose: "Self-evolution settings and skill history",
1410
1258
  overridden: "Overridden",
1411
1259
  deployment: "default",
1412
1260
  loading: "reading settings…",
@@ -1429,6 +1277,8 @@ window.__ModuleLoader__.load({
1429
1277
  approvalConfirmYes: "Approve",
1430
1278
  approvalConfirmNo: "Cancel",
1431
1279
  resetTo: "Restores {value}",
1280
+ saved: "Saved.",
1281
+ resetDone: "Default restored.",
1432
1282
  refused: "The value was not accepted (the page keeps what you see). For the reason, ask in the conversation.",
1433
1283
  resetRefused: "The reset did not take (this row still holds your value). For the reason, ask in the conversation.",
1434
1284
  cardReview: "Session review",
@@ -1451,10 +1301,22 @@ window.__ModuleLoader__.load({
1451
1301
  approvalCount: "{n} waiting",
1452
1302
  approvalRunningCount: "{n} running",
1453
1303
  approvalRefresh: "Refresh",
1454
- approvalExecuting: "Running: this write may already have landed — do not approve it twice; reject it to clear it.",
1304
+ approvalApproving: "approving…",
1305
+ approvalRejecting: "rejecting…",
1306
+ approvalExecuting: "Running: the effect may already have landed; do not approve it twice. Verify it, then use Release to put the claim back; release does not stop or undo the in-flight write.",
1307
+ approvalRelease: "Release",
1308
+ approvalReleasing: "releasing…",
1455
1309
  approvalKindMemory: "memory",
1456
1310
  approvalKindSkill: "skill",
1457
1311
  approvalKindCapability: "capability",
1312
+ approvalVerbAdd: "add",
1313
+ approvalVerbCreate: "create",
1314
+ approvalVerbUpdate: "update",
1315
+ approvalVerbRemove: "remove",
1316
+ approvalVerbDelete: "delete",
1317
+ approvalVerbArchive: "archive",
1318
+ approvalVerbRestore: "restore",
1319
+ approvalVerbMerge: "merge",
1458
1320
  approvalAgeNow: "just now",
1459
1321
  approvalAgeMinutes: "{n} min ago",
1460
1322
  approvalAgeHours: "{n} h ago",
@@ -1464,6 +1326,7 @@ window.__ModuleLoader__.load({
1464
1326
  approvalPreviewClose: "Hide",
1465
1327
  approvalPreviewReading: "reading the preview…",
1466
1328
  approvalPreviewNone: "no preview: ",
1329
+ approvalPreviewPath: "File: {path}",
1467
1330
  approvalDiffAdded: "{n} lines added",
1468
1331
  approvalDiffRemoved: "{n} lines removed",
1469
1332
  approvalDiffCut: "the changed region is truncated",
@@ -1507,10 +1370,10 @@ window.__ModuleLoader__.load({
1507
1370
  /**
1508
1371
  * Resolve one message without the locale seat (tests and the fallback path).
1509
1372
  * @param key - the message key.
1510
- * @param language - which dictionary to read; defaults to English (the platform FALLBACK_LOCALE).
1373
+ * @param language - which dictionary to read; defaults to zh, matching the platform FALLBACK_LOCALE.
1511
1374
  * @returns the copy this bundle carries.
1512
1375
  */
1513
- function message(key, language = "en") {
1376
+ function message(key, language = "zh") {
1514
1377
  return language === "zh" ? zh$1[key] : en$1[key];
1515
1378
  }
1516
1379
  /**
@@ -1522,7 +1385,7 @@ window.__ModuleLoader__.load({
1522
1385
  * @param values - substitutions keyed by the slot name inside the braces.
1523
1386
  * @returns the filled copy.
1524
1387
  */
1525
- function fill$1(template, values) {
1388
+ function fill(template, values) {
1526
1389
  return template.replace(/\{(\w+)\}/g, (match, slot) => {
1527
1390
  const value = values[slot];
1528
1391
  return value === void 0 ? match : String(value);
@@ -1555,6 +1418,18 @@ window.__ModuleLoader__.load({
1555
1418
  * typed from the registry (\`control\`), so booleans are switches, enums are
1556
1419
  * selects and numbers are numeric inputs with the unit shown beside the label.
1557
1420
  */
1421
+ /** Resolve one field's user-language label. */
1422
+ function labelOf(field, lang) {
1423
+ return lang === "en" ? field.labelEn ?? field.label : field.label;
1424
+ }
1425
+ /** Resolve one field's user-language hint. */
1426
+ function hintOf(field, lang) {
1427
+ return lang === "en" ? field.hintEn ?? field.doc : field.hint;
1428
+ }
1429
+ /** Resolve one field's user-language group heading. */
1430
+ function groupOf(field, lang) {
1431
+ return lang === "en" ? field.groupLabelEn ?? field.group : field.groupLabel ?? field.group;
1432
+ }
1558
1433
  /**
1559
1434
  * The NAMESPACE-RELATIVE key of one registry field.
1560
1435
  *
@@ -1644,7 +1519,7 @@ window.__ModuleLoader__.load({
1644
1519
  }
1645
1520
  /** One field's control, typed from the registry. */
1646
1521
  function FieldControl(props) {
1647
- const { field, text } = props;
1522
+ const { field, text, lang } = props;
1648
1523
  const id = controlId(field);
1649
1524
  const common = {
1650
1525
  id,
@@ -1677,7 +1552,7 @@ window.__ModuleLoader__.load({
1677
1552
  }, text), ...field.values.map((value, index) => (0, react.createElement)("option", {
1678
1553
  key: value,
1679
1554
  value
1680
- }, field.valueLabels[index] ?? value))]);
1555
+ }, (lang === "en" ? field.valueLabelsEn ?? field.valueLabels : field.valueLabels)[index] ?? value))]);
1681
1556
  return (0, react.createElement)("input", {
1682
1557
  ...common,
1683
1558
  type: field.control === "number" ? "number" : "text",
@@ -1689,16 +1564,17 @@ window.__ModuleLoader__.load({
1689
1564
  }
1690
1565
  /** One parameter field: label, control, hint, and its reset-to-deployment action. */
1691
1566
  function FieldBlock(props) {
1692
- const { field, t } = props;
1693
- const unit = field.unit === "" ? null : (0, react.createElement)("span", { className: "evolution-param-unit" }, "(" + field.unit + ")");
1694
- 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", {
1567
+ const { field, t, lang } = props;
1568
+ const unitText = lang === "en" ? field.unitEn ?? field.unit : field.unit;
1569
+ const unit = unitText === "" ? null : (0, react.createElement)("span", { className: "evolution-param-unit" }, (lang === "en" ? " (" : "(") + unitText + (lang === "en" ? ")" : ")"));
1570
+ return (0, react.createElement)("div", { className: "evolution-param-field" }, (0, react.createElement)("div", { className: "evolution-param-label" }, (0, react.createElement)("label", { htmlFor: controlId(field) }, labelOf(field, lang) === "" ? field.id : labelOf(field, lang)), unit, props.overridden ? (0, react.createElement)("span", {
1695
1571
  className: "evolution-param-source",
1696
1572
  "data-user": "true"
1697
- }, 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", {
1573
+ }, t("overridden")) : null), FieldControl(props), (0, react.createElement)("p", { className: "evolution-param-hint" }, hintOf(field, lang) === "" ? field.doc : hintOf(field, lang)), props.invalid === null ? null : (0, react.createElement)("p", {
1698
1574
  id: controlId(field) + "-invalid",
1699
1575
  className: "evolution-param-error",
1700
1576
  role: "status"
1701
- }, t(props.invalid)), props.overridden ? (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
1577
+ }, props.invalid), props.overridden ? (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
1702
1578
  type: "button",
1703
1579
  className: "evolution-param-button",
1704
1580
  disabled: props.disabled || props.clearing,
@@ -1714,10 +1590,12 @@ window.__ModuleLoader__.load({
1714
1590
  */
1715
1591
  function ParamCardView(props) {
1716
1592
  const { t, fields, mutateOnce, clear, namespace, snapshot } = props;
1593
+ const lang = props.lang?.() ?? "zh";
1717
1594
  const [open, setOpen] = (0, react.useState)(false);
1718
1595
  const [draft, setDraft] = (0, react.useState)({});
1719
1596
  const [busy, setBusy] = (0, react.useState)(false);
1720
1597
  const [error, setError] = (0, react.useState)("");
1598
+ const [note, setNote] = (0, react.useState)("");
1721
1599
  const [pending, setPending] = (0, react.useState)(null);
1722
1600
  const [clearing, setClearing] = (0, react.useState)(null);
1723
1601
  /**
@@ -1742,7 +1620,19 @@ window.__ModuleLoader__.load({
1742
1620
  setError(t(pending.resets ? "resetRefused" : "refused"));
1743
1621
  return;
1744
1622
  }
1745
- if (!pending.resets) setDraft({});
1623
+ if (pending.accepted) if (pending.resets) {
1624
+ const id = pending.field;
1625
+ setDraft((current) => {
1626
+ if (id === void 0) return current;
1627
+ const next = { ...current };
1628
+ delete next[id];
1629
+ return next;
1630
+ });
1631
+ setNote(t("resetDone"));
1632
+ } else {
1633
+ setDraft({});
1634
+ setNote(t("saved"));
1635
+ }
1746
1636
  }, [
1747
1637
  pending,
1748
1638
  snapshot.user,
@@ -1772,7 +1662,12 @@ window.__ModuleLoader__.load({
1772
1662
  const related = fields.find((candidate) => candidate.id === field.atLeast);
1773
1663
  return related === void 0 ? void 0 : textOf(related);
1774
1664
  };
1775
- const blocked = fields.some((field) => invalidReason(field, textOf(field), peerTextOf(field)) !== null);
1665
+ const invalidText = (field) => {
1666
+ const reason = invalidReason(field, textOf(field), peerTextOf(field));
1667
+ if (reason === null) return null;
1668
+ return reason === "invalidAtLeast" ? t(reason, { n: peerTextOf(field) ?? "" }) : t(reason);
1669
+ };
1670
+ const blocked = fields.some((field) => invalidText(field) !== null);
1776
1671
  const change = (id, next) => {
1777
1672
  setDraft({
1778
1673
  ...draft,
@@ -1790,6 +1685,7 @@ window.__ModuleLoader__.load({
1790
1685
  const reset = (field) => {
1791
1686
  setClearing(field);
1792
1687
  setError("");
1688
+ setNote("");
1793
1689
  (async () => {
1794
1690
  let accepted;
1795
1691
  try {
@@ -1801,13 +1697,15 @@ window.__ModuleLoader__.load({
1801
1697
  }
1802
1698
  setPending({
1803
1699
  accepted,
1804
- resets: true
1700
+ resets: true,
1701
+ field
1805
1702
  });
1806
1703
  })();
1807
1704
  };
1808
1705
  const save = () => {
1809
1706
  setBusy(true);
1810
1707
  setError("");
1708
+ setNote("");
1811
1709
  (async () => {
1812
1710
  try {
1813
1711
  setPending({
@@ -1831,7 +1729,7 @@ window.__ModuleLoader__.load({
1831
1729
  onClick: () => {
1832
1730
  setOpen(!open);
1833
1731
  }
1834
- }, (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", {
1732
+ }, (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-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", {
1835
1733
  className: "evolution-param-note",
1836
1734
  role: "status"
1837
1735
  }, t("readonly")) : null, fields.length === 0 ? (0, react.createElement)("p", {
@@ -1840,25 +1738,29 @@ window.__ModuleLoader__.load({
1840
1738
  }, t("empty")) : fields.flatMap((field, index) => [index === 0 || fields[index - 1]?.group !== field.group ? (0, react.createElement)("h3", {
1841
1739
  key: "group-" + field.group,
1842
1740
  className: "evolution-param-group"
1843
- }, field.group) : null, (0, react.createElement)(FieldBlock, {
1741
+ }, groupOf(field, lang)) : null, (0, react.createElement)(FieldBlock, {
1844
1742
  key: field.id,
1845
1743
  field,
1744
+ lang,
1846
1745
  text: textOf(field),
1847
1746
  overridden: Object.hasOwn(user, keyOf(field)),
1848
1747
  disabled: locked,
1849
1748
  clearing: clearing === field.id,
1850
1749
  resetTo: resetToOf(snapshot.base?.[keyOf(field)]),
1851
- invalid: invalidReason(field, textOf(field), peerTextOf(field)),
1750
+ invalid: invalidText(field),
1852
1751
  t,
1853
1752
  onChange: change,
1854
1753
  onClear: reset
1855
1754
  })]), error === "" ? null : (0, react.createElement)("p", {
1856
1755
  className: "evolution-param-error",
1857
1756
  role: "status"
1858
- }, error), fields.length === 0 ? null : (0, react.createElement)("div", { className: "evolution-param-footer" }, (0, react.createElement)("button", {
1757
+ }, error), note === "" ? null : (0, react.createElement)("p", {
1758
+ className: "evolution-param-note",
1759
+ role: "status"
1760
+ }, note), fields.length === 0 ? null : (0, react.createElement)("div", { className: "evolution-param-footer" }, (0, react.createElement)("button", {
1859
1761
  type: "button",
1860
1762
  className: "evolution-param-button",
1861
- disabled: busy || dirty.length === 0 || blocked,
1763
+ disabled: busy || dirty.length === 0,
1862
1764
  onClick: () => {
1863
1765
  setDraft({});
1864
1766
  setError("");
@@ -1872,21 +1774,235 @@ window.__ModuleLoader__.load({
1872
1774
  }, busy ? t("saving") : t("apply")))) : null);
1873
1775
  }
1874
1776
  /**
1875
- * The card as our own section renders it: the row state comes from the hook seat the
1876
- * renderer binds out of the inject face's `hooks` compartment (components never hold a
1877
- * subscription of their own).
1878
- * @param props - the card data, its callbacks, and the bound row hook.
1879
- * @returns the card.
1777
+ * The card as our own section renders it: the row state comes from the hook seat the
1778
+ * renderer binds out of the inject face's `hooks` compartment (components never hold a
1779
+ * subscription of their own).
1780
+ * @param props - the card data, its callbacks, and the bound row hook.
1781
+ * @returns the card.
1782
+ */
1783
+ /**
1784
+ * Platform card-face wrapper: `settings.plugin.item` injects the face but does NOT bind
1785
+ * the `use<Name>` hook compartment, so this component owns the one stable external-store
1786
+ * subscription the section card gets from its renderer.
1787
+ * @param props - the injected card face.
1788
+ * @returns the card with its live row snapshot.
1789
+ */
1790
+ const pluginSources = /* @__PURE__ */ new Map();
1791
+ /**
1792
+ * Register a plugin-surface form source by namespace. The platform's
1793
+ * `settings.plugin.item` slot does not bind the card's `hooks` compartment, so
1794
+ * the owning settings apply hands the stable source here instead.
1795
+ * @param namespace - settings namespace.
1796
+ * @param source - the stable external-store source.
1797
+ */
1798
+ function registerPluginParamSource(namespace, source) {
1799
+ pluginSources.set(namespace, source);
1800
+ }
1801
+ const MISSING_SNAPSHOT = {
1802
+ status: "unavailable",
1803
+ value: void 0,
1804
+ user: void 0,
1805
+ writable: false,
1806
+ base: void 0,
1807
+ reason: "seat-missing"
1808
+ };
1809
+ const MISSING_SOURCE = {
1810
+ getSnapshot: () => MISSING_SNAPSHOT,
1811
+ subscribe: () => () => {},
1812
+ revive: () => {}
1813
+ };
1814
+ function PluginParamCard(props) {
1815
+ const source = props.hooks?.paramSection ?? pluginSources.get(props.namespace) ?? MISSING_SOURCE;
1816
+ const snapshot = (0, react.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
1817
+ return ParamCardView({
1818
+ namespace: props.namespace,
1819
+ fields: props.fields,
1820
+ t: props.t,
1821
+ ...props.lang === void 0 ? {} : { lang: props.lang },
1822
+ mutateOnce: props.mutateOnce,
1823
+ clear: props.clear,
1824
+ snapshot
1825
+ });
1826
+ }
1827
+ //#endregion
1828
+ //#region src/panels/settings/seam.ts
1829
+ /** Our card slot, hosted by the section this bundle registers. */
1830
+ const CARD_SLOT = "evolution.namespace.card";
1831
+ /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
1832
+ const DIAGNOSTICS_CARD_KEY = "diagnostics";
1833
+ //#endregion
1834
+ //#region ../evolution-client-kit/src/settings-seat.ts
1835
+ const SETTINGS_DESCRIBE = ROUTES.settings.describe;
1836
+ const SETTINGS_MUTATE = ROUTES.settings.mutate;
1837
+ const EMPTY = Object.freeze({
1838
+ status: "unavailable",
1839
+ value: void 0,
1840
+ user: void 0,
1841
+ writable: false,
1842
+ base: void 0,
1843
+ revision: void 0,
1844
+ reason: "transport"
1845
+ });
1846
+ function snapshotFrom(value, ns) {
1847
+ if (typeof value !== "object" || value === null) return EMPTY;
1848
+ const list = value.namespaces;
1849
+ if (!Array.isArray(list)) return EMPTY;
1850
+ const item = list.find((candidate) => typeof candidate === "object" && candidate !== null && candidate.ns === ns);
1851
+ return item === void 0 ? {
1852
+ ...EMPTY,
1853
+ reason: "not-served"
1854
+ } : item;
1855
+ }
1856
+ async function describe(namespace) {
1857
+ try {
1858
+ const response = await fetch(SETTINGS_DESCRIBE, { headers: { accept: "application/json" } });
1859
+ if (!response.ok) return EMPTY;
1860
+ const body = await response.json();
1861
+ if (body.ok !== true) return EMPTY;
1862
+ return snapshotFrom(body.data, namespace);
1863
+ } catch {
1864
+ return EMPTY;
1865
+ }
1866
+ }
1867
+ async function write(namespace, ops, expectedRevision) {
1868
+ try {
1869
+ const response = await fetch(SETTINGS_MUTATE, {
1870
+ method: "POST",
1871
+ headers: {
1872
+ "content-type": "application/json",
1873
+ accept: "application/json"
1874
+ },
1875
+ body: JSON.stringify({
1876
+ ns: namespace,
1877
+ ops,
1878
+ ...expectedRevision === void 0 ? {} : { expectedRevision }
1879
+ })
1880
+ });
1881
+ if (!response.ok) return { ok: false };
1882
+ const body = await response.json();
1883
+ return {
1884
+ ok: body.ok === true,
1885
+ snapshot: snapshotFrom(body.data, namespace)
1886
+ };
1887
+ } catch {
1888
+ return { ok: false };
1889
+ }
1890
+ }
1891
+ function createForm(namespace, ctx) {
1892
+ let snapshot = {
1893
+ ...EMPTY,
1894
+ reason: "seat-missing"
1895
+ };
1896
+ const listeners = /* @__PURE__ */ new Set();
1897
+ let started = false;
1898
+ let listening = false;
1899
+ const notify = () => {
1900
+ for (const listener of listeners) listener();
1901
+ };
1902
+ const refresh = async () => {
1903
+ snapshot = await describe(namespace);
1904
+ notify();
1905
+ };
1906
+ const start = () => {
1907
+ if (started) return;
1908
+ started = true;
1909
+ if (!listening) {
1910
+ listening = true;
1911
+ (ctx.get?.("remote"))?.$on?.("settings/document-updated", (changed) => {
1912
+ if (changed === namespace) refresh();
1913
+ });
1914
+ }
1915
+ refresh();
1916
+ };
1917
+ const form = {
1918
+ getSnapshot: () => snapshot,
1919
+ subscribe: (listener) => {
1920
+ listeners.add(listener);
1921
+ start();
1922
+ return () => {
1923
+ listeners.delete(listener);
1924
+ };
1925
+ },
1926
+ set: async (field, value) => await form.mutate([{
1927
+ op: "set",
1928
+ path: [field],
1929
+ value
1930
+ }]),
1931
+ unset: async (field) => await form.mutate([{
1932
+ op: "unset",
1933
+ path: [field]
1934
+ }]),
1935
+ mutate: async (ops, expectedRevision) => {
1936
+ const outcome = await write(namespace, ops, expectedRevision ?? snapshot.revision);
1937
+ if (outcome.snapshot !== void 0) {
1938
+ snapshot = outcome.snapshot;
1939
+ notify();
1940
+ } else await refresh();
1941
+ return outcome.ok;
1942
+ }
1943
+ };
1944
+ return form;
1945
+ }
1946
+ /**
1947
+ * Build one cached form per family namespace over the family settings routes.
1948
+ * @param ctx - the client plugin context (remote subscription is optional).
1949
+ * @returns the seat; each namespace is served by /api/dsh-evolution/settings/*.
1950
+ */
1951
+ const seats = /* @__PURE__ */ new WeakMap();
1952
+ function createSettingsSeat(ctx) {
1953
+ const cached = seats.get(ctx);
1954
+ if (cached !== void 0) return cached;
1955
+ const forms = /* @__PURE__ */ new Map();
1956
+ const seat = { get(namespace) {
1957
+ let form = forms.get(namespace);
1958
+ if (form === void 0) {
1959
+ form = createForm(namespace, ctx);
1960
+ forms.set(namespace, form);
1961
+ }
1962
+ return form;
1963
+ } };
1964
+ seats.set(ctx, seat);
1965
+ return seat;
1966
+ }
1967
+ //#endregion
1968
+ //#region ../evolution-client-kit/src/action.ts
1969
+ /**
1970
+ * Command-only actions. Approval decisions are a separate host API and must never
1971
+ * be represented as a copyable command here.
1972
+ * @module @lmzhen/dsh-evolution-client-kit
1973
+ */
1974
+ //#endregion
1975
+ //#region ../evolution-client-kit/src/tokens.ts
1976
+ /**
1977
+ * The scale, declared on one element so it never leaks into the host shell.
1978
+ * @param scope - the selector the custom properties are declared on.
1979
+ * @returns the declaration block to put in the stylesheet.
1980
+ */
1981
+ function tokenVars(scope) {
1982
+ 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}";
1983
+ }
1984
+ //#endregion
1985
+ //#region src/panels/settings/SettingsSection.ts
1986
+ /**
1987
+ * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
1988
+ * window's card ahead of them all, because it is the only card here that waits on a decision.
1989
+ *
1990
+ * This is the shape the platform's own settings sections use — a heading plus the
1991
+ * child slots it declared in its registration. The cards themselves are separate
1992
+ * registrations keyed by namespace, so a namespace the Host does not serve simply
1993
+ * renders nothing and a deployment that drops this row loses the whole section.
1994
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client
1880
1995
  */
1881
- function ParamCard(props) {
1882
- return ParamCardView({
1883
- namespace: props.namespace,
1884
- fields: props.fields,
1885
- t: props.t,
1886
- mutateOnce: props.mutateOnce,
1887
- clear: props.clear,
1888
- snapshot: props.useParamSection((state) => state)
1889
- });
1996
+ /**
1997
+ * Render the section.
1998
+ * @param face - the shell's inject face: translator and child-slot renderer.
1999
+ * @returns the section element.
2000
+ */
2001
+ function SettingsSection(face) {
2002
+ 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.6 · " + face.t("aboutPurpose")), (0, react.createElement)("div", {
2003
+ key: DIAGNOSTICS_CARD_KEY,
2004
+ className: "evolution-params-group"
2005
+ }, face.renderSlot(CARD_SLOT, {}, { entryKey: DIAGNOSTICS_CARD_KEY })));
1890
2006
  }
1891
2007
  //#endregion
1892
2008
  //#region src/panels/settings/source.ts
@@ -1922,7 +2038,7 @@ window.__ModuleLoader__.load({
1922
2038
  user: snapshot.user,
1923
2039
  writable: snapshot.writable,
1924
2040
  base: isRecord(snapshot.base) ? snapshot.base : void 0,
1925
- ...snapshot.status === "unavailable" ? { reason: "not-served" } : {}
2041
+ ...snapshot.status === "unavailable" ? { reason: snapshot.reason ?? "not-served" } : {}
1926
2042
  };
1927
2043
  }
1928
2044
  /**
@@ -1981,7 +2097,7 @@ window.__ModuleLoader__.load({
1981
2097
  const seat = probe();
1982
2098
  if (seat === void 0) return;
1983
2099
  const next = seat.get(namespace);
1984
- if (next === form) return;
2100
+ if (next === void 0 || next === form) return;
1985
2101
  offForm?.();
1986
2102
  offForm = void 0;
1987
2103
  form = next;
@@ -2066,122 +2182,6 @@ window.__ModuleLoader__.load({
2066
2182
  };
2067
2183
  }
2068
2184
  //#endregion
2069
- //#region src/panels/settings/RowConfigCard.ts
2070
- /**
2071
- * The Plugins page's per-row configuration entry for a family row.
2072
- *
2073
- * The page owns the settings subscription — it re-renders the entry — and passes the row's
2074
- * form in, so this component renders the SAME card body as our own section, fed by the
2075
- * page's state instead of the bound hook and therefore holding no subscription of its own.
2076
- * `summary` answers the page's one-line slot: a row's description fallback, which stays
2077
- * empty until the row actually overrides something, exactly as the page expects.
2078
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client
2079
- */
2080
- /**
2081
- * Count the keys this row's user layer holds.
2082
- * @param user - the raw user section, as the page reports it.
2083
- * @param fields - the row's fields.
2084
- * @returns how many of them the user overrode.
2085
- */
2086
- function overridden(user, fields, namespace) {
2087
- const layer = isRecord(user) ? user : {};
2088
- return fields.filter((field) => Object.hasOwn(layer, fieldKeyOf(namespace, field))).length;
2089
- }
2090
- /**
2091
- * Render one row's configuration entry.
2092
- * @param props - the page's view/form seat plus this row's face.
2093
- * @returns the entry; `summary` renders nothing for an untouched row (the page then keeps the row's own description).
2094
- */
2095
- function RowConfigCard(props) {
2096
- const { t, fields, namespace, view, form } = props;
2097
- if (form === void 0) return view === "summary" ? null : (0, react.createElement)("p", {
2098
- className: "evolution-param-note",
2099
- role: "status"
2100
- }, t("seatMissing"));
2101
- const changed = overridden(form.state.user, fields, namespace);
2102
- if (view === "summary") return changed === 0 ? null : (0, react.createElement)("span", null, t("changed", { n: changed }));
2103
- return ParamCardView({
2104
- namespace,
2105
- fields,
2106
- t,
2107
- snapshot: sectionSnapshotOf(form.state),
2108
- mutateOnce: async (ops) => form.mutate(ops, form.state.revision),
2109
- clear: async (field) => form.mutate([{
2110
- op: "unset",
2111
- path: [field]
2112
- }], form.state.revision)
2113
- });
2114
- }
2115
- //#endregion
2116
- //#region src/panels/settings/seam.ts
2117
- /** Our card slot, hosted by the section this bundle registers. */
2118
- const CARD_SLOT = "evolution.namespace.card";
2119
- /** The Plugins page's per-row configuration seat (declared by `ui-plugin-manager`). */
2120
- const ROW_CONFIG_SLOT = "plugins.row.config";
2121
- /**
2122
- * The key the Plugins page files a row's configuration under.
2123
- *
2124
- * Spelled here because the page's own helper cannot be imported (a value import from
2125
- * another plugin's client half is forbidden):
2126
- * `ui-plugin-manager`'s config-ledger module is the definition this mirrors.
2127
- * @param bundle - the bundle package name that declares the row.
2128
- * @param rowId - the row id that bundle's patch declares.
2129
- * @returns the `plugins.row.config` key.
2130
- */
2131
- function rowConfigKey(bundle, rowId) {
2132
- return bundle + "#" + rowId;
2133
- }
2134
- /**
2135
- * The pending-window card's slot key.
2136
- *
2137
- * The section pairs cards by key and the parameter cards use their settings namespace; this card has no
2138
- * namespace (its data comes from the host's approval routes), so it carries a key of its own. The constant
2139
- * lives HERE and not in `index.ts` because the SECTION has to name it too, and `index.ts` already imports
2140
- * the section — one definition in the module both sides already share is what keeps that pair acyclic.
2141
- */
2142
- const PENDING_CARD_KEY = "approval-pending";
2143
- /** The diagnostics card's slot key: read-only, no settings namespace (its data comes from HOST_ROUTES.diagnostics). */
2144
- const DIAGNOSTICS_CARD_KEY = "diagnostics";
2145
- //#endregion
2146
- //#region src/panels/settings/SettingsSection.ts
2147
- /**
2148
- * The section shell: title, one-line explanation, then one card per namespace — with the staged-write
2149
- * window's card ahead of them all, because it is the only card here that waits on a decision.
2150
- *
2151
- * This is the shape the platform's own settings sections use — a heading plus the
2152
- * child slots it declared in its registration. The cards themselves are separate
2153
- * registrations keyed by namespace, so a namespace the Host does not serve simply
2154
- * renders nothing and a deployment that drops this row loses the whole section.
2155
- * @module @lmzhen/dsh-evolution-ui/panels/settings/client
2156
- */
2157
- /**
2158
- * Render the section.
2159
- * @param face - the shell's inject face: translator and child-slot renderer.
2160
- * @returns the section element.
2161
- */
2162
- function SettingsSection(face) {
2163
- 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.4 · Web UI umbrella for the evolution family: settings and skill-history panels"), (0, react.createElement)("div", {
2164
- key: PENDING_CARD_KEY,
2165
- className: "evolution-params-group"
2166
- }, face.renderSlot(CARD_SLOT, {}, { entryKey: PENDING_CARD_KEY })), (0, react.createElement)("div", {
2167
- key: DIAGNOSTICS_CARD_KEY,
2168
- className: "evolution-params-group"
2169
- }, face.renderSlot(CARD_SLOT, {}, { entryKey: DIAGNOSTICS_CARD_KEY })), ...CLIENT_PARAM_SECTIONS.map((section_) => (0, react.createElement)("div", {
2170
- key: section_.namespace,
2171
- className: "evolution-params-group"
2172
- }, face.renderSlot(CARD_SLOT, {}, { entryKey: section_.namespace }))));
2173
- }
2174
- //#endregion
2175
- //#region ../evolution-client-kit/src/tokens.ts
2176
- /**
2177
- * The scale, declared on one element so it never leaks into the host shell.
2178
- * @param scope - the selector the custom properties are declared on.
2179
- * @returns the declaration block to put in the stylesheet.
2180
- */
2181
- function tokenVars(scope) {
2182
- 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}";
2183
- }
2184
- //#endregion
2185
2185
  //#region src/panels/settings/styles.ts
2186
2186
  /**
2187
2187
  * The section's stylesheet.
@@ -2258,18 +2258,18 @@ window.__ModuleLoader__.load({
2258
2258
  ".evo-diag-badge[data-status=\"ok\"]{color:var(--dsw-alias-state-success-primary)}",
2259
2259
  ".evo-diag-badge[data-status=\"warn\"]{color:var(--dsw-alias-state-warn-primary)}",
2260
2260
  ".evo-diag-badge[data-status=\"fail\"]{color:var(--dsw-alias-state-error-primary)}",
2261
- ".evo-diag-badge[data-status=\"unknown\"]{color:var(--dsw-alias-state-idle-primary)}",
2261
+ ".evo-diag-badge[data-status=\"unknown\"]{color:var(--dsw-alias-label-tertiary)}",
2262
2262
  ".evo-diag-list{display:flex;flex-direction:column}",
2263
2263
  ".evo-diag-item{padding:var(--evo-space-12) 0}",
2264
2264
  ".evo-diag-item+.evo-diag-item{border-top:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
2265
2265
  ".evo-diag-label{display:block;color:var(--dsw-alias-label-tertiary);font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2266
2266
  ".evo-diag-row-head{display:flex;align-items:baseline;gap:var(--evo-space-6);flex-wrap:wrap}",
2267
- ".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}",
2268
- ".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)}",
2267
+ ".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-label-tertiary));flex:none}",
2268
+ ".evo-diag-status{color:var(--evo-diag-tone,var(--dsw-alias-label-tertiary));font-size:var(--evo-type-xs);line-height:var(--evo-leading-base)}",
2269
2269
  ".evo-diag-item[data-status=\"ok\"]{--evo-diag-tone:var(--dsw-alias-state-success-primary)}",
2270
2270
  ".evo-diag-item[data-status=\"warn\"]{--evo-diag-tone:var(--dsw-alias-state-warn-primary)}",
2271
2271
  ".evo-diag-item[data-status=\"fail\"]{--evo-diag-tone:var(--dsw-alias-state-error-primary)}",
2272
- ".evo-diag-item[data-status=\"unknown\"]{--evo-diag-tone:var(--dsw-alias-state-idle-primary)}",
2272
+ ".evo-diag-item[data-status=\"unknown\"]{--evo-diag-tone:var(--dsw-alias-label-tertiary)}",
2273
2273
  ".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}",
2274
2274
  ".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)}"
2275
2275
  ].join("\n");
@@ -2331,15 +2331,21 @@ window.__ModuleLoader__.load({
2331
2331
  }
2332
2332
  return bound(key);
2333
2333
  };
2334
- const t = (key, values) => values === void 0 ? translate(key) : fill$1(translate(key), values);
2335
- const probe = () => ctx.get("configForms");
2334
+ const t = (key, values) => values === void 0 ? translate(key) : fill(translate(key), values);
2335
+ const language = () => {
2336
+ try {
2337
+ return ctx.locale?.getLocale?.().active === "en" ? "en" : "zh";
2338
+ } catch {
2339
+ return "zh";
2340
+ }
2341
+ };
2342
+ const seat = createSettingsSeat(ctx);
2343
+ const probe = () => seat;
2336
2344
  const sources = createSourceCache(probe);
2337
2345
  ctx.effect(() => () => {
2338
2346
  sources.dispose();
2339
2347
  }, "evolution-settings: sources");
2340
- const approvalSource = sources.sourceFor("evolution-approval");
2341
- const confirmMode = () => confirmModeFrom(approvalSource.getSnapshot().user);
2342
- ctx.inject?.(["configForms"], () => {
2348
+ ctx.inject?.(["remote"], () => {
2343
2349
  sources.revive();
2344
2350
  });
2345
2351
  seam.slots.inject("settings.section", function* () {
@@ -2355,179 +2361,543 @@ window.__ModuleLoader__.load({
2355
2361
  } }
2356
2362
  }, SettingsSection);
2357
2363
  });
2358
- seam.slots.inject(CARD_SLOT, function* () {
2359
- for (const section of CLIENT_PARAM_SECTIONS) {
2360
- const source = sources.sourceFor(section.seatKey);
2361
- const { mutateOnce, clear } = writeFaceFor(probe, section.namespace);
2362
- const face = () => ({
2363
- namespace: section.namespace,
2364
- fields: section.fields,
2365
- t,
2366
- mutateOnce,
2367
- clear,
2368
- hooks: { paramSection: source }
2369
- });
2364
+ for (const [index, section] of CLIENT_PARAM_SECTIONS.entries()) {
2365
+ const source = sources.sourceFor(section.seatKey);
2366
+ registerPluginParamSource(section.namespace, source);
2367
+ const { mutateOnce, clear } = writeFaceFor(probe, section.namespace);
2368
+ const face = () => ({
2369
+ namespace: section.namespace,
2370
+ fields: section.fields,
2371
+ t,
2372
+ lang: language,
2373
+ mutateOnce,
2374
+ clear,
2375
+ hooks: { paramSection: source }
2376
+ });
2377
+ seam.slots.inject("settings.plugin.item", function* () {
2370
2378
  yield seam.slots.register({
2371
- name: CARD_SLOT,
2372
- key: section.namespace,
2379
+ name: "settings.plugin.item",
2380
+ id: section.namespace,
2381
+ order: index * 10,
2382
+ locale: NS$1,
2373
2383
  inject: face
2374
- }, ParamCard);
2384
+ }, PluginParamCard);
2385
+ });
2386
+ }
2387
+ seam.slots.inject(CARD_SLOT, function* () {
2388
+ const diagApi = createDiagnosticsApi((input, init) => fetch(input, init));
2389
+ yield seam.slots.register({
2390
+ name: CARD_SLOT,
2391
+ key: DIAGNOSTICS_CARD_KEY,
2392
+ inject: () => ({
2393
+ t,
2394
+ api: diagApi,
2395
+ fallback: FALLBACKS.diagnostics
2396
+ })
2397
+ }, DiagnosticsCard);
2398
+ });
2399
+ }
2400
+ //#endregion
2401
+ //#region src/panels/history/client/api.ts
2402
+ /**
2403
+ * The host routes this bundle calls, and the ONE place a request is made.
2404
+ *
2405
+ * G5/W7: the paths come from the generated client route table (rendered from evolution-core
2406
+ * HOST_ROUTES), and the transport classifier is the shared generated route-client — the browser half
2407
+ * can no longer drift from the host table or grow a second HTTP vocabulary.
2408
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
2409
+ */
2410
+ /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
2411
+ const HOST_ROUTES = ROUTES["skill-history"];
2412
+ /**
2413
+ * A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
2414
+ * four-state failure so the panel can render the shared failure block (G5/W7 S6b).
2415
+ */
2416
+ var SkillHistoryRefusal = class extends Error {
2417
+ failure;
2418
+ constructor(failure) {
2419
+ super(failureMessage(failure));
2420
+ this.name = "SkillHistoryRefusal";
2421
+ this.failure = failure;
2422
+ }
2423
+ };
2424
+ /**
2425
+ * Build the panel's face over one fetch implementation.
2426
+ * @param fetchImpl - the fetch to use; a spec passes a stub.
2427
+ * @returns the three callbacks the component receives through its inject face.
2428
+ */
2429
+ function createSkillHistoryApi(fetchImpl = fetch) {
2430
+ const request = async (path, init) => {
2431
+ const answer = await requestJson(fetchImpl, path, init);
2432
+ if (!answer.ok) throw new SkillHistoryRefusal(answer.failure);
2433
+ return answer.data;
2434
+ };
2435
+ return {
2436
+ skills: async () => await request(HOST_ROUTES.skills),
2437
+ versions: async (name) => await request(HOST_ROUTES.versions + "?name=" + encodeURIComponent(name)),
2438
+ diff: async (name, v) => await request(HOST_ROUTES.diff + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2439
+ body: async (name, v) => await request(HOST_ROUTES.body + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2440
+ undo: async (name, v, unitId) => {
2441
+ return (await request(HOST_ROUTES.undo, {
2442
+ method: "POST",
2443
+ headers: { "content-type": "application/json" },
2444
+ body: JSON.stringify(unitId !== void 0 && unitId !== "" ? {
2445
+ name,
2446
+ unitId
2447
+ } : v === void 0 ? { name } : {
2448
+ name,
2449
+ v
2450
+ })
2451
+ })).message;
2452
+ }
2453
+ };
2454
+ }
2455
+ //#endregion
2456
+ //#region src/panels/settings/api.ts
2457
+ /**
2458
+ * The approval routes this bundle calls, and the ONE place a request is made.
2459
+ *
2460
+ * Structural types on purpose: the browser half cannot import the host package at runtime, so the row
2461
+ * shape is declared by what the route hands over. Every answer — including a refusal — is parsed the
2462
+ * same way, because a refusal is a business answer the card shows as a sentence, not a transport error.
2463
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/api
2464
+ */
2465
+ /**
2466
+ * Build the approval face over one fetch implementation.
2467
+ * @param doFetch - the fetch to use (the platform's global in the browser, a fake in a spec).
2468
+ * @returns the three calls the pending card makes.
2469
+ */
2470
+ function createApprovalApi(doFetch) {
2471
+ const post = async (path, id) => await requestJson(doFetch, path, {
2472
+ method: "POST",
2473
+ headers: { "content-type": "application/json" },
2474
+ body: JSON.stringify({ id })
2475
+ });
2476
+ return {
2477
+ pending: async (status) => await requestJson(doFetch, status === void 0 ? ROUTES.approval.pending : ROUTES.approval.pending + "?status=" + status),
2478
+ approve: async (id) => await post(ROUTES.approval.approve, id),
2479
+ reject: async (id) => await post(ROUTES.approval.reject, id),
2480
+ release: async (id) => await post(ROUTES.approval.release, id),
2481
+ preview: async (id) => await requestJson(doFetch, ROUTES.approval.preview + "?id=" + encodeURIComponent(id))
2482
+ };
2483
+ }
2484
+ //#endregion
2485
+ //#region src/panels/settings/pending-state.ts
2486
+ /** The state a card mounts in: still loading, nothing in flight, nothing said yet. */
2487
+ const INITIAL_PENDING_STATE = {
2488
+ view: { kind: "loading" },
2489
+ busy: [],
2490
+ notice: null,
2491
+ refreshing: false,
2492
+ open: null,
2493
+ preview: null
2494
+ };
2495
+ /** Open one row's preview: the row is now the open one and its answer is on the way. */
2496
+ function expanded(state, id) {
2497
+ return {
2498
+ ...state,
2499
+ open: id,
2500
+ preview: { kind: "loading" }
2501
+ };
2502
+ }
2503
+ /** Close whatever preview is open. */
2504
+ function collapsed(state) {
2505
+ return {
2506
+ ...state,
2507
+ open: null,
2508
+ preview: null
2509
+ };
2510
+ }
2511
+ /**
2512
+ * A preview answer arrived. An answer for a row the reader has already left is DROPPED: it would
2513
+ * otherwise paint one row's diff under another row's heading.
2514
+ */
2515
+ function previewArrived(state, id, view) {
2516
+ return state.open === id ? {
2517
+ ...state,
2518
+ preview: view
2519
+ } : state;
2520
+ }
2521
+ /** The first load, or a refresh after a decision: back to loading, keeping no stale rows. */
2522
+ function loading(state) {
2523
+ if (state.view.kind === "ready" || state.view.kind === "empty") return {
2524
+ ...state,
2525
+ refreshing: true
2526
+ };
2527
+ return {
2528
+ ...state,
2529
+ view: { kind: "loading" },
2530
+ refreshing: true
2531
+ };
2532
+ }
2533
+ /**
2534
+ * A list arrived. An empty list is its OWN state — the reader is told the window is clear rather than
2535
+ * being shown the same line a slow answer shows.
2536
+ */
2537
+ function loaded(state, rows) {
2538
+ return {
2539
+ ...state,
2540
+ refreshing: false,
2541
+ view: rows.length === 0 ? { kind: "empty" } : {
2542
+ kind: "ready",
2543
+ rows
2375
2544
  }
2376
- const api = createApprovalApi((input, init) => fetch(input, init));
2377
- yield seam.slots.register({
2378
- name: CARD_SLOT,
2379
- key: PENDING_CARD_KEY,
2380
- inject: () => ({
2381
- t,
2382
- api,
2383
- confirmMode,
2384
- fallback: FALLBACKS.approval
2385
- })
2386
- }, PendingCard);
2387
- const diagApi = createDiagnosticsApi((input, init) => fetch(input, init));
2388
- yield seam.slots.register({
2389
- name: CARD_SLOT,
2390
- key: DIAGNOSTICS_CARD_KEY,
2391
- inject: () => ({
2392
- t,
2393
- api: diagApi,
2394
- fallback: FALLBACKS.diagnostics
2395
- })
2396
- }, DiagnosticsCard);
2397
- });
2398
- seam.slots.inject(ROW_CONFIG_SLOT, function* () {
2399
- for (const seat of CLIENT_ROW_SEATS) for (const rowId of seat.rows) {
2400
- const section = CLIENT_PARAM_SECTIONS.find((candidate) => candidate.namespace === rowId);
2401
- if (section === void 0) continue;
2402
- const face = () => ({
2403
- namespace: rowId,
2404
- fields: section.fields,
2405
- t
2406
- });
2407
- yield seam.slots.register({
2408
- name: ROW_CONFIG_SLOT,
2409
- key: rowConfigKey(seat.bundle, rowId),
2410
- inject: face
2411
- }, RowConfigCard);
2545
+ };
2546
+ }
2547
+ /** The list could not be read: the classified failure stays on screen next to a retry control. */
2548
+ function failed(state, failure) {
2549
+ return {
2550
+ ...state,
2551
+ refreshing: false,
2552
+ view: {
2553
+ kind: "failed",
2554
+ failure
2412
2555
  }
2413
- });
2556
+ };
2414
2557
  }
2415
- //#endregion
2416
- //#region ../evolution-client-kit/src/panel.ts
2417
- function interpolate(text, values) {
2418
- if (values === void 0) return text;
2419
- return text.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
2558
+ /** Mark one record as having an action in flight (its buttons are disabled until it ends). */
2559
+ function beginAction(state, id) {
2560
+ return state.busy.includes(id) ? state : {
2561
+ ...state,
2562
+ busy: [...state.busy, id]
2563
+ };
2420
2564
  }
2421
- function injectStyles(doc, css, tagId) {
2422
- if (doc === void 0) return () => {};
2423
- doc.querySelector("style[data-plugin-css=\"" + tagId + "\"]")?.remove();
2424
- const tag = doc.createElement("style");
2425
- tag.setAttribute("data-plugin-css", tagId);
2426
- tag.textContent = css;
2427
- doc.head.append(tag);
2428
- return () => {
2429
- tag.remove();
2565
+ /** The action ended, whatever it answered. */
2566
+ function endAction(state, id) {
2567
+ return {
2568
+ ...state,
2569
+ busy: state.busy.filter((candidate) => candidate !== id)
2570
+ };
2571
+ }
2572
+ /** One refusal or failure line under the list; `null` clears it (a later success does). */
2573
+ function noticed(state, message) {
2574
+ return {
2575
+ ...state,
2576
+ notice: message
2430
2577
  };
2431
2578
  }
2579
+ //#endregion
2580
+ //#region src/panels/settings/PendingCard.ts
2581
+ /**
2582
+ * The "writes awaiting approval" card: the staged write window, with the two decisions on each row.
2583
+ *
2584
+ * It is a card in the SAME section and wears the SAME shell as the parameter cards (header with title,
2585
+ * count and chevron; body of field blocks; a button row per block), so nothing here invents a second
2586
+ * visual language. Two things differ on purpose: the data comes from the host's approval routes rather
2587
+ * than the settings transport, and its three states are distinct — "reading", "nothing staged" and "the
2588
+ * read failed" each say so in their own words (E7: merging the first two is what made an empty panel
2589
+ * look like lost data).
2590
+ *
2591
+ * Every decision re-reads the window instead of predicting the outcome: the host owns the state, and a
2592
+ * refusal is shown with the host's own sentence.
2593
+ * @module @lmzhen/dsh-evolution-ui/panels/settings/client/PendingCard
2594
+ */
2595
+ /**
2596
+ * U-6: the registry's gate word, read out of a section's user layer. ONE definition, exported so the
2597
+ * mapping itself is judgeable: the entry does the wiring, this does the reading. Anything that is not
2598
+ * one of the two explicit words falls back to the shipped default (`auto`) — a document written by a
2599
+ * future version must not silently turn the confirmation ON, and a typo must not turn it off. The
2600
+ * independent review flagged that this reader had no case at all (P2-11).
2601
+ */
2602
+ function confirmModeFrom(user) {
2603
+ const value = typeof user === "object" && user !== null ? user.approvalConfirm : void 0;
2604
+ return value === "ask" || value === "timeout" ? "ask" : "auto";
2605
+ }
2606
+ /** Which word one age bucket gets; an unknown bucket falls back to the count form. */
2607
+ const AGE_KEYS = Object.freeze({
2608
+ now: "approvalAgeNow",
2609
+ minutes: "approvalAgeMinutes",
2610
+ hours: "approvalAgeHours",
2611
+ days: "approvalAgeDays",
2612
+ months: "approvalAgeMonths"
2613
+ });
2614
+ /** Which word one staged kind gets. */
2615
+ /** Verbs the host can report; unknown verbs fall back to the raw word. */
2616
+ const VERB_KEYS = Object.freeze({
2617
+ add: "approvalVerbAdd",
2618
+ create: "approvalVerbCreate",
2619
+ update: "approvalVerbUpdate",
2620
+ replace: "approvalVerbUpdate",
2621
+ patch: "approvalVerbUpdate",
2622
+ remove: "approvalVerbRemove",
2623
+ delete: "approvalVerbDelete",
2624
+ archive: "approvalVerbArchive",
2625
+ restore: "approvalVerbRestore",
2626
+ merge: "approvalVerbMerge"
2627
+ });
2628
+ const KIND_KEYS = Object.freeze({
2629
+ memory: "approvalKindMemory",
2630
+ skill: "approvalKindSkill",
2631
+ capability: "approvalKindCapability"
2632
+ });
2432
2633
  /**
2433
- * Register one sidebar row and its main body under one lifecycle.
2434
- * @param ctx - the client context carrying the slots and locale seats.
2435
- * @param spec - the feature-specific row/body descriptor.
2634
+ * Render the pending window.
2635
+ * @param props - the copy seat and the host calls.
2636
+ * @returns the card.
2436
2637
  */
2437
- function registerSidebarPanel(ctx, spec) {
2438
- const seam = ctx;
2439
- let registered = false;
2440
- ctx.effect(() => {
2441
- try {
2442
- const dispose = seam.locale.register(spec.namespace, spec.dictionaries);
2443
- registered = true;
2444
- return dispose;
2445
- } catch (error) {
2446
- ctx.logger.warn(spec.namespace + ": the locale seat refused the dictionary (" + String(error) + "); the panel falls back to the bundle copy");
2447
- return () => {};
2638
+ function PendingCard(props) {
2639
+ const { t, api } = props;
2640
+ const translate = (key, values) => t(key, values);
2641
+ const confirmMode = props.confirmMode?.() ?? "auto";
2642
+ const [armed, setArmed] = (0, react.useState)(null);
2643
+ const [state, setState] = (0, react.useState)(INITIAL_PENDING_STATE);
2644
+ const [open, setOpen] = (0, react.useState)(true);
2645
+ const viewKind = state.view.kind;
2646
+ const rowCount = state.view.kind === "ready" ? state.view.rows.length : 0;
2647
+ (0, react.useEffect)(() => {
2648
+ if (viewKind === "empty") setOpen(false);
2649
+ else if (viewKind === "ready" && rowCount > 0) setOpen(true);
2650
+ }, [viewKind, rowCount]);
2651
+ const latest = (0, react.useRef)(state);
2652
+ const apply = (next) => {
2653
+ const value = next(latest.current);
2654
+ latest.current = value;
2655
+ setState(value);
2656
+ };
2657
+ /**
2658
+ * Read the window the card is about: the writes WAITING and the writes RUNNING.
2659
+ *
2660
+ * T4-05/A47: the route has always taken `?status=` — the CLI asks for both lists — but this card
2661
+ * asked for neither, so a record whose approve died between the claim and the decision showed as
2662
+ * "nothing is waiting for a decision" while `/evolution pending` and doctor listed it. The two
2663
+ * answers are ordered the way the CLI orders them (waiting first); either one failing fails the
2664
+ * read, because a half-list would read as a complete window.
2665
+ */
2666
+ const readTicket = (0, react.useRef)(0);
2667
+ const load = async () => {
2668
+ apply(loading);
2669
+ const ticket = readTicket.current + 1;
2670
+ readTicket.current = ticket;
2671
+ const [waiting, running] = await Promise.all([api.pending(), api.pending("executing")]);
2672
+ if (readTicket.current !== ticket) return;
2673
+ if (!waiting.ok) {
2674
+ apply((current) => failed(current, waiting.failure));
2675
+ return;
2448
2676
  }
2449
- }, spec.namespace + ": locale");
2450
- ctx.effect(() => injectStyles(typeof document === "undefined" ? void 0 : document, spec.styles.css, spec.styles.tagId), spec.namespace + ": styles");
2451
- const fallback = spec.dictionaries[spec.fallback ?? "zh"];
2452
- const lookup = (key) => {
2453
- const own = fallback[key] ?? key;
2454
- if (!registered) return own;
2455
- try {
2456
- return seam.locale.bind(spec.namespace)(key);
2457
- } catch {
2458
- return own;
2677
+ if (!running.ok) {
2678
+ apply((current) => failed(current, running.failure));
2679
+ return;
2680
+ }
2681
+ apply((current) => loaded(current, [...waiting.data, ...running.data]));
2682
+ };
2683
+ (0, react.useEffect)(() => {
2684
+ load();
2685
+ const onFocus = () => {
2686
+ load();
2687
+ };
2688
+ window.addEventListener("focus", onFocus);
2689
+ return () => {
2690
+ window.removeEventListener("focus", onFocus);
2691
+ };
2692
+ }, []);
2693
+ const decide = (id, action) => {
2694
+ apply((current) => beginAction(current, id));
2695
+ (async () => {
2696
+ const answer = action === "approve" ? await api.approve(id) : action === "reject" ? await api.reject(id) : await api.release(id);
2697
+ if (!answer.ok) {
2698
+ apply((current) => noticed(endAction(current, id), failureText(answer.failure, translate)));
2699
+ return;
2700
+ }
2701
+ apply((current) => noticed(endAction(current, id), null));
2702
+ await load();
2703
+ })();
2704
+ };
2705
+ const age = (row) => {
2706
+ const key = AGE_KEYS[row.age.unit];
2707
+ if (key === void 0) return String(row.age.n);
2708
+ return t(key, { n: row.age.n });
2709
+ };
2710
+ /** Ask the host what this record would store; the answer lands only if the row is still open. */
2711
+ const loadPreview = async (id) => {
2712
+ const answer = await api.preview(id);
2713
+ const view = answer.ok ? answer.data.available ? {
2714
+ kind: "ready",
2715
+ answer: answer.data
2716
+ } : {
2717
+ kind: "unavailable",
2718
+ reason: answer.data.reason
2719
+ } : {
2720
+ kind: "failed",
2721
+ failure: answer.failure
2722
+ };
2723
+ apply((current) => previewArrived(current, id, view));
2724
+ };
2725
+ const togglePreview = (id) => {
2726
+ if (state.open === id) {
2727
+ apply(collapsed);
2728
+ return;
2729
+ }
2730
+ apply((current) => expanded(current, id));
2731
+ loadPreview(id);
2732
+ };
2733
+ /** The facts line, then the source view: the same shape the history panel's diff uses. */
2734
+ const preview = (view) => {
2735
+ if (view.kind === "loading") return (0, react.createElement)("p", {
2736
+ className: "evolution-param-note",
2737
+ role: "status"
2738
+ }, t("approvalPreviewReading"));
2739
+ if (view.kind === "failed") return (0, react.createElement)("p", {
2740
+ className: "evolution-param-error",
2741
+ role: "status"
2742
+ }, failureText(view.failure, translate));
2743
+ if (view.kind === "unavailable") return (0, react.createElement)("p", {
2744
+ className: "evolution-param-error",
2745
+ role: "status"
2746
+ }, t("approvalPreviewNone") + view.reason);
2747
+ const facts = [
2748
+ (0, react.createElement)("span", {
2749
+ className: "evolution-param-fact",
2750
+ key: "path"
2751
+ }, t("approvalPreviewPath", { path: view.answer.path })),
2752
+ (0, react.createElement)("span", {
2753
+ className: "evolution-param-fact",
2754
+ key: "added"
2755
+ }, t("approvalDiffAdded", { n: view.answer.linesAdded })),
2756
+ (0, react.createElement)("span", {
2757
+ className: "evolution-param-fact",
2758
+ key: "removed"
2759
+ }, t("approvalDiffRemoved", { n: view.answer.linesRemoved })),
2760
+ ...view.answer.truncated ? [(0, react.createElement)("span", {
2761
+ className: "evolution-param-fact",
2762
+ key: "cut"
2763
+ }, t("approvalDiffCut"))] : []
2764
+ ];
2765
+ const lines = [];
2766
+ for (const [hunkIndex, hunk] of view.answer.hunks.entries()) {
2767
+ for (const [index, line] of hunk.oldText.split("\n").entries()) {
2768
+ if (hunk.oldText === "" && line === "") continue;
2769
+ lines.push((0, react.createElement)("div", {
2770
+ key: "old-" + String(hunkIndex) + "-" + String(index),
2771
+ className: "evolution-param-line-del"
2772
+ }, "- " + line));
2773
+ }
2774
+ for (const [index, line] of hunk.newText.split("\n").entries()) {
2775
+ if (hunk.newText === "" && line === "") continue;
2776
+ lines.push((0, react.createElement)("div", {
2777
+ key: "new-" + String(hunkIndex) + "-" + String(index),
2778
+ className: "evolution-param-line-add"
2779
+ }, "+ " + line));
2780
+ }
2459
2781
  }
2782
+ 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));
2783
+ };
2784
+ const summaryLine = (entry) => entry.verb === void 0 || entry.target === void 0 ? entry.summary : t(VERB_KEYS[entry.verb] ?? "approvalVerbUpdate") + " " + entry.target + (entry.operationCount !== void 0 && entry.operationCount > 1 ? " ×" + String(entry.operationCount) : "");
2785
+ const row = (entry) => {
2786
+ if (entry.status === "executing") return (0, react.createElement)("div", {
2787
+ className: "evolution-param-field",
2788
+ key: entry.id
2789
+ }, (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", {
2790
+ className: "evolution-param-value",
2791
+ title: entry.summary
2792
+ }, summaryLine(entry))), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), (0, react.createElement)("p", {
2793
+ className: "evolution-param-note",
2794
+ role: "status"
2795
+ }, t("approvalExecuting")), entry.claimedBy === void 0 ? null : (0, react.createElement)("p", { className: "evolution-param-hint" }, entry.claimedBy), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
2796
+ className: "evolution-param-button",
2797
+ type: "button",
2798
+ disabled: state.busy.includes(entry.id),
2799
+ onClick: () => {
2800
+ decide(entry.id, "release");
2801
+ }
2802
+ }, state.busy.includes(entry.id) ? t("approvalReleasing") : t("approvalRelease"))));
2803
+ const busy = state.busy.includes(entry.id);
2804
+ const button = (label, action, tone) => (0, react.createElement)("button", {
2805
+ className: "evolution-param-button",
2806
+ type: "button",
2807
+ disabled: busy,
2808
+ ...tone === "primary" ? { "data-primary": "true" } : { "data-tone": tone },
2809
+ onClick: () => {
2810
+ if (action === "approve" && confirmMode === "ask" && armed !== entry.id) {
2811
+ setArmed(entry.id);
2812
+ return;
2813
+ }
2814
+ setArmed(null);
2815
+ decide(entry.id, action);
2816
+ }
2817
+ }, busy ? t(action === "approve" ? "approvalApproving" : "approvalRejecting") : t(label));
2818
+ return (0, react.createElement)("div", {
2819
+ className: "evolution-param-field",
2820
+ key: entry.id
2821
+ }, (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", {
2822
+ className: "evolution-param-value",
2823
+ title: entry.summary
2824
+ }, summaryLine(entry))), (0, react.createElement)("p", { className: "evolution-param-hint" }, age(entry)), armed === entry.id ? (0, react.createElement)("p", {
2825
+ className: "evolution-param-note",
2826
+ role: "status"
2827
+ }, (0, react.createElement)("span", { className: "evolution-param-value" }, t("approvalConfirmNamed", { n: summaryLine(entry) })), (0, react.createElement)("button", {
2828
+ className: "evolution-param-button",
2829
+ type: "button",
2830
+ "data-confirm": "yes",
2831
+ "data-primary": "true",
2832
+ onClick: () => {
2833
+ setArmed(null);
2834
+ decide(entry.id, "approve");
2835
+ }
2836
+ }, t("approvalConfirmYes")), (0, react.createElement)("button", {
2837
+ className: "evolution-param-button",
2838
+ type: "button",
2839
+ "data-tone": "ghost",
2840
+ onClick: () => {
2841
+ setArmed(null);
2842
+ }
2843
+ }, t("approvalConfirmNo"))) : null, (0, react.createElement)("div", { className: "evolution-param-actions" }, button("approvalApprove", "approve", "primary"), button("approvalReject", "reject", "outline"), (0, react.createElement)("button", {
2844
+ className: "evolution-param-button",
2845
+ type: "button",
2846
+ "data-tone": "ghost",
2847
+ onClick: () => {
2848
+ togglePreview(entry.id);
2849
+ }
2850
+ }, state.open === entry.id ? t("approvalPreviewClose") : t("approvalPreview"))), state.open === entry.id && state.preview !== null ? preview(state.preview) : null);
2460
2851
  };
2461
- const t = (key, values) => interpolate(lookup(key), values);
2462
- const label = () => lookup("entry.label");
2463
- seam.slots.inject("sidebar.panellist", function* () {
2464
- yield seam.slots.register({
2465
- name: "sidebar.panellist",
2466
- id: spec.id,
2467
- order: spec.order,
2468
- label
2469
- }, spec.icon);
2470
- yield seam.slots.register({
2471
- name: "main",
2472
- key: spec.id,
2473
- inject: () => spec.body(t)
2474
- }, spec.component);
2475
- });
2476
- }
2477
- //#endregion
2478
- //#region src/panels/history/client/api.ts
2479
- /**
2480
- * The host routes this bundle calls, and the ONE place a request is made.
2481
- *
2482
- * G5/W7: the paths come from the generated client route table (rendered from evolution-core
2483
- * HOST_ROUTES), and the transport classifier is the shared generated route-client — the browser half
2484
- * can no longer drift from the host table or grow a second HTTP vocabulary.
2485
- * @module @lmzhen/dsh-evolution-ui/panels/history/client
2486
- */
2487
- /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
2488
- const HOST_ROUTES = ROUTES["skill-history"];
2489
- /**
2490
- * A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
2491
- * four-state failure so the panel can render the shared failure block (G5/W7 S6b).
2492
- */
2493
- var SkillHistoryRefusal = class extends Error {
2494
- failure;
2495
- constructor(failure) {
2496
- super(failureMessage(failure));
2497
- this.name = "SkillHistoryRefusal";
2498
- this.failure = failure;
2499
- }
2500
- };
2501
- /**
2502
- * Build the panel's face over one fetch implementation.
2503
- * @param fetchImpl - the fetch to use; a spec passes a stub.
2504
- * @returns the three callbacks the component receives through its inject face.
2505
- */
2506
- function createSkillHistoryApi(fetchImpl = fetch) {
2507
- const request = async (path, init) => {
2508
- const answer = await requestJson(fetchImpl, path, init);
2509
- if (!answer.ok) throw new SkillHistoryRefusal(answer.failure);
2510
- return answer.data;
2852
+ const body = () => {
2853
+ if (state.view.kind === "loading") return (0, react.createElement)("p", {
2854
+ className: "evolution-param-note",
2855
+ role: "status"
2856
+ }, t("approvalLoading"));
2857
+ if (state.view.kind === "empty") return (0, react.createElement)("p", {
2858
+ className: "evolution-param-note",
2859
+ role: "status"
2860
+ }, t("approvalEmpty"));
2861
+ if (state.view.kind === "failed") return (0, react.createElement)("div", { className: "evolution-param-actions" }, FailureBlock({
2862
+ failure: state.view.failure,
2863
+ t: translate,
2864
+ fallback: props.fallback
2865
+ }), (0, react.createElement)("button", {
2866
+ className: "evolution-param-button",
2867
+ type: "button",
2868
+ onClick: () => {
2869
+ load();
2870
+ }
2871
+ }, t("approvalRetry")));
2872
+ return (0, react.createElement)("div", null, ...state.view.rows.map(row), state.notice === null ? null : (0, react.createElement)("p", {
2873
+ className: "evolution-param-error",
2874
+ role: "status"
2875
+ }, state.notice));
2511
2876
  };
2512
- return {
2513
- skills: async () => await request(HOST_ROUTES.skills),
2514
- versions: async (name) => await request(HOST_ROUTES.versions + "?name=" + encodeURIComponent(name)),
2515
- diff: async (name, v) => await request(HOST_ROUTES.diff + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2516
- body: async (name, v) => await request(HOST_ROUTES.body + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
2517
- undo: async (name, v, unitId) => {
2518
- return (await request(HOST_ROUTES.undo, {
2519
- method: "POST",
2520
- headers: { "content-type": "application/json" },
2521
- body: JSON.stringify(unitId !== void 0 && unitId !== "" ? {
2522
- name,
2523
- unitId
2524
- } : v === void 0 ? { name } : {
2525
- name,
2526
- v
2527
- })
2528
- })).message;
2877
+ const rows = state.view.kind === "ready" ? state.view.rows : [];
2878
+ const waiting = rows.filter((entry) => entry.status !== "executing").length;
2879
+ const running = rows.length - waiting;
2880
+ return (0, react.createElement)("section", {
2881
+ className: "evolution-param-card",
2882
+ "data-confirm-mode": confirmMode
2883
+ }, (0, react.createElement)("button", {
2884
+ className: "evolution-param-head",
2885
+ type: "button",
2886
+ onClick: () => {
2887
+ setOpen(!open);
2888
+ },
2889
+ "aria-expanded": open
2890
+ }, (0, react.createElement)("span", { className: "evolution-param-card-title" }, t("approvalTitle")), state.view.kind === "empty" ? (0, react.createElement)("span", {
2891
+ className: "evolution-param-count",
2892
+ role: "status"
2893
+ }, t("approvalEmpty")) : null, 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 ? "▲" : "▼")), (0, react.createElement)("div", { className: "evolution-param-actions" }, (0, react.createElement)("button", {
2894
+ className: "evolution-param-button",
2895
+ type: "button",
2896
+ disabled: state.refreshing,
2897
+ onClick: () => {
2898
+ load();
2529
2899
  }
2530
- };
2900
+ }, state.refreshing ? t("diagRefreshing") : t("approvalRefresh"))), open ? (0, react.createElement)("div", { className: "evolution-param-body" }, body()) : null);
2531
2901
  }
2532
2902
  //#endregion
2533
2903
  //#region src/panels/history/client/messages.ts
@@ -2545,6 +2915,10 @@ window.__ModuleLoader__.load({
2545
2915
  const zh = {
2546
2916
  "entry.label": "技能历史",
2547
2917
  title: "技能历史",
2918
+ "workbench.history": "技能历史",
2919
+ "workbench.pending": "待批",
2920
+ "workbench.merge": "合并",
2921
+ "workbench.health": "健康",
2548
2922
  hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。内容相同的版本会同时标「当前」。",
2549
2923
  search: "按名字或描述筛选",
2550
2924
  "search.none": "没有匹配的技能。",
@@ -2568,7 +2942,7 @@ window.__ModuleLoader__.load({
2568
2942
  "failure.unmounted": "该功能没有挂载。",
2569
2943
  "failure.server": "读取失败,可以重试。",
2570
2944
  "fallback.hint": "可以改用命令:{command}",
2571
- "undo.confirm": "确认回退",
2945
+ "undo.confirm": "确认回退(会写入一版,可再回退)",
2572
2946
  "undo.no": "取消",
2573
2947
  current: "当前",
2574
2948
  "versions.count": "{n} 个版本",
@@ -2589,6 +2963,7 @@ window.__ModuleLoader__.load({
2589
2963
  "markdown.footnotes": "脚注",
2590
2964
  "diff.hide": "收起差异",
2591
2965
  "diff.loading": "读取差异…",
2966
+ "preview.loading": "读取预览…",
2592
2967
  "diff.against": "与 v{n} 比较",
2593
2968
  "diff.first": "这是第一版,整篇都是新增",
2594
2969
  "diff.truncated": "差异过长,只显示前一部分",
@@ -2631,6 +3006,10 @@ window.__ModuleLoader__.load({
2631
3006
  const en = {
2632
3007
  "entry.label": "Skill history",
2633
3008
  title: "Skill history",
3009
+ "workbench.history": "Skill history",
3010
+ "workbench.pending": "Pending",
3011
+ "workbench.merge": "Merge",
3012
+ "workbench.health": "Health",
2634
3013
  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”.",
2635
3014
  search: "Filter by name or description",
2636
3015
  "search.none": "No skill matches.",
@@ -2654,7 +3033,7 @@ window.__ModuleLoader__.load({
2654
3033
  "failure.unmounted": "This feature is not mounted.",
2655
3034
  "failure.server": "The read failed; you can retry.",
2656
3035
  "fallback.hint": "Command fallback: {command}",
2657
- "undo.confirm": "Confirm restore",
3036
+ "undo.confirm": "Confirm restore (writes a new version; can be undone)",
2658
3037
  "undo.no": "Cancel",
2659
3038
  current: "Current",
2660
3039
  "versions.count": "{n} versions",
@@ -2675,6 +3054,7 @@ window.__ModuleLoader__.load({
2675
3054
  "markdown.footnotes": "Footnotes",
2676
3055
  "diff.hide": "Hide diff",
2677
3056
  "diff.loading": "Loading the diff…",
3057
+ "preview.loading": "Loading the preview…",
2678
3058
  "diff.against": "compared with v{n}",
2679
3059
  "diff.first": "first version: everything is new",
2680
3060
  "diff.truncated": "the change is long; only its beginning is shown",
@@ -2713,19 +3093,6 @@ window.__ModuleLoader__.load({
2713
3093
  error: "Could not load",
2714
3094
  "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."
2715
3095
  };
2716
- /**
2717
- * Fill one TEMPLATE’s `{n}` slots. The seat resolves the key, this fills it, so the panel needs
2718
- * only the seat plus this rule and never learns which language it is rendering.
2719
- * @param template - the copy, with `{slot}` placeholders.
2720
- * @param values - the substitutions, keyed by the slot name inside the braces.
2721
- * @returns the filled copy.
2722
- */
2723
- function fill(template, values) {
2724
- return template.replace(/\{(\w+)\}/g, (match, slot) => {
2725
- const value = values[slot];
2726
- return value === void 0 ? match : String(value);
2727
- });
2728
- }
2729
3096
  //#endregion
2730
3097
  //#region src/panels/history/client/styles.ts
2731
3098
  /**
@@ -2815,7 +3182,8 @@ window.__ModuleLoader__.load({
2815
3182
  ".evo-doc-add{border-left-color:var(--evo-tone-success)}",
2816
3183
  ".evo-doc-tag{position:absolute;left:var(--evo-space-8);top:var(--evo-space-8);color:var(--dsw-alias-label-tertiary)}",
2817
3184
  ".evo-doc-line-add{color:var(--evo-tone-success)}",
2818
- ".evo-doc-line-del{color:var(--evo-tone-danger)}"
3185
+ ".evo-doc-line-del{color:var(--evo-tone-danger)}",
3186
+ "@media (max-width:720px){.evo-hist-root{flex-direction:column}.evo-hist-aside{flex:0 0 auto;width:100%;max-height:42%;border-right:0;border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}.evo-hist-row{max-width:none}}"
2819
3187
  ].join("\n");
2820
3188
  //#endregion
2821
3189
  //#region src/panels/history/client/atoms.ts
@@ -3261,7 +3629,7 @@ window.__ModuleLoader__.load({
3261
3629
  */
3262
3630
  function PreviewBody(props) {
3263
3631
  const { face, body } = props;
3264
- if (body.kind === "loading") return note({ children: face.t("diff.loading") });
3632
+ if (body.kind === "loading") return note({ children: face.t("preview.loading") });
3265
3633
  if (body.kind === "failed") return note({
3266
3634
  error: true,
3267
3635
  children: body.message
@@ -3377,7 +3745,9 @@ window.__ModuleLoader__.load({
3377
3745
  const [diffs, setDiffs] = (0, react.useState)(/* @__PURE__ */ new Map());
3378
3746
  const [bodies, setBodies] = (0, react.useState)(/* @__PURE__ */ new Map());
3379
3747
  const readTicket = (0, react.useRef)(0);
3380
- const failed = (error) => {
3748
+ const failureSource = (0, react.useRef)(void 0);
3749
+ const failed = (error, source = "read") => {
3750
+ failureSource.current = source;
3381
3751
  setNoteError(true);
3382
3752
  setNoteText(face.t("error.hint") + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
3383
3753
  setFailure(error instanceof SkillHistoryRefusal ? error.failure : {
@@ -3389,11 +3759,22 @@ window.__ModuleLoader__.load({
3389
3759
  setNoteError(false);
3390
3760
  setNoteText(message);
3391
3761
  };
3762
+ /** A successful read clears only the read failure; a write result is not erased by a later read. */
3763
+ const clearReadFailure = () => {
3764
+ if (failureSource.current !== "read") return;
3765
+ failureSource.current = void 0;
3766
+ setFailure(void 0);
3767
+ setNoteError(false);
3768
+ setNoteText(void 0);
3769
+ };
3392
3770
  /** The sentence one failed read carries, in ONE place: every reader below reports the same way. */
3393
3771
  const failedMessage = (error) => face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
3394
3772
  /** Read the skills again, keeping the sentence: a re-read is not an answer to anything. */
3395
3773
  const reloadSkills = () => {
3396
- face.loadSkills().then(setSkills).catch(failed).finally(() => {
3774
+ face.loadSkills().then((next) => {
3775
+ setSkills(next);
3776
+ clearReadFailure();
3777
+ }).catch(failed).finally(() => {
3397
3778
  setSkillsLoaded(true);
3398
3779
  });
3399
3780
  };
@@ -3414,7 +3795,10 @@ window.__ModuleLoader__.load({
3414
3795
  setDiffs(/* @__PURE__ */ new Map());
3415
3796
  setBodies(/* @__PURE__ */ new Map());
3416
3797
  face.loadVersions(name).then((next) => {
3417
- if (readTicket.current === ticket) setPayload(next);
3798
+ if (readTicket.current === ticket) {
3799
+ setPayload(next);
3800
+ clearReadFailure();
3801
+ }
3418
3802
  }).catch((error) => {
3419
3803
  if (readTicket.current !== ticket) return;
3420
3804
  setVersionsFailed(failedMessage(error));
@@ -3434,6 +3818,10 @@ window.__ModuleLoader__.load({
3434
3818
  reloadSkills();
3435
3819
  if (selected !== void 0) showVersions(selected);
3436
3820
  };
3821
+ (0, react.useEffect)(() => {
3822
+ if (face.focusSkill === void 0 || face.focusSkill === "") return;
3823
+ showVersions(face.focusSkill);
3824
+ }, [face.focusSkill, face.focusRevision]);
3437
3825
  const restore = (v, unitId) => {
3438
3826
  if (selected === void 0) return;
3439
3827
  if (pending !== v) {
@@ -3448,7 +3836,9 @@ window.__ModuleLoader__.load({
3448
3836
  setNoteText(message + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
3449
3837
  });
3450
3838
  succeeded(message);
3451
- }).catch(failed);
3839
+ }).catch((error) => {
3840
+ failed(error, "write");
3841
+ });
3452
3842
  };
3453
3843
  /**
3454
3844
  * Expand one row, or collapse it: one row is open at a time, and each kind keeps its own lazy read.
@@ -3591,61 +3981,289 @@ window.__ModuleLoader__.load({
3591
3981
  return next;
3592
3982
  }
3593
3983
  //#endregion
3594
- //#region src/panels/history/client/ids.ts
3595
- const PANEL_ID = "skill-history";
3596
- function PanelIcon(props = {}) {
3597
- const size = typeof props.size === "number" && props.size > 0 ? props.size : 18;
3598
- return (0, react.createElement)("svg", {
3599
- "aria-hidden": "true",
3600
- width: String(size),
3601
- height: String(size),
3602
- viewBox: "0 0 16 16",
3603
- fill: "none",
3604
- stroke: "currentColor",
3605
- strokeWidth: "1.5",
3606
- strokeLinecap: "round",
3607
- strokeLinejoin: "round"
3608
- }, (0, react.createElement)("circle", {
3609
- cx: "8",
3610
- cy: "8",
3611
- r: "5.75"
3612
- }), (0, react.createElement)("path", { d: "M8 4.75V8l2.25 1.5" }));
3984
+ //#region src/panels/history/client/Workbench.ts
3985
+ /**
3986
+ * The family's one task page. G3 owns the history view; later groups add health
3987
+ * without creating another page or a second navigation registry.
3988
+ * @module @lmzhen/dsh-evolution-ui/panels/history/client
3989
+ */
3990
+ /** Stable page id used by the sidebar row, the page slot key, and navigation params. */
3991
+ const WORKBENCH_ID = "evolution.workbench";
3992
+ /** Convert a navigation params value to a skill name only when it is a non-empty string. */
3993
+ function skillOf(params) {
3994
+ const value = params?.["skillName"];
3995
+ return typeof value === "string" && value !== "" ? value : void 0;
3613
3996
  }
3614
3997
  /**
3615
- * Register the locale namespace, the panel row and the panel body.
3616
- * @param ctx - the client cordis context.
3998
+ * Render the workbench page. The platform keeps it mounted; this component only
3999
+ * consumes a new explicit revision exactly once.
4000
+ * @param props - page owner props plus the injected history face.
4001
+ * @returns the page surface.
3617
4002
  */
3618
- function apply$1(ctx) {
3619
- const api = createSkillHistoryApi();
3620
- registerSidebarPanel(ctx, {
3621
- id: PANEL_ID,
3622
- order: 35,
3623
- namespace: NS,
3624
- dictionaries: {
3625
- zh,
3626
- en
4003
+ function WorkbenchPage(props) {
4004
+ const [selectedSkill, setSelectedSkill] = (0, react.useState)(void 0);
4005
+ const [view, setView] = (0, react.useState)("history");
4006
+ const lastRevision = (0, react.useRef)(void 0);
4007
+ (0, react.useEffect)(() => {
4008
+ if (props.revision === void 0 || props.revision === lastRevision.current) return;
4009
+ lastRevision.current = props.revision;
4010
+ const requested = props.params?.["view"];
4011
+ if (requested === "pending" || requested === "merge" || requested === "health") {
4012
+ setView(requested);
4013
+ return;
4014
+ }
4015
+ setView("history");
4016
+ const skill = skillOf(props.params);
4017
+ if (skill !== void 0) setSelectedSkill(skill);
4018
+ }, [props.params, props.revision]);
4019
+ const viewOrder = [
4020
+ "history",
4021
+ "pending",
4022
+ "merge",
4023
+ "health"
4024
+ ];
4025
+ const onTabKey = (event, current) => {
4026
+ const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
4027
+ if (delta === 0 && event.key !== "Home" && event.key !== "End") return;
4028
+ event.preventDefault();
4029
+ const index = viewOrder.indexOf(current);
4030
+ const next = event.key === "Home" ? viewOrder[0] : event.key === "End" ? viewOrder[viewOrder.length - 1] : viewOrder[(index + delta + viewOrder.length) % viewOrder.length];
4031
+ setView(next);
4032
+ requestAnimationFrame(() => {
4033
+ document.getElementById("evo-workbench-tab-" + next)?.focus();
4034
+ });
4035
+ };
4036
+ const hasMerge = props.hasView?.("merge") ?? true;
4037
+ const hasHealth = props.hasView?.("health") ?? true;
4038
+ const focusRevision = props.revision === lastRevision.current ? props.revision : void 0;
4039
+ return (0, react.createElement)("section", {
4040
+ className: "evolution-page-surface evo-workbench",
4041
+ "data-page": view
4042
+ }, (0, react.createElement)("div", { className: "evo-workbench-head" }, (0, react.createElement)("h1", { className: "evo-workbench-title" }, props.t("entry.label")), (0, react.createElement)("div", {
4043
+ className: "evo-workbench-tabs",
4044
+ role: "tablist",
4045
+ "aria-label": props.t("entry.label")
4046
+ }, (0, react.createElement)("button", {
4047
+ id: "evo-workbench-tab-history",
4048
+ type: "button",
4049
+ role: "tab",
4050
+ tabIndex: view === "history" ? 0 : -1,
4051
+ "aria-selected": view === "history" ? "true" : "false",
4052
+ "aria-controls": "evo-workbench-panel",
4053
+ "data-active": view === "history" ? "true" : void 0,
4054
+ onClick: () => {
4055
+ setView("history");
3627
4056
  },
3628
- fallback: "en",
3629
- styles: {
3630
- css: CSS,
3631
- tagId: CSS_TAG_ID
4057
+ onKeyDown: (event) => {
4058
+ onTabKey(event, "history");
4059
+ }
4060
+ }, props.t("workbench.history")), (0, react.createElement)("button", {
4061
+ id: "evo-workbench-tab-pending",
4062
+ type: "button",
4063
+ role: "tab",
4064
+ tabIndex: view === "pending" ? 0 : -1,
4065
+ "aria-selected": view === "pending" ? "true" : "false",
4066
+ "aria-controls": "evo-workbench-panel",
4067
+ "data-active": view === "pending" ? "true" : void 0,
4068
+ onClick: () => {
4069
+ setView("pending");
3632
4070
  },
3633
- icon: PanelIcon,
3634
- component: SkillHistoryPanel,
3635
- body: (t) => ({
3636
- t,
3637
- format: (key, values) => fill(t(key), values),
3638
- loadSkills: api.skills,
3639
- loadVersions: api.versions,
3640
- loadDiff: api.diff,
3641
- loadBody: api.body,
3642
- undo: api.undo,
3643
- markdownWords: {
3644
- copy: t("markdown.copy"),
3645
- copied: t("markdown.copied"),
3646
- footnotes: t("markdown.footnotes")
3647
- }
3648
- })
4071
+ onKeyDown: (event) => {
4072
+ onTabKey(event, "pending");
4073
+ }
4074
+ }, props.t("workbench.pending")), !hasMerge ? null : (0, react.createElement)("button", {
4075
+ id: "evo-workbench-tab-merge",
4076
+ type: "button",
4077
+ role: "tab",
4078
+ tabIndex: view === "merge" ? 0 : -1,
4079
+ "aria-selected": view === "merge" ? "true" : "false",
4080
+ "aria-controls": "evo-workbench-panel",
4081
+ "data-active": view === "merge" ? "true" : void 0,
4082
+ onClick: () => {
4083
+ setView("merge");
4084
+ },
4085
+ onKeyDown: (event) => {
4086
+ onTabKey(event, "merge");
4087
+ }
4088
+ }, props.t("workbench.merge")), !hasHealth ? null : (0, react.createElement)("button", {
4089
+ id: "evo-workbench-tab-health",
4090
+ type: "button",
4091
+ role: "tab",
4092
+ tabIndex: view === "health" ? 0 : -1,
4093
+ "aria-selected": view === "health" ? "true" : "false",
4094
+ "aria-controls": "evo-workbench-panel",
4095
+ "data-active": view === "health" ? "true" : void 0,
4096
+ onClick: () => {
4097
+ setView("health");
4098
+ },
4099
+ onKeyDown: (event) => {
4100
+ onTabKey(event, "health");
4101
+ }
4102
+ }, props.t("workbench.health")))), (0, react.createElement)("div", {
4103
+ className: "evo-workbench-body",
4104
+ id: "evo-workbench-panel",
4105
+ role: "tabpanel"
4106
+ }, view === "history" ? (0, react.createElement)(SkillHistoryPanel, {
4107
+ ...props.history,
4108
+ focusSkill: selectedSkill,
4109
+ focusRevision
4110
+ }) : view === "pending" ? (0, react.createElement)(PendingCard, props.pending) : null, (0, react.createElement)("div", {
4111
+ hidden: view !== "merge" && view !== "health",
4112
+ className: "evo-workbench-feature"
4113
+ }, props.renderSlot?.("evolution.workbench.view", { activeView: view }) ?? null)));
4114
+ }
4115
+ /**
4116
+ * Render the sidebar entry that opens the workbench.
4117
+ * @param props - active page id, opener, and copy.
4118
+ * @returns the row button.
4119
+ */
4120
+ function WorkbenchNav(props) {
4121
+ const active = props.activePageId === WORKBENCH_ID;
4122
+ return (0, react.createElement)("button", {
4123
+ type: "button",
4124
+ className: "evo-workbench-nav",
4125
+ "data-active": active ? "true" : void 0,
4126
+ "aria-current": active ? "page" : void 0,
4127
+ onClick: () => {
4128
+ props.openPage(WORKBENCH_ID);
4129
+ }
4130
+ }, props.t("entry.label"));
4131
+ }
4132
+ /** Workbench shell and sidebar row; colours come from the platform tokens. */
4133
+ const WORKBENCH_CSS = [
4134
+ tokenVars(".evo-workbench"),
4135
+ ".evo-workbench{display:flex;flex-direction:column;height:100%;min-height:0;min-width:0}",
4136
+ ".evo-workbench-head{display:flex;align-items:center;justify-content:space-between;gap:var(--evo-space-16);padding:var(--evo-space-12) var(--evo-space-16);border-bottom:var(--evo-hairline-width) solid var(--dsw-alias-border-l2)}",
4137
+ ".evo-workbench-tabs{display:flex;gap:var(--evo-space-4)}",
4138
+ ".evo-workbench-tabs button{padding:var(--evo-space-4) var(--evo-space-8);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:var(--evo-type-sm);cursor:pointer}",
4139
+ ".evo-workbench-tabs button[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
4140
+ ".evo-workbench-tabs button:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4141
+ ".evo-workbench-title{margin:0;font-size:var(--evo-type-lg);font-weight:600;line-height:var(--evo-leading-base)}",
4142
+ ".evo-workbench-body{display:flex;flex:1;min-height:0;min-width:0;overflow:hidden}",
4143
+ ".evo-workbench-nav{display:flex;width:100%;align-items:center;gap:var(--evo-space-8);padding:var(--evo-space-6) var(--evo-space-12);border:0;border-radius:var(--evo-radius-control);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:var(--evo-type-sm);text-align:left;cursor:pointer}",
4144
+ ".evo-workbench-nav:hover,.evo-workbench-nav[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
4145
+ ".evo-workbench-nav:focus-visible{outline:var(--evo-focus-ring);outline-offset:var(--evo-focus-offset)}",
4146
+ "@media (max-width:720px){.evo-workbench-head{flex-wrap:wrap}.evo-workbench-tabs{flex-wrap:wrap}}"
4147
+ ].join("");
4148
+ //#endregion
4149
+ //#region src/panels/history/client/index.ts
4150
+ function injectStyles(documentRef, css) {
4151
+ if (documentRef === void 0) return () => {};
4152
+ documentRef.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-ui/panel.css\"]")?.remove();
4153
+ const tag = documentRef.createElement("style");
4154
+ tag.setAttribute("data-plugin-css", CSS_TAG_ID);
4155
+ tag.textContent = css;
4156
+ documentRef.head.appendChild(tag);
4157
+ return () => {
4158
+ tag.remove();
4159
+ };
4160
+ }
4161
+ /**
4162
+ * Register the locale namespace, the workbench page, and its sidebar entry.
4163
+ * @param ctx - client cordis context.
4164
+ */
4165
+ function apply$1(ctx) {
4166
+ const seam = ctx;
4167
+ const api = createSkillHistoryApi();
4168
+ let registered = false;
4169
+ ctx.effect(() => {
4170
+ try {
4171
+ const dispose = seam.locale.register(NS, {
4172
+ zh,
4173
+ en
4174
+ });
4175
+ registered = true;
4176
+ return dispose;
4177
+ } catch (error) {
4178
+ ctx.logger.warn("evolution-skill-history: the locale seat refused the dictionary (" + String(error) + "); the workbench falls back to the bundle copy");
4179
+ return () => {};
4180
+ }
4181
+ }, "evolution-skill-history: locale");
4182
+ ctx.effect(() => injectStyles(typeof document === "undefined" ? void 0 : document, CSS + WORKBENCH_CSS), "evolution-skill-history: styles");
4183
+ const fallback = zh;
4184
+ const lookup = (key) => {
4185
+ const own = fallback[key] ?? key;
4186
+ if (!registered) return own;
4187
+ try {
4188
+ return seam.locale.bind(NS)(key);
4189
+ } catch {
4190
+ return own;
4191
+ }
4192
+ };
4193
+ const t = (key, values) => {
4194
+ const template = lookup(key);
4195
+ if (values === void 0) return template;
4196
+ return template.replace(/\{(\w+)\}/g, (match, slot) => values[slot] === void 0 ? match : String(values[slot]));
4197
+ };
4198
+ const historyFace = () => ({
4199
+ t,
4200
+ format: (key, values) => t(key, values),
4201
+ loadSkills: api.skills,
4202
+ loadVersions: api.versions,
4203
+ loadDiff: api.diff,
4204
+ loadBody: api.body,
4205
+ undo: api.undo,
4206
+ markdownWords: {
4207
+ copy: t("markdown.copy"),
4208
+ copied: t("markdown.copied"),
4209
+ footnotes: t("markdown.footnotes")
4210
+ }
4211
+ });
4212
+ const pendingT = (key, values) => {
4213
+ let template;
4214
+ try {
4215
+ template = seam.locale.bind(NS$1)(key);
4216
+ } catch {
4217
+ template = message(key);
4218
+ }
4219
+ return values === void 0 ? template : fill(template, values);
4220
+ };
4221
+ const settingsSeat = createSettingsSeat(ctx);
4222
+ let approvalConfirmMode = "ask";
4223
+ const approvalForm = settingsSeat.get("evolution-approval");
4224
+ const stopApprovalWatch = approvalForm?.subscribe(() => {
4225
+ approvalConfirmMode = confirmModeFrom(approvalForm.getSnapshot().user);
4226
+ });
4227
+ if (stopApprovalWatch !== void 0) ctx.effect(() => stopApprovalWatch, "evolution-history: approval confirm mode");
4228
+ const confirmMode = () => approvalConfirmMode;
4229
+ const hasView = (view) => {
4230
+ try {
4231
+ return (seam.slots.entries?.("evolution.workbench.view") ?? []).some((entry) => entry.options?.id === (view === "merge" ? "merge" : "skill-health"));
4232
+ } catch {
4233
+ return false;
4234
+ }
4235
+ };
4236
+ const pendingFace = () => ({
4237
+ t: pendingT,
4238
+ api: createApprovalApi((input, init) => fetch(input, init)),
4239
+ confirmMode,
4240
+ fallback: FALLBACKS.approval
4241
+ });
4242
+ const label = () => t("entry.label");
4243
+ seam.slots.inject("sidebar.pages", function* () {
4244
+ yield seam.slots.register({
4245
+ name: "sidebar.pages",
4246
+ id: WORKBENCH_ID,
4247
+ order: 35,
4248
+ label,
4249
+ inject: () => ({ t })
4250
+ }, WorkbenchNav);
4251
+ });
4252
+ seam.slots.inject("layout.page", function* () {
4253
+ yield seam.slots.register({
4254
+ name: "layout.page",
4255
+ key: WORKBENCH_ID,
4256
+ children: { "evolution.workbench.view": {
4257
+ kind: "list",
4258
+ scope: "root"
4259
+ } },
4260
+ inject: () => ({
4261
+ t,
4262
+ history: historyFace(),
4263
+ pending: pendingFace(),
4264
+ hasView
4265
+ })
4266
+ }, WorkbenchPage);
3649
4267
  });
3650
4268
  }
3651
4269
  //#endregion