dsh-custom-mode 1.11.6 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -61,7 +61,7 @@ try {
61
61
  const ZH = {
62
62
  "nav": "自定义模式",
63
63
  "assistant.heading": "助手",
64
- "assistant.hint": "每个助手即一个独立模式:独立的系统提示词、基础模式与插件开关。新建会话时在模式选择器中选用。",
64
+ "assistant.hint": "每个助手是一套独立模式,有自己的提示词、基础模式和插件开关。新建会话时在模式菜单里选用。",
65
65
  "assistant.empty": "还没有助手,用下面的输入框新建一个。",
66
66
  "assistant.loadingList": "正在读取助手列表…",
67
67
  "assistant.newPlaceholder": "新助手的名字(例如:写作助手)",
@@ -81,7 +81,7 @@ try {
81
81
  "btn.resetHint": "把编辑器里的内容换成出厂模板(新建助手时得到的那一份)。它同样只改草稿,点保存才落盘 —— 误点可以用「重新读取」撤销。",
82
82
  "btn.delete": "删除这个助手",
83
83
  "btn.cancel": "取消",
84
- "msg.created": "已创建。现在可以为它写系统提示词。",
84
+ "msg.created": "已创建。现在可以为它写系统提示词。新建会话的模式菜单要刷新一次才会出现它。",
85
85
  "msg.duplicated": "已复制。两份配置彼此独立。",
86
86
  "msg.deleted": "已删除。",
87
87
  "msg.createFailed": "创建失败",
@@ -120,7 +120,8 @@ try {
120
120
  "meta.versionHint": "安装时不钉版本号会受 pnpm 发布冷却期影响(默认 24 小时),可能装到较旧的版本。要换版本请按 README 的钉版本命令重装,然后重启 DSH。",
121
121
  "api.saved": "已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。",
122
122
  "api.savedWithExclusiveRows": "已保存({name},基础模式 {mode})。两套壳注册同名工具、不能同时启用 —— 已自动关掉:{rows}。新建会话即生效,当前会话保持原配置。",
123
- "api.created": "已创建「{name}」。现在可以为它写系统提示词。",
123
+ "api.created": "已创建「{name}」。现在可以为它写系统提示词。新建会话的模式菜单要刷新一次才会出现它。",
124
+ "api.savedPrompt": "已保存系统提示词({name})。下一步模型调用即生效。",
124
125
  "api.duplicated": "已复制自「{from}」。两份配置彼此独立。",
125
126
  "api.deleted": "已删除「{name}」。正在使用它的会话不受影响;新建会话时它不再出现。",
126
127
  "api.reordered": "顺序已保存:新建会话时的模式选择器按这个顺序排列。",
@@ -153,6 +154,9 @@ try {
153
154
  "warn.approvalGateMissing": "审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。",
154
155
  "warn.unresolvableRows": "有行在本机这条 DSH 线上跑不起来。平台会因此判整个模式为 broken,把它从新会话的选择器里丢掉(不报错)。点下方「按本线修复」只关掉那几行,其它选择不动。",
155
156
  "warn.presetBroken": "平台判这个模式为 broken,它会从新会话的选择器里消失 —— 而上面的「无法解析」检查解释不了原因(多半是某一行依赖的服务没起来)。点下方「按本线修复」把那几行关掉;平台自己给的原因见下面那行。",
157
+ "warn.badPromptVariable": "系统提示词里有未注册或写错的变量引用,这个模式的每次请求都会失败。改掉那一处后再保存。",
158
+ "warn.duplicateRowIds": "组成文件里有重复的行 id。开关只作用于第一处,请删掉重复的那一行。",
159
+ "warn.predicateFailed": "有一行的平台条件求值失败,这一行已按关闭处理,避免整个模式无法挂载。",
156
160
  "warn.exclusiveRowsActive": "两条互斥的壳同时开着(它们注册同名工具),平台会判整个模式为 broken 并从新会话的选择器里丢掉。关掉其中一条即可;在设置页保存一次也会自动避让。",
157
161
  "warn.baseCompositionUnavailable": "取不到本机这条 DSH 线的出厂组成:基础模式与插件开关暂时不能改(系统提示词不受影响,照常可保存)。已尝试的途径写在宿主日志里。",
158
162
  "warn.personaOffWithPrompt": "「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 所写的提示词不会生效。打开这一行,或清空提示词。",
@@ -170,7 +174,7 @@ try {
170
174
  "msg.importTooLarge": "文件太大(上限 1MB)。",
171
175
  "msg.exported": "已导出为文件。",
172
176
  "msg.exportFailed": "导出失败",
173
- "delete.title": "永久删除这个助手?",
177
+ "delete.title": "永久删除「{name}」?",
174
178
  "delete.description": "这会删除磁盘上的模式目录,连同它的系统提示词一起消失,无法撤销。正在使用它的会话不受影响;新建会话时它不再出现在选择器里。({id})",
175
179
  "delete.acknowledge": "我明白这个助手的提示词会被永久删除",
176
180
  "delete.confirm": "永久删除",
@@ -181,11 +185,16 @@ try {
181
185
  "name.placeholder": "自定义模式",
182
186
  "name.descriptionPlaceholder": "模式描述(显示在模式选择器里,可留空)",
183
187
  "mode.heading": "基础模式",
184
- "mode.hint": "基础模式只决定行集合与工具能力:本模式的身份行始终替换基础模式的那一行(提示词由本页编辑,complete: false),不继承其提示语语义。保存后新建会话即生效,无需重启。",
188
+ "mode.hint": "基础模式只决定有哪些行、有哪些工具。本模式的身份始终用这一页的提示词,不继承基础模式的提示语。保存后新建会话生效,无需重启。",
185
189
  "rows.heading": "插件开关",
190
+ "rows.filterPlaceholder": "筛选行(名称或 id)",
191
+ "cap.heading": "能力",
192
+ "cap.expand": "展开能力",
193
+ "cap.collapse": "收起能力",
194
+ "cap.hint": "基础模式决定有哪些工具。开关逐行生效。这两项从下一次新建会话开始。",
186
195
  "rows.hint": "逐行控制本模式挂载的插件。未改动的行保持官方默认(含平台判断);手动设置后以手动值为准。",
187
196
  "prompt.heading": "系统提示词",
188
- "prompt.hint": "每次模型调用前重新读取:保存后下一步生效,仅影响使用本助手的会话。「导入」读文件进编辑器(保存前不写入);「导出」存为 .md;「恢复出厂提示词」填回出厂模板(同样需保存)。",
197
+ "prompt.hint": "保存后,下一次模型调用就会用这段文字。导入只进编辑器;导出为 Markdown。恢复出厂后仍要点保存。",
189
198
  "status.enabled": "已启用",
190
199
  "status.disabled": "已停用",
191
200
  "status.changed": "已改",
@@ -195,6 +204,7 @@ try {
195
204
  "btn.saving": "处理中…",
196
205
  "btn.reload": "重新读取",
197
206
  "btn.reloadDiscard": "放弃修改并重新读取",
207
+ "btn.reloadConfirm": "放弃未保存的修改并重新读取?",
198
208
  "msg.unsaved": "有未保存的修改",
199
209
  "msg.readFailed": "读取失败",
200
210
  "msg.saveFailed": "保存失败",
@@ -230,6 +240,10 @@ try {
230
240
  "row.tool-subagent-codex.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
231
241
  "row.tool-subagent-claude-code.label": "Claude Code 子代理",
232
242
  "row.tool-subagent-claude-code.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
243
+ "row.time-context.label": "时间上下文(模型看得到时间)",
244
+ "row.time-context.note": "在合适的步骤里附上当前时间、浏览器时区与距上一条消息的间隔;默认 10 分钟内不重复注入",
245
+ "row.tool-schedule.label": "定时提醒工具",
246
+ "row.tool-schedule.note": "让 agent 创建/列出/修改/删除持久提醒;存储与投递由宿主的 schedule 服务负责",
233
247
  "row.workflow-ptc.label": "工作流引擎",
234
248
  "row.workflow-worker-thread.label": "工作流 Worker 线程",
235
249
  "row.workflow-worker-thread.note": "把工作流跑在独立的 worker 线程里",
@@ -269,14 +283,14 @@ try {
269
283
  // 同一个模式在一页里有两个名字:用户点了「Cordis 模式」药丸,读到的说明却说「创造模式」。
270
284
  "base.cordis.label": "创造模式",
271
285
  "base.cordis.note": "标准模式 + 读写运行时的 Cordis 工具集,可让 agent 自己改 harness",
272
- "base.all.label": "自定义模式",
286
+ "base.all.label": "全部出厂行",
273
287
  "base.all.note": "不继承任何单一原生模式:以标准模式的全套行为底,补上只有 PTC / 极简 / 创造模式才有的行,全部都能逐行开关"
274
288
  }
275
289
 
276
290
  const EN = {
277
291
  "nav": "Custom mode",
278
292
  "assistant.heading": "Assistants",
279
- "assistant.hint": "Each assistant is an independent mode: its own system prompt, base mode and plugin switches. Pick one in the mode picker when you start a new session.",
293
+ "assistant.hint": "Each assistant is its own mode, with its own prompt, base mode and plugin switches. Pick one from the mode menu when you start a session.",
280
294
  "assistant.empty": "No assistants yet — create one with the field below.",
281
295
  "assistant.loadingList": "Loading assistants…",
282
296
  "assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
@@ -296,7 +310,7 @@ try {
296
310
  "btn.resetHint": "Puts the shipped template back into the editor (the text a new assistant starts from). Like every other edit it only changes the draft — save to apply, or use Reload to discard.",
297
311
  "btn.delete": "Delete this assistant",
298
312
  "btn.cancel": "Cancel",
299
- "msg.created": "Created. Now you can write its system prompt.",
313
+ "msg.created": "Created. You can write its system prompt now. Refresh once before the new-session mode menu will list it.",
300
314
  "msg.duplicated": "Duplicated. The two are independent from here on.",
301
315
  "msg.deleted": "Deleted.",
302
316
  "msg.createFailed": "Could not create",
@@ -335,7 +349,8 @@ try {
335
349
  "meta.versionHint": "Installing without a pinned version is subject to pnpm’s release cooldown (24 h by default) and can land on an older release. To change version, reinstall with the pinned command from the README and restart DSH.",
336
350
  "api.saved": "Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.",
337
351
  "api.savedWithExclusiveRows": "Saved ({name}, base mode {mode}). The two shell implementations register the same tool names and cannot both be on — turned off automatically: {rows}. A new session picks it up; the current one keeps its configuration.",
338
- "api.created": "Created {name}. You can write its system prompt now.",
352
+ "api.created": "Created {name}. You can write its system prompt now. Refresh once before the new-session mode menu will list it.",
353
+ "api.savedPrompt": "System prompt saved ({name}). It applies on the next model step.",
339
354
  "api.duplicated": "Copied from {from}. The two are independent from now on.",
340
355
  "api.deleted": "Deleted {name}. Sessions already using it keep running; it no longer appears for new sessions.",
341
356
  "api.reordered": "Order saved: the new-session mode picker follows it.",
@@ -370,7 +385,10 @@ try {
370
385
  "warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
371
386
  "warn.approvalGateMissing": "The approval gate is off: this DSH build has no tools/pre-execute event, so in-session prompt rewrites do not ask for approval. See the details.",
372
387
  "warn.unresolvableRows": "Some rows cannot be resolved on this DSH line: the platform marks the whole mode broken and silently drops it from the new-session picker. Click Fix for this line — it only turns those rows off and leaves your other choices alone.",
373
- "warn.presetBroken": "The platform marks this mode broken, so it disappears from the new-session picker — and the \"unresolvable\" check above cannot explain why (usually a row whose service or engine never started). Click Fix for this line to turn those rows off. The host's own reason is in Details.",
388
+ "warn.presetBroken": "The platform marks this mode broken, so it disappears from the new-session picker — and the \"unresolvable\" check above cannot explain why (usually a row whose service or engine never started). Click Fix for this line to turn those rows off. The host's own reason is on the line below.",
389
+ "warn.badPromptVariable": "The system prompt contains an unknown or invalid variable reference, so every request in this mode fails. Fix it, then save.",
390
+ "warn.duplicateRowIds": "The composition repeats a row id. A switch applies only to the first copy. Remove the duplicate.",
391
+ "warn.predicateFailed": "A platform condition failed to evaluate. That row is treated as off so the mode can still mount.",
374
392
  "warn.exclusiveRowsActive": "Both shells are on, and they register the same tool names: the platform marks the whole mode broken and silently drops it from the new-session picker. Turn one of them off — saving from the settings page also resolves it automatically.",
375
393
  "warn.noName": "No name: the mode picker will show the directory id (e.g. custom).",
376
394
  "warn.baseCompositionUnavailable": "This DSH line exposes no shipped composition: the base mode and the plugin switches cannot be changed right now (the system prompt is unaffected and still saves). The routes that were tried are in the host log.",
@@ -385,7 +403,7 @@ try {
385
403
  "msg.importTooLarge": "That file is too large (1 MB limit).",
386
404
  "msg.exported": "Exported to a file.",
387
405
  "msg.exportFailed": "Export failed",
388
- "delete.title": "Delete this assistant permanently?",
406
+ "delete.title": "Delete \"{name}\" permanently?",
389
407
  "delete.description": "This removes the mode directory from disk, system prompt included, and cannot be undone. Sessions already using it keep running; new sessions no longer offer it. ({id})",
390
408
  "delete.acknowledge": "I understand this assistant's prompt will be deleted permanently",
391
409
  "delete.confirm": "Delete permanently",
@@ -396,11 +414,16 @@ try {
396
414
  "name.placeholder": "Custom mode",
397
415
  "name.descriptionPlaceholder": "Mode description (shown in the mode picker, optional)",
398
416
  "mode.heading": "Base mode",
399
- "mode.hint": "The base decides the row set and the tool abilities: this mode always replaces the base's persona row with its own reader (complete: false), so the base's prompt semantics are not inherited. A save applies to the next new session; no restart needed.",
417
+ "mode.hint": "The base decides which rows and tools this mode has. Its identity is always the prompt on this page, not the base mode's own wording. A save applies to the next new session; no restart is required.",
400
418
  "rows.heading": "Plugin switches",
419
+ "rows.filterPlaceholder": "Filter rows by name or id",
420
+ "cap.heading": "Capabilities",
421
+ "cap.expand": "Show capabilities",
422
+ "cap.collapse": "Hide capabilities",
423
+ "cap.hint": "The base mode chooses the tools. Each switch applies to one row. Both start on the next new session.",
401
424
  "rows.hint": "Control row by row which plugins this mode mounts. Untouched rows keep the official default (platform conditions included); an explicit choice wins.",
402
425
  "prompt.heading": "System prompt",
403
- "prompt.hint": "This text is re-read before every model call, so a save applies on the next step and only affects sessions on this assistant. \"Import\" reads a file into the editor (nothing is written until you save); \"Export\" saves the current text as a .md file; \"Reset to factory prompt\" puts the shipped template back into the editor (also saved only when you save).",
426
+ "prompt.hint": "After you save, the next model step uses this text. Import only fills the editor. Export writes Markdown. Reset to factory still needs a save.",
404
427
  "status.enabled": "Enabled",
405
428
  "status.disabled": "Disabled",
406
429
  "status.changed": "changed",
@@ -410,6 +433,7 @@ try {
410
433
  "btn.saving": "Working…",
411
434
  "btn.reload": "Reload",
412
435
  "btn.reloadDiscard": "Discard edits and reload",
436
+ "btn.reloadConfirm": "Discard unsaved edits and reload?",
413
437
  "msg.unsaved": "Unsaved changes",
414
438
  "msg.readFailed": "Load failed",
415
439
  "msg.saveFailed": "Save failed",
@@ -445,6 +469,10 @@ try {
445
469
  "row.tool-subagent-codex.note": "Needs its Bundle installed first; the shipped state is in the details",
446
470
  "row.tool-subagent-claude-code.label": "Claude Code subagent",
447
471
  "row.tool-subagent-claude-code.note": "Needs its Bundle installed first; the shipped state is in the details",
472
+ "row.time-context.label": "Time context (the model can see the clock)",
473
+ "row.time-context.note": "On eligible steps, appends the current time, the browser zone and the gap since the previous model-visible message; 10-minute minimum interval by default",
474
+ "row.tool-schedule.label": "Scheduled reminders tool",
475
+ "row.tool-schedule.note": "Lets the agent create, list, edit and delete durable reminders; the host schedule service owns storage and delivery",
448
476
  "row.workflow-ptc.label": "Workflow engine",
449
477
  "row.workflow-worker-thread.label": "Workflow worker thread",
450
478
  "row.workflow-worker-thread.note": "Runs workflows on a separate worker thread",
@@ -482,7 +510,7 @@ try {
482
510
  "base.minimal.note": "Shell and terminal only, 7 rows; no files, search, skills or subagents. The system prompt is not minimal's either (this mode replaces the persona row)",
483
511
  "base.cordis.label": "Creator mode",
484
512
  "base.cordis.note": "Standard plus the Cordis toolset, letting the agent modify its own harness",
485
- "base.all.label": "Custom mode",
513
+ "base.all.label": "All shipped rows",
486
514
  "base.all.note": "Inherits no single shipped mode: standard's full row list plus the rows only PTC / Minimal / Creator declare, every one of them switchable"
487
515
  }
488
516
 
@@ -500,14 +528,22 @@ try {
500
528
  }
501
529
 
502
530
  function activeLanguage(locale) {
531
+ let id = ""
503
532
  try {
504
533
  const snapshot = locale !== undefined && typeof locale.getLocale === "function" ? locale.getLocale() : undefined
505
- const id = snapshot !== undefined && typeof snapshot.active === "string" ? snapshot.active : ""
506
- if (id !== "") return id.toLowerCase().startsWith("zh") ? "zh" : "en"
534
+ if (snapshot !== undefined && typeof snapshot.active === "string") id = snapshot.active
507
535
  } catch (error) {
508
- /* a shell without a readable snapshot falls through to the default */
536
+ /* a shell without a readable snapshot falls through */
509
537
  }
510
- return "zh"
538
+ if (id === "" && typeof navigator !== "undefined" && typeof navigator.language === "string") id = navigator.language
539
+ const norm = id.toLowerCase()
540
+ if (norm.startsWith("zh")) return "zh"
541
+ if (norm.startsWith("en")) return "en"
542
+ const primary = norm.split("-")[0]
543
+ if (Object.prototype.hasOwnProperty.call(TRANSLATIONS, norm)) return norm
544
+ if (primary !== "" && Object.prototype.hasOwnProperty.call(TRANSLATIONS, primary)) return primary
545
+ // 壳目前内置 zh / en,语言包可以再注册别的 id。没有对应词典时用英文,避免整页中文或裸键。
546
+ return "en"
511
547
  }
512
548
 
513
549
  /**
@@ -685,7 +721,7 @@ try {
685
721
  * grids, the status line).
686
722
  */
687
723
  const CSS = [
688
- ".cpfe{--g:8px;display:flex;flex-direction:column;gap:24px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px}",
724
+ ".cpfe{--g:8px;display:flex;flex-direction:column;gap:24px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px;font:inherit;color:inherit}",
689
725
  // 分节标题:度量抄官方设置面板里的同类文本。
690
726
  // 实测(**0.1.7-rc.2 与 0.2.0-rc.2 各量一次,结论相同**):官方 `_title`(设置行标题)
691
727
  // 与 `groupTitle`(插件页分组标题)都是 14px/22px **w400**;官方唯一的 14px w500 是插件
@@ -702,7 +738,7 @@ try {
702
738
  ".cpfe-desc{box-sizing:border-box;min-height:56px;max-height:200px;resize:vertical;padding:8px 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;margin-bottom:8px}",
703
739
  ".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
704
740
  ".cpfe-note{display:block;margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
705
- ".cpfe-mono{display:block;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
741
+ ".cpfe-mono{display:block;margin:0;font-family:var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
706
742
  ".cpfe-pills{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
707
743
  ".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
708
744
  // 壳的菜单浮层靠一个独立的 backing 元素上色(实测在 0.1.7-rc.2 + headless 下那块是透明的,
@@ -724,7 +760,7 @@ try {
724
760
  ".cpfe-row-open{border-color:var(--dsw-alias-border-l2)}",
725
761
  ".cpfe-row-detail{display:flex;flex-direction:column;gap:4px;margin:0 0 12px;padding:0 0 0 16px;border:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
726
762
  ".cpfe-detail-line{display:flex;gap:8px;min-width:0}",
727
- ".cpfe-detail-key{flex:0 0 62px;color:var(--dsw-alias-label-secondary)}",
763
+ ".cpfe-detail-key{flex:0 0 9.5em;color:var(--dsw-alias-label-secondary)}",
728
764
  ".cpfe-detail-value{min-width:0;overflow-wrap:anywhere}",
729
765
  ".cpfe-sec-head{display:flex;align-items:center;justify-content:space-between;gap:var(--g);flex-wrap:wrap}",
730
766
  ".cpfe-newinput{flex:1;min-width:200px}",
@@ -739,8 +775,11 @@ try {
739
775
  ".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:0;padding:16px 0;border:0;border-radius:0;background:none}",
740
776
  ".cpfe-row-meta{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}",
741
777
  ".cpfe-row-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap;height:18px}",
742
- ".cpfe-editor{box-sizing:border-box;width:100%;min-height:240px;resize:vertical;padding:12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:20px}",
778
+ ".cpfe-editor{box-sizing:border-box;width:100%;min-height:240px;resize:vertical;padding:12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:13px;line-height:20px}",
743
779
  ".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap}",
780
+ ".cpfe-savebar{position:sticky;bottom:0;z-index:2;padding:8px 0;background:var(--dsw-alias-bg-base)}",
781
+ ".cpfe-cap-toggle{appearance:none;cursor:pointer;border:0;background:transparent;padding:0;font:inherit;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
782
+ ".cpfe-filter{box-sizing:border-box;width:100%;height:36px;margin:0 0 8px;padding:0 12px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
744
783
  ".cpfe-status{font-size:13px;line-height:20px}",
745
784
  ".cpfe-ok{color:var(--dsw-alias-label-secondary)}",
746
785
  ".cpfe-err{color:var(--dsw-alias-state-error-primary)}",
@@ -748,7 +787,7 @@ try {
748
787
  ".cpfe-danger{color:var(--dsw-alias-state-error-primary)}",
749
788
  ".cpfe-meta-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0}",
750
789
  ".cpfe-version{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
751
- ".cpfe-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
790
+ ".cpfe-path{font-family:var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
752
791
  // ── fallback-path controls (unused when the shell provides the atoms) ──
753
792
  ".cpfe-btn{appearance:none;cursor:pointer;padding:0 14px;height:36px;border-radius:var(--dsw-radius-md);border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px}",
754
793
  ".cpfe-btn:disabled{opacity:.4;cursor:not-allowed}",
@@ -762,7 +801,7 @@ try {
762
801
  // 光拦状态还不够:DOM 会先显示用户敲进去的字/翻过去的开关,等下一次重渲染才被拉回来 ——
763
802
  // 那就是"我明明改了,它自己弹回去了"。`pointer-events` 让点击根本到不了控件,
764
803
  // 视觉上也给一个明确的"此刻不能动"。
765
- ".cpfe-busy .cpfe-switch,.cpfe-busy .cpfe-row-toggle,.cpfe-busy .cpfe-input,.cpfe-busy .cpfe-field,.cpfe-busy .cpfe-editor{pointer-events:none;opacity:.65}",
804
+ ".cpfe-busy .cpfe-switch,.cpfe-busy .cpfe-row-switch,.cpfe-busy .cpfe-row-toggle,.cpfe-busy .cpfe-input,.cpfe-busy .cpfe-field,.cpfe-busy .cpfe-editor,.cpfe-busy .cpfe-pill,.cpfe-busy .cpfe-selector{pointer-events:none;opacity:.65}",
766
805
  ".cpfe-busy .cpfe-editor{cursor:progress}",
767
806
  ".cpfe-tag{font-size:12px;line-height:18px;padding:0 5px;border-radius:4px;border:.5px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}",
768
807
  ".cpfe-tag-success,.cpfe-tag-info{color:var(--dsw-alias-state-success-primary)}",
@@ -877,6 +916,7 @@ try {
877
916
  // 改动历史:列表来自 state(只有元数据),正文点「载入这一版」时按需取。
878
917
  history: Array.isArray(state.history) ? state.history : [],
879
918
  historyPick: "",
919
+ broken: typeof state.broken === "string" && state.broken !== "" ? state.broken : null,
880
920
  // 「配置了却不生效」的告警码;文案按当前语言渲染。
881
921
  warnings: Array.isArray(state.warnings) ? state.warnings : [],
882
922
  }
@@ -927,6 +967,20 @@ try {
927
967
  * this change: 33 rows at 274px wide and 84–117px tall with a bare `tool-bash` line each, i.e. uneven
928
968
  * heights and a lot of vertical noise for information most users never read.
929
969
  */
970
+ function filterRows(rows, query, translateRow) {
971
+ const needle = String(query || "").trim().toLowerCase()
972
+ if (needle === "") return rows
973
+ const out = []
974
+ for (const row of rows) {
975
+ const title = String(translateRow("row." + row.id + ".label", row.label)).toLowerCase()
976
+ const self = title.includes(needle) || row.id.toLowerCase().includes(needle)
977
+ const children = Array.isArray(row.children) ? row.children : []
978
+ const kids = self ? children : filterRows(children, query, translateRow)
979
+ if (self || kids.length > 0) out.push(self ? row : { ...row, children: kids })
980
+ }
981
+ return out
982
+ }
983
+
930
984
  function RowList(props) {
931
985
  const { rows, overrides, onToggle, expanded, onToggleExpand, depth, t } = props
932
986
  return react.createElement(
@@ -990,6 +1044,7 @@ try {
990
1044
  ),
991
1045
  react.createElement(A.Switch, {
992
1046
  checked: effective,
1047
+ disabled: props.disabled === true,
993
1048
  onChange: (next) => onToggle(row.id, next),
994
1049
  // `Switch` renders NO text: its `label` is the accessible name only, so the
995
1050
  // visible title is drawn next to it (an unlabelled toggle is unusable).
@@ -1060,6 +1115,7 @@ try {
1060
1115
  onToggleExpand: onToggleExpand,
1061
1116
  depth: depth + 1,
1062
1117
  t: t,
1118
+ disabled: props.disabled === true,
1063
1119
  })
1064
1120
  : null,
1065
1121
  )
@@ -1107,10 +1163,19 @@ try {
1107
1163
  * 词典里没有这个 id 时(旧宿主 / 未来新增)退回机器名,宁可难看也不要空。
1108
1164
  */
1109
1165
  const params = result.params
1110
- const shown =
1111
- params !== null && typeof params === "object" && typeof params.mode === "string"
1112
- ? { ...params, mode: translate("base." + params.mode + ".label", params.mode, shellT) }
1113
- : params
1166
+ let shown = params
1167
+ if (params !== null && typeof params === "object") {
1168
+ shown = { ...params }
1169
+ if (typeof params.mode === "string") {
1170
+ shown.mode = translate("base." + params.mode + ".label", params.mode, shellT)
1171
+ }
1172
+ if (typeof params.rows === "string") {
1173
+ shown.rows = params.rows
1174
+ .split(",")
1175
+ .map((id) => translate("row." + id.trim() + ".label", id.trim(), shellT))
1176
+ .join(", ")
1177
+ }
1178
+ }
1114
1179
  return fillPlaceholders(template, shown)
1115
1180
  }
1116
1181
  }
@@ -1141,6 +1206,9 @@ try {
1141
1206
  // 两个官方风格的下拉(壳的 `Menu`):助手切换与改动历史。壳不提供 Menu 时退回原生控件。
1142
1207
  const [assistantOpen, setAssistantOpen] = react.useState(false)
1143
1208
  const [historyOpen, setHistoryOpen] = react.useState(false)
1209
+ const [listFailed, setListFailed] = react.useState(false)
1210
+ const [capOpen, setCapOpen] = react.useState(false)
1211
+ const [rowQuery, setRowQuery] = react.useState("")
1144
1212
  /** 本机这条线上有无法解析的行时,一键把它们关掉(服务端复用保存同一条排版手术)。 */
1145
1213
  const repairRowsNow = async () => {
1146
1214
  if (draft === null) return
@@ -1250,10 +1318,12 @@ try {
1250
1318
  const result = await fetchList()
1251
1319
  if (result === null || result === undefined || result.ok !== true) {
1252
1320
  setFailed(true)
1321
+ setListFailed(true)
1253
1322
  setStatus(apiText(result, "msg.readFailed"))
1254
1323
  return
1255
1324
  }
1256
1325
  const assistants = Array.isArray(result.assistants) ? result.assistants : []
1326
+ setListFailed(false)
1257
1327
  setList(assistants)
1258
1328
  const wanted =
1259
1329
  typeof preferId === "string" && preferId !== ""
@@ -1343,7 +1413,18 @@ try {
1343
1413
  }
1344
1414
 
1345
1415
  const pick = async (id) => {
1346
- if (id === selected) return
1416
+ if (id === selected) {
1417
+ setBusy(true)
1418
+ try {
1419
+ await open(id, { force: true })
1420
+ } catch (error) {
1421
+ setFailed(true)
1422
+ setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
1423
+ } finally {
1424
+ setBusy(false)
1425
+ }
1426
+ return
1427
+ }
1347
1428
  setSelected(id)
1348
1429
  setDeleteOpen(false)
1349
1430
  setAcknowledged(false)
@@ -1361,6 +1442,7 @@ try {
1361
1442
 
1362
1443
  /** Create a new assistant, or duplicate the selected one. */
1363
1444
  const create = async (options) => {
1445
+ if (busy === true) return
1364
1446
  const from = options !== null && typeof options === "object" ? options.from : undefined
1365
1447
  const name =
1366
1448
  options !== null && typeof options === "object" && typeof options.name === "string"
@@ -1541,7 +1623,7 @@ try {
1541
1623
  * confirmation instead of losing the guardrail entirely.
1542
1624
  */
1543
1625
  const askDelete = () => {
1544
- if (typeof A.RiskConfirmation === "function") {
1626
+ if (renderable(A.RiskConfirmation)) {
1545
1627
  setAcknowledged(false)
1546
1628
  setDeleteOpen(true)
1547
1629
  return
@@ -1569,7 +1651,13 @@ try {
1569
1651
  })
1570
1652
  if (result !== null && result !== undefined && result.ok === true) {
1571
1653
  setFailed(false)
1572
- setStatus(apiText(result, "msg.saved"))
1654
+ const compositionChanged =
1655
+ entry !== undefined &&
1656
+ (draft.mode !== entry.saved.mode ||
1657
+ JSON.stringify(draft.overrides) !== JSON.stringify(entry.saved.overrides))
1658
+ const spoken =
1659
+ result.code === "saved" && compositionChanged !== true ? { ...result, code: "savedPrompt" } : result
1660
+ setStatus(apiText(spoken, "msg.saved"))
1573
1661
  // The server normalises what it stores (trimmed name, recomputed overrides),
1574
1662
  // so the just-saved draft is replaced by what it actually wrote. Without this
1575
1663
  // the page would show 「已保存」 and 「未保存」 at the same time.
@@ -1612,7 +1700,14 @@ try {
1612
1700
  react.createElement("h2", { className: "cpfe-h" }, t("assistant.heading")),
1613
1701
  react.createElement(SectionHint, { text: t("assistant.hint") }),
1614
1702
  list === null
1615
- ? react.createElement("p", { className: "cpfe-sub" }, t("assistant.loadingList"))
1703
+ ? react.createElement(
1704
+ "div",
1705
+ null,
1706
+ react.createElement("p", { className: "cpfe-sub" }, listFailed ? t("msg.readFailed") : t("assistant.loadingList")),
1707
+ listFailed
1708
+ ? react.createElement(A.Button, { disabled: busy, onClick: () => reload("") }, t("btn.reload"))
1709
+ : null,
1710
+ )
1616
1711
  : assistants.length === 0
1617
1712
  ? react.createElement("p", { className: "cpfe-sub" }, t("assistant.empty"))
1618
1713
  : react.createElement(
@@ -1708,7 +1803,7 @@ try {
1708
1803
  placeholder: t("assistant.newPlaceholder"),
1709
1804
  "aria-label": t("assistant.newPlaceholder"),
1710
1805
  onKeyDown: (event) => {
1711
- if (event.key === "Enter") create()
1806
+ if (event.key === "Enter" && busy !== true) create()
1712
1807
  },
1713
1808
  onChange: (event) => {
1714
1809
  setNewName(event.target.value)
@@ -1726,6 +1821,60 @@ try {
1726
1821
  busy ? t("btn.creating") : t("btn.create"),
1727
1822
  ),
1728
1823
  ),
1824
+ editorReady
1825
+ ? react.createElement(
1826
+ "div",
1827
+ { className: "cpfe-actions" },
1828
+ react.createElement(
1829
+ A.Button,
1830
+ {
1831
+ variant: "outline",
1832
+ size: "sm",
1833
+ icon: A.IconChevronUpOutline14 === undefined ? null : react.createElement(A.IconChevronUpOutline14),
1834
+ disabled: busy || position <= 0,
1835
+ onClick: () => move("up"),
1836
+ },
1837
+ t("btn.moveUp"),
1838
+ ),
1839
+ react.createElement(
1840
+ A.Button,
1841
+ {
1842
+ variant: "outline",
1843
+ size: "sm",
1844
+ icon: A.IconChevronDownOutline14 === undefined ? null : react.createElement(A.IconChevronDownOutline14),
1845
+ disabled: busy || position === -1 || position >= assistants.length - 1,
1846
+ onClick: () => move("down"),
1847
+ },
1848
+ t("btn.moveDown"),
1849
+ ),
1850
+ react.createElement(
1851
+ A.Button,
1852
+ {
1853
+ variant: "outline",
1854
+ size: "sm",
1855
+ icon: A.IconCopyOutline16 === undefined ? null : react.createElement(A.IconCopyOutline16),
1856
+ disabled: busy,
1857
+ onClick: duplicate,
1858
+ },
1859
+ t("btn.duplicate"),
1860
+ ),
1861
+ react.createElement(
1862
+ A.Button,
1863
+ {
1864
+ variant: "ghost",
1865
+ size: "sm",
1866
+ className: "cpfe-danger",
1867
+ icon: A.IconTrashOutline16 === undefined ? null : react.createElement(A.IconTrashOutline16),
1868
+ disabled: busy,
1869
+ onClick: askDelete,
1870
+ },
1871
+ t("btn.delete"),
1872
+ ),
1873
+ )
1874
+ : null,
1875
+ typeof (assistants.find((item) => item.id === selected) || {}).broken === "string"
1876
+ ? react.createElement("p", { className: "cpfe-note" }, t("assistant.brokenHint"))
1877
+ : null,
1729
1878
  react.createElement("p", { className: "cpfe-note" }, t("msg.readOnlyHint")),
1730
1879
  )
1731
1880
 
@@ -1758,55 +1907,6 @@ try {
1758
1907
  "aria-label": t("name.descriptionPlaceholder"),
1759
1908
  onChange: (event) => update({ description: event.target.value }),
1760
1909
  }),
1761
- react.createElement(
1762
- "div",
1763
- { className: "cpfe-actions" },
1764
- react.createElement(
1765
- A.Button,
1766
- {
1767
- variant: "outline",
1768
- size: "sm",
1769
- icon: A.IconChevronUpOutline14 === undefined ? null : react.createElement(A.IconChevronUpOutline14),
1770
- disabled: busy || position <= 0,
1771
- onClick: () => move("up"),
1772
- },
1773
- t("btn.moveUp"),
1774
- ),
1775
- react.createElement(
1776
- A.Button,
1777
- {
1778
- variant: "outline",
1779
- size: "sm",
1780
- icon: A.IconChevronDownOutline14 === undefined ? null : react.createElement(A.IconChevronDownOutline14),
1781
- disabled: busy || position === -1 || position >= assistants.length - 1,
1782
- onClick: () => move("down"),
1783
- },
1784
- t("btn.moveDown"),
1785
- ),
1786
- react.createElement(
1787
- A.Button,
1788
- {
1789
- variant: "outline",
1790
- size: "sm",
1791
- icon: A.IconCopyOutline16 === undefined ? null : react.createElement(A.IconCopyOutline16),
1792
- disabled: busy,
1793
- onClick: duplicate,
1794
- },
1795
- t("btn.duplicate"),
1796
- ),
1797
- react.createElement(
1798
- A.Button,
1799
- {
1800
- variant: "ghost",
1801
- size: "sm",
1802
- className: "cpfe-danger",
1803
- icon: A.IconTrashOutline16 === undefined ? null : react.createElement(A.IconTrashOutline16),
1804
- disabled: busy,
1805
- onClick: askDelete,
1806
- },
1807
- t("btn.delete"),
1808
- ),
1809
- ),
1810
1910
  ),
1811
1911
  react.createElement(
1812
1912
  "section",
@@ -1823,7 +1923,13 @@ try {
1823
1923
  payload.modes.map((mode) =>
1824
1924
  react.createElement(
1825
1925
  A.Pill,
1826
- { key: mode.id, active: draft.mode === mode.id, onClick: () => pickMode(mode.id) },
1926
+ {
1927
+ key: mode.id,
1928
+ active: draft.mode === mode.id,
1929
+ disabled: busy,
1930
+ "aria-pressed": draft.mode === mode.id,
1931
+ onClick: () => pickMode(mode.id),
1932
+ },
1827
1933
  t("base." + mode.id + ".label", mode.label),
1828
1934
  ),
1829
1935
  ),
@@ -1851,15 +1957,30 @@ try {
1851
1957
  react.createElement(SectionHint, { text: t("rows.hint") }),
1852
1958
  draft.baseUnavailable !== null
1853
1959
  ? react.createElement("p", { className: "cpfe-note" }, t("rows.unavailable"))
1854
- : react.createElement(RowList, {
1855
- expanded: expandedRows,
1856
- onToggleExpand: toggleRowExpanded,
1857
- rows: payload.rows,
1858
- overrides: draft.overrides,
1859
- onToggle: (id, next) => update({ overrides: { ...draft.overrides, [id]: next } }),
1860
- depth: 0,
1861
- t: t,
1862
- }),
1960
+ : react.createElement(
1961
+ "div",
1962
+ null,
1963
+ react.createElement("input", {
1964
+ className: "cpfe-filter",
1965
+ value: rowQuery,
1966
+ placeholder: t("rows.filterPlaceholder"),
1967
+ "aria-label": t("rows.filterPlaceholder"),
1968
+ onChange: (event) => setRowQuery(event.target.value),
1969
+ }),
1970
+ react.createElement(RowList, {
1971
+ expanded: expandedRows,
1972
+ onToggleExpand: toggleRowExpanded,
1973
+ rows: filterRows(payload.rows, rowQuery, t),
1974
+ overrides: draft.overrides,
1975
+ onToggle: (id, next) => {
1976
+ if (draft.mode !== savedMode) return
1977
+ update({ overrides: { ...draft.overrides, [id]: next } })
1978
+ },
1979
+ depth: 0,
1980
+ t: t,
1981
+ disabled: busy === true || draft.mode !== savedMode,
1982
+ }),
1983
+ ),
1863
1984
  ),
1864
1985
  react.createElement(
1865
1986
  "section",
@@ -2076,10 +2197,10 @@ try {
2076
2197
  ),
2077
2198
  ),
2078
2199
  ),
2079
- ...editorSections,
2200
+ ...(editorReady ? editorSections.filter((section) => section.key === "name" || section.key === "prompt") : []),
2080
2201
  react.createElement(
2081
2202
  "div",
2082
- { className: "cpfe-bar" },
2203
+ { className: "cpfe-bar cpfe-savebar" },
2083
2204
  react.createElement(
2084
2205
  A.Button,
2085
2206
  {
@@ -2095,11 +2216,14 @@ try {
2095
2216
  {
2096
2217
  variant: "outline",
2097
2218
  icon: A.IconRefreshOutline16 === undefined ? null : react.createElement(A.IconRefreshOutline16),
2098
- disabled: busy || editorReady === false,
2219
+ disabled: busy,
2099
2220
  // Explicitly destructive of the local draft, so it says so while there is
2100
2221
  // one: switching assistants never discards edits, and this button is the
2101
2222
  // one place that does.
2102
- onClick: () => reload(selected, { discard: true }),
2223
+ onClick: () => {
2224
+ if (dirty && typeof window !== "undefined" && window.confirm(t("btn.reloadConfirm")) !== true) return
2225
+ reload(selected, { discard: true })
2226
+ },
2103
2227
  },
2104
2228
  dirty ? t("btn.reloadDiscard") : t("btn.reload"),
2105
2229
  ),
@@ -2119,13 +2243,32 @@ try {
2119
2243
  react.createElement("span", { className: "cpfe-path" }, editorReady ? payload.compositionPath : ""),
2120
2244
  ),
2121
2245
  ),
2246
+ editorReady
2247
+ ? react.createElement(
2248
+ "section",
2249
+ null,
2250
+ react.createElement(
2251
+ "button",
2252
+ {
2253
+ type: "button",
2254
+ "data-cap": "toggle",
2255
+ className: "cpfe-h",
2256
+ "aria-expanded": capOpen,
2257
+ onClick: () => setCapOpen((open) => open !== true),
2258
+ },
2259
+ (capOpen ? t("cap.collapse") : t("cap.expand")) + " · " + t("cap.heading"),
2260
+ ),
2261
+ react.createElement(SectionHint, { text: t("cap.hint") }),
2262
+ capOpen ? editorSections.filter((section) => section.key === "mode" || section.key === "rows") : null,
2263
+ )
2264
+ : null,
2122
2265
  // The confirmation is a portal: rendering it here keeps every piece of this page's
2123
2266
  // state in one component.
2124
- typeof A.RiskConfirmation === "function" && editorReady
2267
+ renderable(A.RiskConfirmation) && editorReady
2125
2268
  ? react.createElement(A.RiskConfirmation, {
2126
2269
  open: deleteOpen,
2127
- title: t("delete.title"),
2128
- description: fillPlaceholders(t("delete.description"), { id: draft.id }),
2270
+ title: fillPlaceholders(t("delete.title"), { name: draft.name || draft.id }),
2271
+ description: fillPlaceholders(t("delete.description"), { id: draft.id, name: draft.name || draft.id }),
2129
2272
  acknowledgeLabel: t("delete.acknowledge"),
2130
2273
  cancelLabel: t("btn.cancel"),
2131
2274
  closeLabel: t("delete.close"),