dsh-custom-mode 1.11.5 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -61,11 +61,10 @@ 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": "新助手的名字(例如:写作助手)",
68
- "assistant.meta": "标识",
69
68
  "assistant.copyName": "「{name}」副本",
70
69
  "assistant.broken": "本机无法启动",
71
70
  "assistant.brokenRows": "{count} 行在本机不可用",
@@ -82,8 +81,8 @@ try {
82
81
  "btn.resetHint": "把编辑器里的内容换成出厂模板(新建助手时得到的那一份)。它同样只改草稿,点保存才落盘 —— 误点可以用「重新读取」撤销。",
83
82
  "btn.delete": "删除这个助手",
84
83
  "btn.cancel": "取消",
85
- "msg.created": "已创建。现在可以为它写系统提示词。",
86
- "msg.duplicated": "已复制。两份从此各改各的。",
84
+ "msg.created": "已创建。现在可以为它写系统提示词。新建会话的模式菜单要刷新一次才会出现它。",
85
+ "msg.duplicated": "已复制。两份配置彼此独立。",
87
86
  "msg.deleted": "已删除。",
88
87
  "msg.createFailed": "创建失败",
89
88
  "msg.deleteFailed": "删除失败",
@@ -99,16 +98,9 @@ try {
99
98
  "api.alreadyLast": "「{name}」已经在最后面。",
100
99
  "api.badVariableName": "变量引用的写法不合法:{variable} 里的名字只能用小写字母、数字、下划线,且以字母开头。要写字面量花括号,请用单个左花括号,或不闭合的双左花括号。",
101
100
  "api.unknownVariable": "{variable} 不是已注册的变量,渲染会报错并让本模式每个请求都失败。可用:{known}。",
102
- "assistant.short": "每个助手是一个独立模式:自己的系统提示词、基础模式与插件开关。",
103
- "name.short": "改名只影响显示,内部标识与已有会话不受影响。",
104
- "mode.short": "底子决定「行集合」与工具能力;persona 行始终由本模式替换。",
105
101
  "mode.unavailable": "本机这条 DSH 线没有可读的出厂组成,基础模式暂时不能切换。",
106
102
  "rows.unavailable": "出厂组成取不到,插件开关暂时不能编辑;磁盘上原有的行状态仍然生效。",
107
- "mode.pendingRows": "底子已改为「{mode}」:保存后,下面的行列表会按新底子重算。",
108
- "rows.short": "逐行控制挂载哪些插件;没拨过的行保持官方默认。",
109
- "prompt.short": "这段文本就是本模式的系统提示词,保存后下一步生效。",
110
- "aria.expandHint": "展开完整说明",
111
- "aria.collapseHint": "收起完整说明",
103
+ "mode.pendingRows": "基础模式已改为「{mode}」:保存后,下面的行列表会按新的基础模式重算。",
112
104
  "aria.expand": "展开详情",
113
105
  "aria.collapse": "收起详情",
114
106
  "detail.id": "行 id",
@@ -128,8 +120,9 @@ try {
128
120
  "meta.versionHint": "安装时不钉版本号会受 pnpm 发布冷却期影响(默认 24 小时),可能装到较旧的版本。要换版本请按 README 的钉版本命令重装,然后重启 DSH。",
129
121
  "api.saved": "已保存({name},基础模式 {mode})。新建会话即生效,当前会话保持原配置。",
130
122
  "api.savedWithExclusiveRows": "已保存({name},基础模式 {mode})。两套壳注册同名工具、不能同时启用 —— 已自动关掉:{rows}。新建会话即生效,当前会话保持原配置。",
131
- "api.created": "已创建「{name}」。现在可以为它写系统提示词。",
132
- "api.duplicated": "已复制自「{from}」。两份从此各改各的。",
123
+ "api.created": "已创建「{name}」。现在可以为它写系统提示词。新建会话的模式菜单要刷新一次才会出现它。",
124
+ "api.savedPrompt": "已保存系统提示词({name})。下一步模型调用即生效。",
125
+ "api.duplicated": "已复制自「{from}」。两份配置彼此独立。",
133
126
  "api.deleted": "已删除「{name}」。正在使用它的会话不受影响;新建会话时它不再出现。",
134
127
  "api.reordered": "顺序已保存:新建会话时的模式选择器按这个顺序排列。",
135
128
  "api.nameRequired": "请先给新助手起个名字。",
@@ -161,11 +154,12 @@ try {
161
154
  "warn.approvalGateMissing": "审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。",
162
155
  "warn.unresolvableRows": "有行在本机这条 DSH 线上跑不起来。平台会因此判整个模式为 broken,把它从新会话的选择器里丢掉(不报错)。点下方「按本线修复」只关掉那几行,其它选择不动。",
163
156
  "warn.presetBroken": "平台判这个模式为 broken,它会从新会话的选择器里消失 —— 而上面的「无法解析」检查解释不了原因(多半是某一行依赖的服务没起来)。点下方「按本线修复」把那几行关掉;平台自己给的原因见下面那行。",
157
+ "warn.badPromptVariable": "系统提示词里有未注册或写错的变量引用,这个模式的每次请求都会失败。改掉那一处后再保存。",
158
+ "warn.duplicateRowIds": "组成文件里有重复的行 id。开关只作用于第一处,请删掉重复的那一行。",
159
+ "warn.predicateFailed": "有一行的平台条件求值失败,这一行已按关闭处理,避免整个模式无法挂载。",
164
160
  "warn.exclusiveRowsActive": "两条互斥的壳同时开着(它们注册同名工具),平台会判整个模式为 broken 并从新会话的选择器里丢掉。关掉其中一条即可;在设置页保存一次也会自动避让。",
165
161
  "warn.baseCompositionUnavailable": "取不到本机这条 DSH 线的出厂组成:基础模式与插件开关暂时不能改(系统提示词不受影响,照常可保存)。已尝试的途径写在宿主日志里。",
166
- "warn.approvalGateMissing.label": "审批闸门未启用",
167
- "warn.approvalGateMissing.hint": "这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。设置页不受影响;要恢复保护请升级 DSH,或把「custom_prompt 工具」那一行关掉。",
168
- "warn.personaOffWithPrompt": "「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 你写的提示词现在不起作用。要么打开这一行,要么清空提示词。",
162
+ "warn.personaOffWithPrompt": "「身份(系统提示词)」这一行是关的,所以 prompt.md 不会被注入 —— 所写的提示词不会生效。打开这一行,或清空提示词。",
169
163
  "warn.toolOff": "「custom_prompt 工具」这一行是关的:会话里无法让 agent 改提示词,只能在本页改。",
170
164
  "warn.noDescription": "没有描述:新建会话的模式选择器里会显示成「暂无描述」。",
171
165
  "warn.noName": "没有名字:模式选择器里会显示成目录 id(例如 custom)。",
@@ -180,7 +174,7 @@ try {
180
174
  "msg.importTooLarge": "文件太大(上限 1MB)。",
181
175
  "msg.exported": "已导出为文件。",
182
176
  "msg.exportFailed": "导出失败",
183
- "delete.title": "永久删除这个助手?",
177
+ "delete.title": "永久删除「{name}」?",
184
178
  "delete.description": "这会删除磁盘上的模式目录,连同它的系统提示词一起消失,无法撤销。正在使用它的会话不受影响;新建会话时它不再出现在选择器里。({id})",
185
179
  "delete.acknowledge": "我明白这个助手的提示词会被永久删除",
186
180
  "delete.confirm": "永久删除",
@@ -191,9 +185,14 @@ try {
191
185
  "name.placeholder": "自定义模式",
192
186
  "name.descriptionPlaceholder": "模式描述(显示在模式选择器里,可留空)",
193
187
  "mode.heading": "基础模式",
194
- "mode.hint": "底子只决定「行集合」与工具能力:本模式的身份行始终替换底子那一行(提示词由你编辑,complete: false),底子的提示语语义不继承。保存后新建会话即生效,无需重启。",
188
+ "mode.hint": "基础模式只决定有哪些行、有哪些工具。本模式的身份始终用这一页的提示词,不继承基础模式的提示语。保存后新建会话生效,无需重启。",
195
189
  "rows.heading": "插件开关",
196
- "rows.hint": "逐行控制本模式挂载的插件。未拨动的行保持官方默认(含平台判断);手动拨动后以你的为准。",
190
+ "rows.filterPlaceholder": "筛选行(名称或 id)",
191
+ "cap.heading": "能力",
192
+ "cap.expand": "展开能力",
193
+ "cap.collapse": "收起能力",
194
+ "cap.hint": "基础模式决定有哪些行;插件开关逐行生效。这两项从新建会话起生效。",
195
+ "rows.hint": "逐行控制本模式挂载的插件。未改动的行保持官方默认(含平台判断);手动设置后以手动值为准。",
197
196
  "prompt.heading": "系统提示词",
198
197
  "prompt.hint": "每次模型调用前重新读取:保存后下一步生效,仅影响使用本助手的会话。「导入」读文件进编辑器(保存前不写入);「导出」存为 .md;「恢复出厂提示词」填回出厂模板(同样需保存)。",
199
198
  "status.enabled": "已启用",
@@ -205,10 +204,8 @@ try {
205
204
  "btn.saving": "处理中…",
206
205
  "btn.reload": "重新读取",
207
206
  "btn.reloadDiscard": "放弃修改并重新读取",
207
+ "btn.reloadConfirm": "放弃未保存的修改并重新读取?",
208
208
  "msg.unsaved": "有未保存的修改",
209
- "msg.loading": "正在读取…",
210
- "msg.notLoaded": "(尚未读取)",
211
- "msg.reread": "已重新读取",
212
209
  "msg.readFailed": "读取失败",
213
210
  "msg.saveFailed": "保存失败",
214
211
  "msg.saved": "已保存。新建会话即生效,当前会话保持原配置。",
@@ -243,6 +240,10 @@ try {
243
240
  "row.tool-subagent-codex.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
244
241
  "row.tool-subagent-claude-code.label": "Claude Code 子代理",
245
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 服务负责",
246
247
  "row.workflow-ptc.label": "工作流引擎",
247
248
  "row.workflow-worker-thread.label": "工作流 Worker 线程",
248
249
  "row.workflow-worker-thread.note": "把工作流跑在独立的 worker 线程里",
@@ -274,9 +275,15 @@ try {
274
275
  "base.ptc.note": "在标准模式基础上启用 PTC 工具呈现(tool-presentation)",
275
276
  "base.minimal.label": "极简模式",
276
277
  "base.minimal.note": "只有 Shell 与终端,共 7 行;没有文件、检索、技能、子代理。系统提示词不是极简那一套(persona 行被本模式替换)",
277
- "base.cordis.label": "Cordis 模式",
278
+ // ★ 与官方对齐:cordis 这个模式在官方界面里叫「创造模式」(英文 Creator mode),
279
+ // 不是「Cordis 模式」—— 实测 `dsh-client-ui-agent-preset` 的词典:
280
+ // `presetCordisName: "创造模式"` / `"Creator mode"`(0.1.7-rc.2 与 0.2.0-rc.2 同名)。
281
+ // 「Cordis」在官方用语里是**技术名**(Cordis 插件 / Cordis 运行时),不是模式名。
282
+ // 此前这里写「Cordis 模式」、而同一份词典的 `base.all.note` 又写「创造模式」,
283
+ // 同一个模式在一页里有两个名字:用户点了「Cordis 模式」药丸,读到的说明却说「创造模式」。
284
+ "base.cordis.label": "创造模式",
278
285
  "base.cordis.note": "标准模式 + 读写运行时的 Cordis 工具集,可让 agent 自己改 harness",
279
- "base.all.label": "自定义模式",
286
+ "base.all.label": "全部出厂行",
280
287
  "base.all.note": "不继承任何单一原生模式:以标准模式的全套行为底,补上只有 PTC / 极简 / 创造模式才有的行,全部都能逐行开关"
281
288
  }
282
289
 
@@ -287,7 +294,6 @@ try {
287
294
  "assistant.empty": "No assistants yet — create one with the field below.",
288
295
  "assistant.loadingList": "Loading assistants…",
289
296
  "assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
290
- "assistant.meta": "ID",
291
297
  "assistant.copyName": "{name} (copy)",
292
298
  "assistant.broken": "Will not start here",
293
299
  "assistant.brokenRows": "{count} row(s) unusable here",
@@ -304,7 +310,7 @@ try {
304
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.",
305
311
  "btn.delete": "Delete this assistant",
306
312
  "btn.cancel": "Cancel",
307
- "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.",
308
314
  "msg.duplicated": "Duplicated. The two are independent from here on.",
309
315
  "msg.deleted": "Deleted.",
310
316
  "msg.createFailed": "Could not create",
@@ -321,16 +327,9 @@ try {
321
327
  "api.alreadyLast": "{name} is already last.",
322
328
  "api.badVariableName": "{variable} is not a valid variable reference: names may use lower-case letters, digits and underscores, and must start with a letter. For a literal brace, use a single opening brace or an unclosed double brace.",
323
329
  "api.unknownVariable": "{variable} is not a registered variable — rendering would fail every request in this mode. Available: {known}.",
324
- "assistant.short": "Each assistant is its own mode: its own system prompt, base mode and plugin switches.",
325
- "name.short": "Renaming only changes what is displayed — not the internal id or existing sessions.",
326
- "mode.short": "The base decides the row set and tool abilities; the persona row is always replaced by this mode.",
327
330
  "mode.unavailable": "This DSH line exposes no readable shipped composition, so the base mode cannot be switched right now.",
328
331
  "rows.unavailable": "No shipped composition is available, so the plugin switches cannot be edited right now; the row states already on disk still apply.",
329
- "mode.pendingRows": "Base changed to {mode}: the row list below is recomputed from the new base when you save.",
330
- "rows.short": "Control which plugins this mode mounts, row by row; untouched rows keep the shipped default.",
331
- "prompt.short": "Saving this text makes it the system prompt of this mode, and it takes effect on the next step.",
332
- "aria.expandHint": "Show the full explanation",
333
- "aria.collapseHint": "Hide the full explanation",
332
+ "mode.pendingRows": "Base mode changed to {mode}: the row list below is recomputed from it when you save.",
334
333
  "aria.expand": "Show details",
335
334
  "aria.collapse": "Hide details",
336
335
  "detail.id": "Row id",
@@ -350,7 +349,8 @@ try {
350
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.",
351
350
  "api.saved": "Saved ({name}, base mode {mode}). A new session picks it up; the current one keeps its configuration.",
352
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.",
353
- "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.",
354
354
  "api.duplicated": "Copied from {from}. The two are independent from now on.",
355
355
  "api.deleted": "Deleted {name}. Sessions already using it keep running; it no longer appears for new sessions.",
356
356
  "api.reordered": "Order saved: the new-session mode picker follows it.",
@@ -380,15 +380,16 @@ try {
380
380
  "api.badJson": "The request body is not valid JSON",
381
381
  "api.savedPromptOnly": "System prompt saved (this machine exposes no shipped composition, so the plugin switches and the base mode were left untouched).",
382
382
  "api.baseCompositionUnavailable": "No shipped composition for base mode \"{mode}\" on this machine, so its plugin switches and base mode cannot be changed (the system prompt can still be saved on its own).",
383
- "warn.personaOffWithPrompt": "The \"Identity (system prompt)\" row is off, so prompt.md is never injected — the prompt you wrote has no effect. Turn the row on, or clear the prompt.",
383
+ "warn.personaOffWithPrompt": "The \"Identity (system prompt)\" row is off, so prompt.md is never injected — the prompt being edited has no effect. Turn the row on, or clear the prompt.",
384
384
  "warn.toolOff": "The \"custom_prompt tool\" row is off: the agent cannot change the prompt from inside a session, only this page can.",
385
385
  "warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
386
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.",
387
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.",
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 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.",
389
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.",
390
- "warn.approvalGateMissing.label": "Approval gate is off",
391
- "warn.approvalGateMissing.hint": "This DSH build has no tools/pre-execute event, so in-session prompt rewrites do NOT ask for approval. The settings page is unaffected; upgrade DSH or turn the custom_prompt tool row off to restore the gate.",
392
393
  "warn.noName": "No name: the mode picker will show the directory id (e.g. custom).",
393
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.",
394
395
  "history.label": "Change history",
@@ -402,7 +403,7 @@ try {
402
403
  "msg.importTooLarge": "That file is too large (1 MB limit).",
403
404
  "msg.exported": "Exported to a file.",
404
405
  "msg.exportFailed": "Export failed",
405
- "delete.title": "Delete this assistant permanently?",
406
+ "delete.title": "Delete \"{name}\" permanently?",
406
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})",
407
408
  "delete.acknowledge": "I understand this assistant's prompt will be deleted permanently",
408
409
  "delete.confirm": "Delete permanently",
@@ -413,24 +414,27 @@ try {
413
414
  "name.placeholder": "Custom mode",
414
415
  "name.descriptionPlaceholder": "Mode description (shown in the mode picker, optional)",
415
416
  "mode.heading": "Base mode",
416
- "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.",
417
418
  "rows.heading": "Plugin switches",
418
- "rows.hint": "Control row by row which plugins this mode mounts, laid out like the official plugin list. Untouched rows keep the official default (platform conditions included); your manual choice wins.",
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 row set. Each switch applies to one row. Both take effect on the next new session.",
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.",
419
425
  "prompt.heading": "System prompt",
420
426
  "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).",
421
427
  "status.enabled": "Enabled",
422
428
  "status.disabled": "Disabled",
423
429
  "status.changed": "changed",
424
- "tag.essential": "core",
430
+ "tag.essential": "Core",
425
431
  "tag.followPlatform": "follows platform",
426
432
  "btn.save": "Save",
427
433
  "btn.saving": "Working…",
428
434
  "btn.reload": "Reload",
429
435
  "btn.reloadDiscard": "Discard edits and reload",
436
+ "btn.reloadConfirm": "Discard unsaved edits and reload?",
430
437
  "msg.unsaved": "Unsaved changes",
431
- "msg.loading": "Loading…",
432
- "msg.notLoaded": "(not loaded)",
433
- "msg.reread": "Reloaded",
434
438
  "msg.readFailed": "Load failed",
435
439
  "msg.saveFailed": "Save failed",
436
440
  "msg.saved": "Saved. A new session picks it up; the current one keeps its configuration.",
@@ -465,6 +469,10 @@ try {
465
469
  "row.tool-subagent-codex.note": "Needs its Bundle installed first; the shipped state is in the details",
466
470
  "row.tool-subagent-claude-code.label": "Claude Code subagent",
467
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",
468
476
  "row.workflow-ptc.label": "Workflow engine",
469
477
  "row.workflow-worker-thread.label": "Workflow worker thread",
470
478
  "row.workflow-worker-thread.note": "Runs workflows on a separate worker thread",
@@ -490,16 +498,20 @@ try {
490
498
  "row.persistent-bash.label": "Persistent bash",
491
499
  "row.terminal-pwsh.label": "Terminal (pwsh)",
492
500
  "row.persistent-pwsh.label": "Persistent pwsh",
493
- "base.standard.label": "Standard",
501
+ // ★ 与官方选择器逐字一致(实测 `dsh-client-ui-agent-preset` 的英文字典,两版 DSH 同名):
502
+ // 官方英文名都带 " mode" 后缀 —— "Standard mode" / "PTC mode" / "Minimal mode" /
503
+ // "Creator mode"。本仓此前写成 "Standard" / "PTC" / "Minimal" / "Cordis",
504
+ // 与用户在官方模式选择器里看到的同一个模式名对不上。
505
+ "base.standard.label": "Standard mode",
494
506
  "base.standard.note": "Full coding agent: shell, files, search, skills, planning, goals, subagents, workflows",
495
- "base.ptc.label": "PTC",
507
+ "base.ptc.label": "PTC mode",
496
508
  "base.ptc.note": "Standard plus PTC tool presentation (tool-presentation)",
497
- "base.minimal.label": "Minimal",
509
+ "base.minimal.label": "Minimal mode",
498
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)",
499
- "base.cordis.label": "Cordis",
511
+ "base.cordis.label": "Creator mode",
500
512
  "base.cordis.note": "Standard plus the Cordis toolset, letting the agent modify its own harness",
501
- "base.all.label": "Custom",
502
- "base.all.note": "Inherits no single shipped mode: standard's full row list plus the rows only PTC / Minimal / Cordis declare, every one of them switchable"
513
+ "base.all.label": "All shipped rows",
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"
503
515
  }
504
516
 
505
517
  const TRANSLATIONS = { zh: ZH, en: EN }
@@ -651,12 +663,14 @@ try {
651
663
  IconChevronDownOutline14: "IconChevronDownOutlineRegular",
652
664
  IconChevronUpOutline14: "IconChevronUpOutlineRegular",
653
665
  IconChevronLeftOutline14: "IconChevronLeftOutlineRegular",
666
+ IconChevronRightOutline14: "IconChevronRightOutlineRegular",
654
667
  IconPlusOutline16: "IconPlusOutlineRegular",
655
668
  IconCheckOutline14: "IconCheckOutlineRegular",
656
669
  IconCopyOutline16: "IconCopyOutlineRegular",
657
670
  IconDownloadOutline16: "IconDownloadOutlineRegular",
658
671
  IconRefreshOutline16: "IconRefreshOutlineRegular",
659
672
  IconTrashOutline16: "IconTrashOutlineRegular",
673
+ IconWarningOutline14: "IconWarningOutlineRegular",
660
674
  }
661
675
 
662
676
  function withIconAliases(atoms) {
@@ -700,16 +714,20 @@ try {
700
714
  */
701
715
  const CSS = [
702
716
  ".cpfe{--g:8px;display:flex;flex-direction:column;gap:24px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px}",
703
- // ★ 区块标题对齐官方设置页的度量(实测 0.1.7-rc.2:区块标题 14px/22px w500,引言 12px/18px
704
- // tertiary)。原来是 15px w700 —— 在官方页面里那看起来就是"另一个产品"。
705
- ".cpfe-h{margin:0 0 2px;font-size:14px;line-height:22px;font-weight:500;color:var(--dsw-alias-label-primary)}",
717
+ // 分节标题:度量抄官方设置面板里的同类文本。
718
+ // 实测(**0.1.7-rc.2 与 0.2.0-rc.2 各量一次,结论相同**):官方 `_title`(设置行标题)
719
+ // 与 `groupTitle`(插件页分组标题)都是 14px/22px **w400**;官方唯一的 14px w500 是插件
720
+ // 卡片标题(`cardTitle`,行高 20px,形态不同),页级标题则是 16px/24px w500。
721
+ // 本页是设置面板里的分节,取 w400。
722
+ // ⚠️ 本仓长期记作「官方区块标题 14px/22px w500」—— 那一条在 2026-10-02 的复量中被证伪。
723
+ ".cpfe-h{margin:0 0 2px;font-size:14px;line-height:22px;font-weight:400;color:var(--dsw-alias-label-primary)}",
706
724
  ".cpfe-sub{margin:0 0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
707
725
  // 区块引言:官方是标题下的一行 12px 灰字 —— 没有折叠按钮,也没有那个蓝色圆点。
708
726
  ".cpfe-hint{display:flex;flex-direction:column;gap:2px;margin:0 0 6px}",
709
727
  ".cpfe-hint-line{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
710
728
  ".cpfe-hint-detail{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
711
729
  // 描述:多行、自适应高度(没有多行输入组件,所以用 textarea + 同一批语义变量)
712
- ".cpfe-desc{box-sizing:border-box;min-height:56px;max-height:200px;resize:vertical;padding:8px 12px;border-radius:10px;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}",
730
+ ".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}",
713
731
  ".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
714
732
  ".cpfe-note{display:block;margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
715
733
  ".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}",
@@ -725,16 +743,16 @@ try {
725
743
  ".cpfe-actions{display:flex;flex-wrap:wrap;gap:var(--g);align-items:center;margin-top:4px}",
726
744
  ".cpfe-newrow{display:flex;gap:var(--g);align-items:center;flex-wrap:wrap;margin-top:10px}",
727
745
  ".cpfe-field{display:flex;width:100%;margin-bottom:8px}",
728
- ".cpfe-row-head{font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary)}",
746
+ ".cpfe-row-head{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
729
747
  ".cpfe-row-switch{flex:0 0 auto}",
730
748
  ".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
731
749
  ".cpfe-row-note{min-height:18px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
732
- ".cpfe-row-toggle{flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:1}",
750
+ ".cpfe-row-toggle{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}",
733
751
  ".cpfe-row-toggle:hover{background:var(--dsw-alias-bg-layer-2)}",
734
752
  ".cpfe-row-open{border-color:var(--dsw-alias-border-l2)}",
735
753
  ".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)}",
736
754
  ".cpfe-detail-line{display:flex;gap:8px;min-width:0}",
737
- ".cpfe-detail-key{flex:0 0 62px;color:var(--dsw-alias-label-secondary)}",
755
+ ".cpfe-detail-key{flex:0 0 9.5em;color:var(--dsw-alias-label-secondary)}",
738
756
  ".cpfe-detail-value{min-width:0;overflow-wrap:anywhere}",
739
757
  ".cpfe-sec-head{display:flex;align-items:center;justify-content:space-between;gap:var(--g);flex-wrap:wrap}",
740
758
  ".cpfe-newinput{flex:1;min-width:200px}",
@@ -746,11 +764,14 @@ try {
746
764
  ".cpfe-line:last-child{border-bottom:0}",
747
765
  ".cpfe-group{display:flex;flex-direction:column;min-width:0}",
748
766
  ".cpfe-kids{display:flex;flex-direction:column;margin-left:0;padding-left:16px}",
749
- ".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:0;padding:12px 0;border:0;border-radius:0;background:none}",
767
+ ".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}",
750
768
  ".cpfe-row-meta{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}",
751
769
  ".cpfe-row-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap;height:18px}",
752
- ".cpfe-editor{box-sizing:border-box;width:100%;min-height:240px;resize:vertical;padding:12px;border-radius:10px;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}",
770
+ ".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}",
753
771
  ".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap}",
772
+ ".cpfe-savebar{position:sticky;bottom:0;z-index:2;padding:8px 0;background:var(--dsw-alias-bg-base)}",
773
+ ".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)}",
774
+ ".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}",
754
775
  ".cpfe-status{font-size:13px;line-height:20px}",
755
776
  ".cpfe-ok{color:var(--dsw-alias-label-secondary)}",
756
777
  ".cpfe-err{color:var(--dsw-alias-state-error-primary)}",
@@ -760,19 +781,19 @@ try {
760
781
  ".cpfe-version{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
761
782
  ".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}",
762
783
  // ── fallback-path controls (unused when the shell provides the atoms) ──
763
- ".cpfe-btn{appearance:none;cursor:pointer;padding:0 14px;height:32px;border-radius:8px;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}",
764
- ".cpfe-btn:disabled{opacity:.5;cursor:default}",
765
- ".cpfe-btn-sm{height:28px;padding:0 10px;font-size:12px}",
784
+ ".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}",
785
+ ".cpfe-btn:disabled{opacity:.4;cursor:not-allowed}",
786
+ ".cpfe-btn-sm{height:28px;padding:0 10px;font-size:12px;line-height:18px;border-radius:var(--dsw-radius-sm)}",
766
787
  ".cpfe-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);border-color:transparent;font-weight:500}",
767
788
  ".cpfe-btn-ghost{border-color:transparent;background:transparent}",
768
- ".cpfe-input{box-sizing:border-box;width:100%;height:34px;padding:0 12px;border-radius:8px;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}",
789
+ ".cpfe-input{box-sizing:border-box;width:100%;height:36px;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}",
769
790
  ".cpfe-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
770
791
  // 忙碌期间禁掉草稿类控件的交互(issue #6)。
771
792
  // 状态层已经拦住了改动(`update()` 在 busy 时直接返回),但对**受控组件**来说,
772
793
  // 光拦状态还不够:DOM 会先显示用户敲进去的字/翻过去的开关,等下一次重渲染才被拉回来 ——
773
794
  // 那就是"我明明改了,它自己弹回去了"。`pointer-events` 让点击根本到不了控件,
774
795
  // 视觉上也给一个明确的"此刻不能动"。
775
- ".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}",
796
+ ".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}",
776
797
  ".cpfe-busy .cpfe-editor{cursor:progress}",
777
798
  ".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)}",
778
799
  ".cpfe-tag-success,.cpfe-tag-info{color:var(--dsw-alias-state-success-primary)}",
@@ -780,6 +801,26 @@ try {
780
801
  ".cpfe-tag-danger{color:var(--dsw-alias-state-error-primary)}",
781
802
  ".cpfe-pill{appearance:none;cursor:pointer;height:28px;padding:0 12px;border-radius:999px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px}",
782
803
  ".cpfe-pill-on{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}",
804
+ // ── 官方的「设置项下拉」形态 ────────────────────────────────────────────
805
+ // 实测(**0.1.7-rc.2 与 0.2.0-rc.2 各量一次,结论相同**):官方的 `_selector` 是
806
+ // height 36px · border-radius var(--dsw-radius-md) · background var(--dsw-alias-bg-module-platform)
807
+ // · padding 0 14px · font-size 14px/line-height 22px · gap 12px,hover 换
808
+ // var(--dsw-alias-interactive-bg-hover)。
809
+ // 壳的 Button 只有 primary / ghost / outline / toolbar 四种变体,没有"灰底选择器"这一种 ——
810
+ // 官方各设置页也是**各自定义** `_selector`(oY77xG / hVGvvW / _2XZxNq / T1PP_q 各一份、
811
+ // 度量完全一致),所以这里照同一套度量自绘,做法与官方一致。
812
+ // 双类选择器(`.cpfe .cpfe-selector`)用来压过壳 Button 的单类变体规则,
813
+ // 免得 `_ghost_` / `_sm_` 把灰底和高度覆盖掉。
814
+ ".cpfe .cpfe-selector{height:36px;padding:0 14px;gap:12px;border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px;font-weight:400}",
815
+ ".cpfe .cpfe-selector:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
816
+ // ── 告警区 ──────────────────────────────────────────────────────────────
817
+ // 这一段此前**一条 CSS 都没有**(2026-10-02 逐表核对:样式表里 `cpfe-warn` 零规则),
818
+ // 于是出现告警时它是一串没有排版的裸 `<p>`:没有行距、图标与文字不对齐、
819
+ // 与页面其余部分(12px/18px)的字号也对不上。度量沿用页面其余部分。
820
+ ".cpfe-warns{display:flex;flex-direction:column;gap:12px}",
821
+ ".cpfe-warn-row{display:flex;flex-direction:column;gap:6px;align-items:flex-start}",
822
+ ".cpfe-warn{display:flex;gap:6px;align-items:flex-start;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary)}",
823
+ ".cpfe-warn svg{flex:0 0 auto;margin-top:2px}",
783
824
  ].join("")
784
825
 
785
826
  /** Apply the page stylesheet once, keyed so a re-mount never duplicates it. */
@@ -867,6 +908,7 @@ try {
867
908
  // 改动历史:列表来自 state(只有元数据),正文点「载入这一版」时按需取。
868
909
  history: Array.isArray(state.history) ? state.history : [],
869
910
  historyPick: "",
911
+ broken: typeof state.broken === "string" && state.broken !== "" ? state.broken : null,
870
912
  // 「配置了却不生效」的告警码;文案按当前语言渲染。
871
913
  warnings: Array.isArray(state.warnings) ? state.warnings : [],
872
914
  }
@@ -890,21 +932,21 @@ try {
890
932
  }
891
933
 
892
934
  /**
893
- * A section's one-line hint, with the full explanation behind a disclosure.
935
+ * A section's one-line introduction, sitting under its heading.
894
936
  *
895
- * The sections used to open with a paragraph each. On a real screen that is a wall of text above the
896
- * controls the user came for, and the same "compact line + open on demand" language as the row list keeps
897
- * the page scannable without dropping a word of the explanation.
937
+ * It takes ONE string and renders it as one line — the shape the official settings pages use:
938
+ * a single 12px/18px tertiary line under the title, with no disclosure control and no bullet.
939
+ *
940
+ * It used to take `hint` + `detail` and pick `detail` when present, for a collapsible
941
+ * introduction that no longer exists. Every caller passed both, so the short form was
942
+ * **never rendered** — five dictionary entries (`assistant.short`, `name.short`, `mode.short`,
943
+ * `rows.short`, `prompt.short`) that looked alive in a grep but reached no pixel. Both are gone.
898
944
  */
899
945
  function SectionHint(props) {
900
- const { hint, detail } = props
901
- // 官方设置页的区块引言就是标题下的一段 12px 灰字:没有圆点、没有折叠按钮,也**不重复**。
902
- // 有完整说明就用完整说明(它是短句的超集),否则用短句 —— 两个都印会在屏幕上出现两遍。
903
- const text = typeof detail === "string" && detail !== "" ? detail : hint
904
946
  return react.createElement(
905
947
  "div",
906
948
  { className: "cpfe-hint" },
907
- react.createElement("span", { className: "cpfe-hint-line" }, text),
949
+ react.createElement("span", { className: "cpfe-hint-line" }, props.text),
908
950
  )
909
951
  }
910
952
 
@@ -917,6 +959,20 @@ try {
917
959
  * this change: 33 rows at 274px wide and 84–117px tall with a bare `tool-bash` line each, i.e. uneven
918
960
  * heights and a lot of vertical noise for information most users never read.
919
961
  */
962
+ function filterRows(rows, query, translateRow) {
963
+ const needle = String(query || "").trim().toLowerCase()
964
+ if (needle === "") return rows
965
+ const out = []
966
+ for (const row of rows) {
967
+ const title = String(translateRow("row." + row.id + ".label", row.label)).toLowerCase()
968
+ const self = title.includes(needle) || row.id.toLowerCase().includes(needle)
969
+ const children = Array.isArray(row.children) ? row.children : []
970
+ const kids = self ? children : filterRows(children, query, translateRow)
971
+ if (self || kids.length > 0) out.push(self ? row : { ...row, children: kids })
972
+ }
973
+ return out
974
+ }
975
+
920
976
  function RowList(props) {
921
977
  const { rows, overrides, onToggle, expanded, onToggleExpand, depth, t } = props
922
978
  return react.createElement(
@@ -971,10 +1027,16 @@ try {
971
1027
  "aria-label": t(open ? "aria.collapse" : "aria.expand"),
972
1028
  onClick: () => onToggleExpand(row.id),
973
1029
  },
974
- open ? "▾" : "▸",
1030
+ // 用壳自己的图标,不再手绘 "▸"/"▾" 字符:字符的字形、基线与粗细都随字体走,
1031
+ // 与官方那一排 14px 线性图标放在一起就是两种东西。壳没提供时退回原字符。
1032
+ (() => {
1033
+ const Icon = open ? A.IconChevronDownOutline14 : A.IconChevronRightOutline14
1034
+ return renderable(Icon) ? react.createElement(Icon, { size: 14 }) : open ? "▾" : "▸"
1035
+ })(),
975
1036
  ),
976
1037
  react.createElement(A.Switch, {
977
1038
  checked: effective,
1039
+ disabled: props.disabled === true,
978
1040
  onChange: (next) => onToggle(row.id, next),
979
1041
  // `Switch` renders NO text: its `label` is the accessible name only, so the
980
1042
  // visible title is drawn next to it (an unlabelled toggle is unusable).
@@ -1045,6 +1107,7 @@ try {
1045
1107
  onToggleExpand: onToggleExpand,
1046
1108
  depth: depth + 1,
1047
1109
  t: t,
1110
+ disabled: props.disabled === true,
1048
1111
  })
1049
1112
  : null,
1050
1113
  )
@@ -1092,10 +1155,19 @@ try {
1092
1155
  * 词典里没有这个 id 时(旧宿主 / 未来新增)退回机器名,宁可难看也不要空。
1093
1156
  */
1094
1157
  const params = result.params
1095
- const shown =
1096
- params !== null && typeof params === "object" && typeof params.mode === "string"
1097
- ? { ...params, mode: translate("base." + params.mode + ".label", params.mode, shellT) }
1098
- : params
1158
+ let shown = params
1159
+ if (params !== null && typeof params === "object") {
1160
+ shown = { ...params }
1161
+ if (typeof params.mode === "string") {
1162
+ shown.mode = translate("base." + params.mode + ".label", params.mode, shellT)
1163
+ }
1164
+ if (typeof params.rows === "string") {
1165
+ shown.rows = params.rows
1166
+ .split(",")
1167
+ .map((id) => translate("row." + id.trim() + ".label", id.trim(), shellT))
1168
+ .join(", ")
1169
+ }
1170
+ }
1099
1171
  return fillPlaceholders(template, shown)
1100
1172
  }
1101
1173
  }
@@ -1126,6 +1198,9 @@ try {
1126
1198
  // 两个官方风格的下拉(壳的 `Menu`):助手切换与改动历史。壳不提供 Menu 时退回原生控件。
1127
1199
  const [assistantOpen, setAssistantOpen] = react.useState(false)
1128
1200
  const [historyOpen, setHistoryOpen] = react.useState(false)
1201
+ const [listFailed, setListFailed] = react.useState(false)
1202
+ const [capOpen, setCapOpen] = react.useState(false)
1203
+ const [rowQuery, setRowQuery] = react.useState("")
1129
1204
  /** 本机这条线上有无法解析的行时,一键把它们关掉(服务端复用保存同一条排版手术)。 */
1130
1205
  const repairRowsNow = async () => {
1131
1206
  if (draft === null) return
@@ -1235,10 +1310,12 @@ try {
1235
1310
  const result = await fetchList()
1236
1311
  if (result === null || result === undefined || result.ok !== true) {
1237
1312
  setFailed(true)
1313
+ setListFailed(true)
1238
1314
  setStatus(apiText(result, "msg.readFailed"))
1239
1315
  return
1240
1316
  }
1241
1317
  const assistants = Array.isArray(result.assistants) ? result.assistants : []
1318
+ setListFailed(false)
1242
1319
  setList(assistants)
1243
1320
  const wanted =
1244
1321
  typeof preferId === "string" && preferId !== ""
@@ -1328,7 +1405,18 @@ try {
1328
1405
  }
1329
1406
 
1330
1407
  const pick = async (id) => {
1331
- if (id === selected) return
1408
+ if (id === selected) {
1409
+ setBusy(true)
1410
+ try {
1411
+ await open(id, { force: true })
1412
+ } catch (error) {
1413
+ setFailed(true)
1414
+ setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
1415
+ } finally {
1416
+ setBusy(false)
1417
+ }
1418
+ return
1419
+ }
1332
1420
  setSelected(id)
1333
1421
  setDeleteOpen(false)
1334
1422
  setAcknowledged(false)
@@ -1346,6 +1434,7 @@ try {
1346
1434
 
1347
1435
  /** Create a new assistant, or duplicate the selected one. */
1348
1436
  const create = async (options) => {
1437
+ if (busy === true) return
1349
1438
  const from = options !== null && typeof options === "object" ? options.from : undefined
1350
1439
  const name =
1351
1440
  options !== null && typeof options === "object" && typeof options.name === "string"
@@ -1526,7 +1615,7 @@ try {
1526
1615
  * confirmation instead of losing the guardrail entirely.
1527
1616
  */
1528
1617
  const askDelete = () => {
1529
- if (typeof A.RiskConfirmation === "function") {
1618
+ if (renderable(A.RiskConfirmation)) {
1530
1619
  setAcknowledged(false)
1531
1620
  setDeleteOpen(true)
1532
1621
  return
@@ -1554,7 +1643,13 @@ try {
1554
1643
  })
1555
1644
  if (result !== null && result !== undefined && result.ok === true) {
1556
1645
  setFailed(false)
1557
- setStatus(apiText(result, "msg.saved"))
1646
+ const compositionChanged =
1647
+ entry !== undefined &&
1648
+ (draft.mode !== entry.saved.mode ||
1649
+ JSON.stringify(draft.overrides) !== JSON.stringify(entry.saved.overrides))
1650
+ const spoken =
1651
+ result.code === "saved" && compositionChanged !== true ? { ...result, code: "savedPrompt" } : result
1652
+ setStatus(apiText(spoken, "msg.saved"))
1558
1653
  // The server normalises what it stores (trimmed name, recomputed overrides),
1559
1654
  // so the just-saved draft is replaced by what it actually wrote. Without this
1560
1655
  // the page would show 「已保存」 and 「未保存」 at the same time.
@@ -1595,16 +1690,16 @@ try {
1595
1690
  "section",
1596
1691
  null,
1597
1692
  react.createElement("h2", { className: "cpfe-h" }, t("assistant.heading")),
1598
- react.createElement(SectionHint, {
1599
- id: "hint:assistant",
1600
- hint: t("assistant.short", t("assistant.hint")),
1601
- detail: t("assistant.hint"),
1602
- expanded: expandedRows,
1603
- onToggleExpand: toggleRowExpanded,
1604
- t: t,
1605
- }),
1693
+ react.createElement(SectionHint, { text: t("assistant.hint") }),
1606
1694
  list === null
1607
- ? react.createElement("p", { className: "cpfe-sub" }, t("assistant.loadingList"))
1695
+ ? react.createElement(
1696
+ "div",
1697
+ null,
1698
+ react.createElement("p", { className: "cpfe-sub" }, listFailed ? t("msg.readFailed") : t("assistant.loadingList")),
1699
+ listFailed
1700
+ ? react.createElement(A.Button, { disabled: busy, onClick: () => reload("") }, t("btn.reload"))
1701
+ : null,
1702
+ )
1608
1703
  : assistants.length === 0
1609
1704
  ? react.createElement("p", { className: "cpfe-sub" }, t("assistant.empty"))
1610
1705
  : react.createElement(
@@ -1627,14 +1722,17 @@ try {
1627
1722
  A.Button,
1628
1723
  {
1629
1724
  variant: "ghost",
1630
- size: "sm",
1725
+ className: "cpfe-selector",
1631
1726
  disabled: busy,
1632
- icon: renderable(A.IconChevronDownOutline14)
1633
- ? react.createElement(A.IconChevronDownOutline14, { size: 14 })
1634
- : null,
1635
1727
  onClick: () => setAssistantOpen((open) => open !== true),
1636
1728
  },
1637
1729
  (assistants.find((item) => item.id === selected) || {}).name || selected || t("assistant.heading"),
1730
+ // 官方设置项的下拉是「文字在左、chevron 在右」(实测「通用设置」页的
1731
+ // 「工作区内修改 / 中文 / 详细」都是这个形态)⇒ 图标放在 children 末尾,
1732
+ // 不走 `icon` prop —— 那个会渲染到文字**前面**,成为页面上唯一一个反向的下拉。
1733
+ renderable(A.IconChevronDownOutline14)
1734
+ ? react.createElement(A.IconChevronDownOutline14, { size: 14 })
1735
+ : null,
1638
1736
  ),
1639
1737
  items: assistants.map((item) => ({
1640
1738
  id: item.id,
@@ -1651,7 +1749,7 @@ try {
1651
1749
  })
1652
1750
  : react.createElement(
1653
1751
  "div",
1654
- { className: "cpfe-pills cpfe-assistants" },
1752
+ { className: "cpfe-pills" },
1655
1753
  assistants.map((item) =>
1656
1754
  react.createElement(
1657
1755
  "span",
@@ -1697,7 +1795,7 @@ try {
1697
1795
  placeholder: t("assistant.newPlaceholder"),
1698
1796
  "aria-label": t("assistant.newPlaceholder"),
1699
1797
  onKeyDown: (event) => {
1700
- if (event.key === "Enter") create()
1798
+ if (event.key === "Enter" && busy !== true) create()
1701
1799
  },
1702
1800
  onChange: (event) => {
1703
1801
  setNewName(event.target.value)
@@ -1715,6 +1813,60 @@ try {
1715
1813
  busy ? t("btn.creating") : t("btn.create"),
1716
1814
  ),
1717
1815
  ),
1816
+ editorReady
1817
+ ? react.createElement(
1818
+ "div",
1819
+ { className: "cpfe-actions" },
1820
+ react.createElement(
1821
+ A.Button,
1822
+ {
1823
+ variant: "outline",
1824
+ size: "sm",
1825
+ icon: A.IconChevronUpOutline14 === undefined ? null : react.createElement(A.IconChevronUpOutline14),
1826
+ disabled: busy || position <= 0,
1827
+ onClick: () => move("up"),
1828
+ },
1829
+ t("btn.moveUp"),
1830
+ ),
1831
+ react.createElement(
1832
+ A.Button,
1833
+ {
1834
+ variant: "outline",
1835
+ size: "sm",
1836
+ icon: A.IconChevronDownOutline14 === undefined ? null : react.createElement(A.IconChevronDownOutline14),
1837
+ disabled: busy || position === -1 || position >= assistants.length - 1,
1838
+ onClick: () => move("down"),
1839
+ },
1840
+ t("btn.moveDown"),
1841
+ ),
1842
+ react.createElement(
1843
+ A.Button,
1844
+ {
1845
+ variant: "outline",
1846
+ size: "sm",
1847
+ icon: A.IconCopyOutline16 === undefined ? null : react.createElement(A.IconCopyOutline16),
1848
+ disabled: busy,
1849
+ onClick: duplicate,
1850
+ },
1851
+ t("btn.duplicate"),
1852
+ ),
1853
+ react.createElement(
1854
+ A.Button,
1855
+ {
1856
+ variant: "ghost",
1857
+ size: "sm",
1858
+ className: "cpfe-danger",
1859
+ icon: A.IconTrashOutline16 === undefined ? null : react.createElement(A.IconTrashOutline16),
1860
+ disabled: busy,
1861
+ onClick: askDelete,
1862
+ },
1863
+ t("btn.delete"),
1864
+ ),
1865
+ )
1866
+ : null,
1867
+ typeof (assistants.find((item) => item.id === selected) || {}).broken === "string"
1868
+ ? react.createElement("p", { className: "cpfe-note" }, t("assistant.brokenHint"))
1869
+ : null,
1718
1870
  react.createElement("p", { className: "cpfe-note" }, t("msg.readOnlyHint")),
1719
1871
  )
1720
1872
 
@@ -1725,14 +1877,7 @@ try {
1725
1877
  "section",
1726
1878
  { key: "name" },
1727
1879
  react.createElement("h2", { className: "cpfe-h" }, t("name.heading")),
1728
- react.createElement(SectionHint, {
1729
- id: "hint:name",
1730
- hint: t("name.short", t("name.hint")),
1731
- detail: t("name.hint"),
1732
- expanded: expandedRows,
1733
- onToggleExpand: toggleRowExpanded,
1734
- t: t,
1735
- }),
1880
+ react.createElement(SectionHint, { text: t("name.hint") }),
1736
1881
  react.createElement(A.Input, {
1737
1882
  className: "cpfe-field",
1738
1883
  value: draft.name,
@@ -1754,68 +1899,12 @@ try {
1754
1899
  "aria-label": t("name.descriptionPlaceholder"),
1755
1900
  onChange: (event) => update({ description: event.target.value }),
1756
1901
  }),
1757
- react.createElement(
1758
- "div",
1759
- { className: "cpfe-actions" },
1760
- react.createElement(
1761
- A.Button,
1762
- {
1763
- variant: "outline",
1764
- size: "sm",
1765
- icon: A.IconChevronUpOutline14 === undefined ? null : react.createElement(A.IconChevronUpOutline14),
1766
- disabled: busy || position <= 0,
1767
- onClick: () => move("up"),
1768
- },
1769
- t("btn.moveUp"),
1770
- ),
1771
- react.createElement(
1772
- A.Button,
1773
- {
1774
- variant: "outline",
1775
- size: "sm",
1776
- icon: A.IconChevronDownOutline14 === undefined ? null : react.createElement(A.IconChevronDownOutline14),
1777
- disabled: busy || position === -1 || position >= assistants.length - 1,
1778
- onClick: () => move("down"),
1779
- },
1780
- t("btn.moveDown"),
1781
- ),
1782
- react.createElement(
1783
- A.Button,
1784
- {
1785
- variant: "outline",
1786
- size: "sm",
1787
- icon: A.IconCopyOutline16 === undefined ? null : react.createElement(A.IconCopyOutline16),
1788
- disabled: busy,
1789
- onClick: duplicate,
1790
- },
1791
- t("btn.duplicate"),
1792
- ),
1793
- react.createElement(
1794
- A.Button,
1795
- {
1796
- variant: "ghost",
1797
- size: "sm",
1798
- className: "cpfe-danger",
1799
- icon: A.IconTrashOutline16 === undefined ? null : react.createElement(A.IconTrashOutline16),
1800
- disabled: busy,
1801
- onClick: askDelete,
1802
- },
1803
- t("btn.delete"),
1804
- ),
1805
- ),
1806
1902
  ),
1807
1903
  react.createElement(
1808
1904
  "section",
1809
1905
  { key: "mode" },
1810
1906
  react.createElement("h2", { className: "cpfe-h" }, t("mode.heading")),
1811
- react.createElement(SectionHint, {
1812
- id: "hint:mode",
1813
- hint: t("mode.short", t("mode.hint")),
1814
- detail: t("mode.hint"),
1815
- expanded: expandedRows,
1816
- onToggleExpand: toggleRowExpanded,
1817
- t: t,
1818
- }),
1907
+ react.createElement(SectionHint, { text: t("mode.hint") }),
1819
1908
  // 降级态(本机这条线没有可读的出厂组成):不给可点的模式药丸,改说一句为什么 ——
1820
1909
  // 点了也只会拿到一个服务端错误,那比置灰更糟。
1821
1910
  draft.baseUnavailable !== null
@@ -1826,7 +1915,13 @@ try {
1826
1915
  payload.modes.map((mode) =>
1827
1916
  react.createElement(
1828
1917
  A.Pill,
1829
- { key: mode.id, active: draft.mode === mode.id, onClick: () => pickMode(mode.id) },
1918
+ {
1919
+ key: mode.id,
1920
+ active: draft.mode === mode.id,
1921
+ disabled: busy,
1922
+ "aria-pressed": draft.mode === mode.id,
1923
+ onClick: () => pickMode(mode.id),
1924
+ },
1830
1925
  t("base." + mode.id + ".label", mode.label),
1831
1926
  ),
1832
1927
  ),
@@ -1851,25 +1946,33 @@ try {
1851
1946
  "section",
1852
1947
  { key: "rows" },
1853
1948
  react.createElement("h2", { className: "cpfe-h" }, t("rows.heading")),
1854
- react.createElement(SectionHint, {
1855
- id: "hint:rows",
1856
- hint: t("rows.short", t("rows.hint")),
1857
- detail: t("rows.hint"),
1858
- expanded: expandedRows,
1859
- onToggleExpand: toggleRowExpanded,
1860
- t: t,
1861
- }),
1949
+ react.createElement(SectionHint, { text: t("rows.hint") }),
1862
1950
  draft.baseUnavailable !== null
1863
1951
  ? react.createElement("p", { className: "cpfe-note" }, t("rows.unavailable"))
1864
- : react.createElement(RowList, {
1865
- expanded: expandedRows,
1866
- onToggleExpand: toggleRowExpanded,
1867
- rows: payload.rows,
1868
- overrides: draft.overrides,
1869
- onToggle: (id, next) => update({ overrides: { ...draft.overrides, [id]: next } }),
1870
- depth: 0,
1871
- t: t,
1872
- }),
1952
+ : react.createElement(
1953
+ "div",
1954
+ null,
1955
+ react.createElement("input", {
1956
+ className: "cpfe-filter",
1957
+ value: rowQuery,
1958
+ placeholder: t("rows.filterPlaceholder"),
1959
+ "aria-label": t("rows.filterPlaceholder"),
1960
+ onChange: (event) => setRowQuery(event.target.value),
1961
+ }),
1962
+ react.createElement(RowList, {
1963
+ expanded: expandedRows,
1964
+ onToggleExpand: toggleRowExpanded,
1965
+ rows: filterRows(payload.rows, rowQuery, t),
1966
+ overrides: draft.overrides,
1967
+ onToggle: (id, next) => {
1968
+ if (draft.mode !== savedMode) return
1969
+ update({ overrides: { ...draft.overrides, [id]: next } })
1970
+ },
1971
+ depth: 0,
1972
+ t: t,
1973
+ disabled: busy === true || draft.mode !== savedMode,
1974
+ }),
1975
+ ),
1873
1976
  ),
1874
1977
  react.createElement(
1875
1978
  "section",
@@ -1934,14 +2037,7 @@ try {
1934
2037
  }),
1935
2038
  ),
1936
2039
  ),
1937
- react.createElement(SectionHint, {
1938
- id: "hint:prompt",
1939
- hint: t("prompt.short", t("prompt.hint")),
1940
- detail: t("prompt.hint"),
1941
- expanded: expandedRows,
1942
- onToggleExpand: toggleRowExpanded,
1943
- t: t,
1944
- }),
2040
+ react.createElement(SectionHint, { text: t("prompt.hint") }),
1945
2041
  react.createElement("textarea", {
1946
2042
  className: "cpfe-editor",
1947
2043
  value: draft.prompt,
@@ -1976,11 +2072,8 @@ try {
1976
2072
  A.Button,
1977
2073
  {
1978
2074
  variant: "ghost",
1979
- size: "sm",
2075
+ className: "cpfe-selector",
1980
2076
  disabled: busy,
1981
- icon: renderable(A.IconChevronDownOutline14)
1982
- ? react.createElement(A.IconChevronDownOutline14, { size: 14 })
1983
- : null,
1984
2077
  onClick: () => setHistoryOpen((open) => open !== true),
1985
2078
  },
1986
2079
  draft.historyPick === ""
@@ -1991,6 +2084,9 @@ try {
1991
2084
  ? t("history.pick")
1992
2085
  : formatWhen(entry.at) + " · " + t("history.by." + entry.by)
1993
2086
  })(),
2087
+ renderable(A.IconChevronDownOutline14)
2088
+ ? react.createElement(A.IconChevronDownOutline14, { size: 14 })
2089
+ : null,
1994
2090
  ),
1995
2091
  items: draft.history.map((entry) => ({
1996
2092
  id: String(entry.n),
@@ -2053,7 +2149,17 @@ try {
2053
2149
  react.createElement(
2054
2150
  "div",
2055
2151
  { key: code, className: "cpfe-warn-row" },
2056
- react.createElement("p", { className: "cpfe-warn" }, "⚠ " + t("warn." + code)),
2152
+ react.createElement(
2153
+ "p",
2154
+ { className: "cpfe-warn" },
2155
+ // 图标与文本分成两个节点:文本单独包一层 span,`align-items:flex-start`
2156
+ // 才能把图标钉在**第一行**,而不是让它在一段多行告警里垂直居中。
2157
+ // 壳没提供图标时退回原来的 "⚠" 字符。
2158
+ renderable(A.IconWarningOutline14)
2159
+ ? react.createElement(A.IconWarningOutline14, { size: 14 })
2160
+ : "⚠",
2161
+ react.createElement("span", null, t("warn." + code)),
2162
+ ),
2057
2163
  // 平台自己给的那句话(`state.broken`,形如
2058
2164
  // `tool-workflow (@deepseek-ai/dsh-tool-workflow): waiting for workflowEngine`)。
2059
2165
  //
@@ -2083,10 +2189,10 @@ try {
2083
2189
  ),
2084
2190
  ),
2085
2191
  ),
2086
- ...editorSections,
2192
+ ...(editorReady ? editorSections.filter((section) => section.key === "name" || section.key === "prompt") : []),
2087
2193
  react.createElement(
2088
2194
  "div",
2089
- { className: "cpfe-bar" },
2195
+ { className: "cpfe-bar cpfe-savebar" },
2090
2196
  react.createElement(
2091
2197
  A.Button,
2092
2198
  {
@@ -2102,11 +2208,14 @@ try {
2102
2208
  {
2103
2209
  variant: "outline",
2104
2210
  icon: A.IconRefreshOutline16 === undefined ? null : react.createElement(A.IconRefreshOutline16),
2105
- disabled: busy || editorReady === false,
2211
+ disabled: busy,
2106
2212
  // Explicitly destructive of the local draft, so it says so while there is
2107
2213
  // one: switching assistants never discards edits, and this button is the
2108
2214
  // one place that does.
2109
- onClick: () => reload(selected, { discard: true }),
2215
+ onClick: () => {
2216
+ if (dirty && typeof window !== "undefined" && window.confirm(t("btn.reloadConfirm")) !== true) return
2217
+ reload(selected, { discard: true })
2218
+ },
2110
2219
  },
2111
2220
  dirty ? t("btn.reloadDiscard") : t("btn.reload"),
2112
2221
  ),
@@ -2126,13 +2235,32 @@ try {
2126
2235
  react.createElement("span", { className: "cpfe-path" }, editorReady ? payload.compositionPath : ""),
2127
2236
  ),
2128
2237
  ),
2238
+ editorReady
2239
+ ? react.createElement(
2240
+ "section",
2241
+ null,
2242
+ react.createElement(
2243
+ "button",
2244
+ {
2245
+ type: "button",
2246
+ "data-cap": "toggle",
2247
+ className: "cpfe-h",
2248
+ "aria-expanded": capOpen,
2249
+ onClick: () => setCapOpen((open) => open !== true),
2250
+ },
2251
+ (capOpen ? t("cap.collapse") : t("cap.expand")) + " · " + t("cap.heading"),
2252
+ ),
2253
+ react.createElement(SectionHint, { text: t("cap.hint") }),
2254
+ capOpen ? editorSections.filter((section) => section.key === "mode" || section.key === "rows") : null,
2255
+ )
2256
+ : null,
2129
2257
  // The confirmation is a portal: rendering it here keeps every piece of this page's
2130
2258
  // state in one component.
2131
- typeof A.RiskConfirmation === "function" && editorReady
2259
+ renderable(A.RiskConfirmation) && editorReady
2132
2260
  ? react.createElement(A.RiskConfirmation, {
2133
2261
  open: deleteOpen,
2134
- title: t("delete.title"),
2135
- description: fillPlaceholders(t("delete.description"), { id: draft.id }),
2262
+ title: fillPlaceholders(t("delete.title"), { name: draft.name || draft.id }),
2263
+ description: fillPlaceholders(t("delete.description"), { id: draft.id, name: draft.name || draft.id }),
2136
2264
  acknowledgeLabel: t("delete.acknowledge"),
2137
2265
  cancelLabel: t("btn.cancel"),
2138
2266
  closeLabel: t("delete.close"),