dsh-custom-mode 2.2.5 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +76 -57
- package/README.zh.md +76 -57
- package/assistants.mjs +8 -2
- package/atomic.mjs +156 -56
- package/client.js +653 -227
- package/composition.mjs +5 -4
- package/index.mjs +86 -47
- package/locales.mjs +138 -20
- package/package.json +4 -4
- package/preset/prompt-reader.mjs +35 -2
- package/preset/prompt-tool.mjs +1 -0
- package/preset/prompt.md +0 -2
- package/preset-backend/declarative.mjs +43 -15
- package/seed.mjs +5 -1
package/client.js
CHANGED
|
@@ -58,15 +58,70 @@ try {
|
|
|
58
58
|
* truth, checked for key parity by test/locales.test.mjs).
|
|
59
59
|
*/
|
|
60
60
|
const ZH = {
|
|
61
|
+
"update.heading": "插件更新",
|
|
62
|
+
"update.orphanHint": "以下助手已不在当前列表,但本页仍保留其未保存草稿,因此暂不能离页。",
|
|
63
|
+
"update.orphanName": "{name}(ID:{id})",
|
|
64
|
+
"update.orphanView": "查看并保留草稿",
|
|
65
|
+
"update.orphanHide": "收起草稿",
|
|
66
|
+
"update.orphanCopy": "下方包含完整提示词、名称、描述、模式和覆盖项,可选中复制到本地文件保留。放弃仅移除本页内存草稿,不修改磁盘文件。",
|
|
67
|
+
"update.orphanDiscard": "放弃此草稿…",
|
|
68
|
+
"update.orphanConfirm": "确认放弃“{name}”(ID:{id})的未保存草稿?请先复制保留需要的内容。放弃后无法从本页恢复;磁盘文件不会改动。",
|
|
69
|
+
"update.orphanAcknowledge": "我已保留需要的内容,确认放弃后无法从本页恢复。",
|
|
70
|
+
"update.orphanChanged": "草稿或助手列表已变化,未放弃任何内容。请重新查看并确认。",
|
|
71
|
+
"update.orphanDiscarded": "已放弃所确认的本页草稿;磁盘文件未改动。",
|
|
72
|
+
"update.running": "运行中的插件版本:{version}",
|
|
73
|
+
"update.unknown": "未知",
|
|
74
|
+
"update.check": "检查更新",
|
|
75
|
+
"update.checking": "正在检查…",
|
|
76
|
+
"update.inPlugins": "在插件管理中更新",
|
|
77
|
+
"update.details": "打开插件详情",
|
|
78
|
+
"update.guide": "更新指引",
|
|
79
|
+
"update.idle": "仅在点击检查时查询宿主配置的包仓库;不会自动安装。",
|
|
80
|
+
"update.unavailable": "当前连接未提供原生版本检查能力。可打开插件详情或查看更新指引。",
|
|
81
|
+
"update.permission": "此操作缺少宿主授权,或权限服务暂不可用。",
|
|
82
|
+
"update.updatePermission": "可以查看版本,但当前连接没有更新权限。",
|
|
83
|
+
"update.available": "可用更新:{version}。安装由官方插件管理处理。",
|
|
84
|
+
"update.current": "此次检查未返回更高版本。",
|
|
85
|
+
"update.filtered": "发现较新版本,但没有符合官方默认条件的稳定兼容版本。",
|
|
86
|
+
"update.partial": "结果未覆盖全部新版本,暂未找到可推荐的更新。",
|
|
87
|
+
"update.omitted": "另有 {count} 个较新版本未列出。",
|
|
88
|
+
"update.disk": "磁盘安装版本:{version};它可能尚未在运行中加载。",
|
|
89
|
+
"update.managed": "此安装无法使用原生包仓库更新,请通过原来源或插件详情管理;不会自动切换来源。",
|
|
90
|
+
"update.notFound": "宿主插件列表中没有找到此插件,无法确认更新。",
|
|
91
|
+
"update.network": "宿主查询包仓库失败,请稍后重试;尚不能确认更新状态。",
|
|
92
|
+
"update.refused": "宿主未提供可用的版本列表,尚不能确认更新状态。",
|
|
93
|
+
"update.failed": "未能完成更新检查,请重试或打开插件详情查看。",
|
|
94
|
+
"update.finishOperation": "请等待当前操作完成后再离开此页。",
|
|
95
|
+
"update.finishRecovery": "请先完成重试读取或重试生效,再离开此页。",
|
|
96
|
+
"update.saveDrafts": "请先保存或主动放弃所有助手草稿及未提交的新建输入,再离开此页。",
|
|
97
|
+
"update.navigationFailed": "未能打开插件详情;草稿已保留,可稍后重试。",
|
|
98
|
+
"create.nextStep": "创建后可在上方继续编辑并保存;使用时,在新会话的模式菜单选择它。",
|
|
99
|
+
"btn.retryRead": "重试读取",
|
|
100
|
+
"btn.retrySync": "重试生效",
|
|
101
|
+
"api.savedButSyncFailed": "文件已保存,但这些助手尚未生效:{ids}。请点「重试生效」,无需重复原操作。",
|
|
102
|
+
"api.syncFailed": "这些助手仍未生效:{ids}。请重试;仍失败时查看宿主日志。",
|
|
103
|
+
"api.presetRegistrationSyncFailed": "尚未确认已保存的助手配置在宿主注册表中生效,请处理已有告警或查看宿主日志。",
|
|
104
|
+
"api.synchronized": "配置已生效。新会话请从模式菜单选择助手。",
|
|
105
|
+
"api.recoveryRequired": "助手文件需要恢复:{path}。请保留目录中的备份,并查看宿主日志中的恢复说明。",
|
|
106
|
+
"msg.completedReadFailed": "{result} 页面刷新失败。请点「重试读取」,无需重复原操作。",
|
|
107
|
+
"msg.synchronized": "配置已生效。",
|
|
108
|
+
"msg.syncFailed": "重新应用配置失败,请重试。",
|
|
109
|
+
"msg.loaded": "已载入到编辑器。尚未保存。",
|
|
110
|
+
"status.reading": "正在读取…",
|
|
111
|
+
"status.saving": "正在保存…",
|
|
112
|
+
"status.creating": "正在创建助手…",
|
|
113
|
+
"status.deleting": "正在删除助手…",
|
|
114
|
+
"status.reordering": "正在保存顺序…",
|
|
115
|
+
"status.repairing": "正在修复…",
|
|
116
|
+
"status.importing": "正在读取导入文件…",
|
|
117
|
+
"status.synchronizing": "正在应用配置…",
|
|
61
118
|
"nav": "自定义模式",
|
|
62
119
|
"assistant.heading": "助手",
|
|
63
|
-
"assistant.hint": "
|
|
64
|
-
"posture.hint": "
|
|
120
|
+
"assistant.hint": "选择要编辑的助手。每位助手独立保存系统提示词、基础模式和插件开关。",
|
|
121
|
+
"posture.hint": "选择起步姿态并命名,再点「新增助手」。",
|
|
65
122
|
"tree.posture": "起步",
|
|
66
|
-
"tree.base": "
|
|
67
|
-
"tree.
|
|
68
|
-
"tree.baseSkip": "这一支点不了。建好后到「当前助手」里改。",
|
|
69
|
-
"tree.baseLocked": "只有官方标准模式是亮的。其余四档是暗的,新建时点不了。",
|
|
123
|
+
"tree.base": "初始基础模式",
|
|
124
|
+
"tree.baseLocked": "新建助手从官方标准模式起步。建好后,可在「当前助手」调整基础模式。",
|
|
70
125
|
"posture.develop.label": "开发",
|
|
71
126
|
"posture.develop.note": "官方标准模式,工具按出厂。",
|
|
72
127
|
"posture.write.label": "写作",
|
|
@@ -74,7 +129,7 @@ try {
|
|
|
74
129
|
"posture.chat.label": "聊天",
|
|
75
130
|
"posture.chat.note": "官方标准模式。文件和终端关闭。",
|
|
76
131
|
"editor.heading": "当前助手",
|
|
77
|
-
"editor.hint": "
|
|
132
|
+
"editor.hint": "正在编辑「{name}」。这里的选择只决定编辑对象;新会话请从模式菜单选择助手。",
|
|
78
133
|
"assistant.empty": "还没有助手,用下面的输入框新建一个。",
|
|
79
134
|
"assistant.loadingList": "正在读取助手列表…",
|
|
80
135
|
"assistant.newPlaceholder": "新助手的名字(例如:写作助手)",
|
|
@@ -95,7 +150,7 @@ try {
|
|
|
95
150
|
"btn.resetHint": "把编辑器换回此助手新建时保存的提示词。只改草稿,保存后才写入。",
|
|
96
151
|
"btn.delete": "删除",
|
|
97
152
|
"btn.cancel": "取消",
|
|
98
|
-
"msg.created": "
|
|
153
|
+
"msg.created": "已创建。可继续编辑;使用时请在新会话的模式菜单选择它。",
|
|
99
154
|
"msg.duplicated": "已复制。两份配置彼此独立。",
|
|
100
155
|
"msg.deleted": "已删除。",
|
|
101
156
|
"msg.createFailed": "创建失败",
|
|
@@ -135,7 +190,7 @@ try {
|
|
|
135
190
|
"api.saved": "已保存({name},{mode})。下一步组装时会重读已保存的提示词。",
|
|
136
191
|
"api.conflict": "磁盘上的提示词已经变了,这次没有写入。先重新读取,再决定要不要覆盖。",
|
|
137
192
|
"api.savedWithExclusiveRows": "已保存({name},{mode})。已关闭不能同时启用的行:{rows}。",
|
|
138
|
-
"api.created": "已创建「{name}
|
|
193
|
+
"api.created": "已创建「{name}」。可继续编辑;使用时请在新会话的模式菜单选择它。",
|
|
139
194
|
"api.savedPrompt": "已保存系统提示词({name})。下一步组装时会重读这个文件。",
|
|
140
195
|
"api.duplicated": "已从「{from}」复制。两份互不影响。",
|
|
141
196
|
"api.deleted": "已删除「{name}」。正在使用它的会话不受影响。",
|
|
@@ -171,6 +226,8 @@ try {
|
|
|
171
226
|
"warn.approvalGateMissing": "当前版本没有审批事件。会话内改写提示词不会请求确认。",
|
|
172
227
|
"warn.unresolvableRows": "有的行在本机无法启动,此模式不会出现在新会话菜单里。点「按本线修复」只关闭那些行。",
|
|
173
228
|
"warn.presetBroken": "此模式无法启动,不会出现在新会话菜单里。点「按本线修复」。原因见下一行。",
|
|
229
|
+
"warn.presetRegistrationSyncFailed": "尚未确认这位助手已保存的配置在宿主注册表中生效。请先处理已有告警或查看宿主日志;无需重复保存。",
|
|
230
|
+
"warn.presetRegistrationRestartSuggested": "尚未确认这位助手已保存的配置在宿主注册表中生效。如刚更新或恢复插件,建议重启 DeepSeek Harness 后重新检查;无需重复保存。",
|
|
174
231
|
"warn.badPromptVariable": "提示词里有不可用的变量。改掉后再保存,否则此模式的请求会失败。",
|
|
175
232
|
"warn.duplicateRowIds": "组成文件里有重复的行 id。开关只作用于第一处。",
|
|
176
233
|
"warn.predicateFailed": "有一行的平台条件无法计算,已按关闭处理。",
|
|
@@ -218,7 +275,7 @@ try {
|
|
|
218
275
|
"tag.essential": "核心",
|
|
219
276
|
"tag.followPlatform": "随平台",
|
|
220
277
|
"btn.save": "保存",
|
|
221
|
-
"btn.saving": "
|
|
278
|
+
"btn.saving": "保存中…",
|
|
222
279
|
"btn.reload": "重新读取",
|
|
223
280
|
"btn.reloadDiscard": "放弃修改并重新读取",
|
|
224
281
|
"btn.reloadConfirm": "放弃未保存的修改并重新读取?",
|
|
@@ -277,6 +334,8 @@ try {
|
|
|
277
334
|
"row.tool-plugin-manager.label": "插件管理(安装 / 启停)",
|
|
278
335
|
"row.tool-plugin-manager.note": "供模型安装或开关插件。",
|
|
279
336
|
"row.tool-presentation.label": "PTC 工具呈现",
|
|
337
|
+
"row.tool-executor-ptc.label": "PTC 程序执行器",
|
|
338
|
+
"row.tool-executor-ptc.note": "通过 run_code 执行程序并调用工具,同时向模型提供工具 SDK。",
|
|
280
339
|
"row.present.label": "交付文件(present)",
|
|
281
340
|
"row.command-goal.label": "目标命令",
|
|
282
341
|
"row.tool-goal.label": "目标",
|
|
@@ -301,15 +360,70 @@ try {
|
|
|
301
360
|
}
|
|
302
361
|
|
|
303
362
|
const EN = {
|
|
363
|
+
"update.heading": "Plugin updates",
|
|
364
|
+
"update.orphanHint": "These assistants left the current list, but this page still holds their unsaved drafts. Resolve them before leaving.",
|
|
365
|
+
"update.orphanName": "{name} (ID: {id})",
|
|
366
|
+
"update.orphanView": "View and keep draft",
|
|
367
|
+
"update.orphanHide": "Hide draft",
|
|
368
|
+
"update.orphanCopy": "The text below includes the full prompt, name, description, mode and overrides. Select and copy it to a local file to keep it. Discarding only removes this page's in-memory draft; files on disk are unchanged.",
|
|
369
|
+
"update.orphanDiscard": "Discard this draft…",
|
|
370
|
+
"update.orphanConfirm": "Discard the unsaved draft for \"{name}\" (ID: {id})? First copy any content you need to keep. This page cannot recover it after discarding; files on disk will remain unchanged.",
|
|
371
|
+
"update.orphanAcknowledge": "I have kept the content I need and understand this page cannot recover the discarded draft.",
|
|
372
|
+
"update.orphanChanged": "The draft or assistant list changed. Nothing was discarded. Review it again before confirming.",
|
|
373
|
+
"update.orphanDiscarded": "The confirmed draft was discarded from this page; files on disk are unchanged.",
|
|
374
|
+
"update.running": "Running plugin version: {version}",
|
|
375
|
+
"update.unknown": "unknown",
|
|
376
|
+
"update.check": "Check for updates",
|
|
377
|
+
"update.checking": "Checking…",
|
|
378
|
+
"update.inPlugins": "Update in Plugins",
|
|
379
|
+
"update.details": "Open plugin details",
|
|
380
|
+
"update.guide": "Update guide",
|
|
381
|
+
"update.idle": "Only a click checks the registries configured by the host; installation is never automatic.",
|
|
382
|
+
"update.unavailable": "This connection does not offer native version checks. Open plugin details or the update guide.",
|
|
383
|
+
"update.permission": "Host permission is missing for this action, or the access service is unavailable.",
|
|
384
|
+
"update.updatePermission": "You can view versions, but this connection does not have update permission.",
|
|
385
|
+
"update.available": "Update available: {version}. Official plugin management handles installation.",
|
|
386
|
+
"update.current": "This check returned no newer releases.",
|
|
387
|
+
"update.filtered": "Newer releases exist, but none meet the official default conditions for a stable compatible version.",
|
|
388
|
+
"update.partial": "The result omits some newer releases; no recommended update was found.",
|
|
389
|
+
"update.omitted": "{count} additional newer releases were omitted.",
|
|
390
|
+
"update.disk": "Installed on disk: {version}; this version may not be loaded in the running host yet.",
|
|
391
|
+
"update.managed": "Manage this installation through its original source or plugin details; it cannot use this native registry update path. The source will not be changed automatically.",
|
|
392
|
+
"update.notFound": "The host did not list this plugin, so its update status could not be determined.",
|
|
393
|
+
"update.network": "The host could not query the registry. Try again later; update status is unknown.",
|
|
394
|
+
"update.refused": "The host did not provide a usable version list; update status is unknown.",
|
|
395
|
+
"update.failed": "The update check could not finish. Retry or open plugin details.",
|
|
396
|
+
"update.finishOperation": "Wait for the current operation to finish before leaving this page.",
|
|
397
|
+
"update.finishRecovery": "Complete Retry refresh or Retry activation before leaving this page.",
|
|
398
|
+
"update.saveDrafts": "Save or explicitly discard all assistant drafts and unsubmitted new-assistant input before leaving this page.",
|
|
399
|
+
"update.navigationFailed": "Plugin details could not open. Your drafts are preserved; try again later.",
|
|
400
|
+
"create.nextStep": "After creating, edit and save above. To use the assistant, choose it in the mode menu for a new session.",
|
|
401
|
+
"btn.retryRead": "Retry refresh",
|
|
402
|
+
"btn.retrySync": "Retry activation",
|
|
403
|
+
"api.savedButSyncFailed": "Files are saved, but these assistants are not active yet: {ids}. Select Retry activation; you do not need to repeat the original action.",
|
|
404
|
+
"api.syncFailed": "These assistants are still not active: {ids}. Retry, then check the host log if the problem continues.",
|
|
405
|
+
"api.presetRegistrationSyncFailed": "The saved assistant configuration has not been confirmed in the host registry. Address existing warnings or check the host log.",
|
|
406
|
+
"api.synchronized": "Configuration is active. Choose an assistant in the mode menu for a new session.",
|
|
407
|
+
"api.recoveryRequired": "Assistant files need recovery: {path}. Keep the backups in that directory and check the host log for recovery details.",
|
|
408
|
+
"msg.completedReadFailed": "{result} The page could not refresh. Select Retry refresh; do not repeat the original action.",
|
|
409
|
+
"msg.synchronized": "Configuration is active.",
|
|
410
|
+
"msg.syncFailed": "Could not apply the configuration. Please retry.",
|
|
411
|
+
"msg.loaded": "Loaded into the editor. Not saved yet.",
|
|
412
|
+
"status.reading": "Reading…",
|
|
413
|
+
"status.saving": "Saving…",
|
|
414
|
+
"status.creating": "Creating assistant…",
|
|
415
|
+
"status.deleting": "Deleting assistant…",
|
|
416
|
+
"status.reordering": "Saving order…",
|
|
417
|
+
"status.repairing": "Repairing…",
|
|
418
|
+
"status.importing": "Reading import file…",
|
|
419
|
+
"status.synchronizing": "Applying configuration…",
|
|
304
420
|
"nav": "Custom mode",
|
|
305
421
|
"assistant.heading": "Assistants",
|
|
306
|
-
"assistant.hint": "
|
|
307
|
-
"posture.hint": "
|
|
422
|
+
"assistant.hint": "Choose an assistant to edit. Each keeps its own system prompt, base mode and plugin switches.",
|
|
423
|
+
"posture.hint": "Choose a starting posture and a name, then select New assistant.",
|
|
308
424
|
"tree.posture": "Starter",
|
|
309
|
-
"tree.base": "
|
|
310
|
-
"tree.
|
|
311
|
-
"tree.baseSkip": "Cannot be chosen on this branch. Change it later under This assistant.",
|
|
312
|
-
"tree.baseLocked": "Only the official Standard mode is lit. The other four are dim and cannot be chosen when creating.",
|
|
425
|
+
"tree.base": "Starting base mode",
|
|
426
|
+
"tree.baseLocked": "New assistants start on the official Standard mode. You can change the base mode under This assistant after creating one.",
|
|
313
427
|
"posture.develop.label": "Coding",
|
|
314
428
|
"posture.develop.note": "Official Standard mode, tools as shipped.",
|
|
315
429
|
"posture.write.label": "Writing",
|
|
@@ -317,7 +431,7 @@ try {
|
|
|
317
431
|
"posture.chat.label": "Chat",
|
|
318
432
|
"posture.chat.note": "Official Standard mode. Files and terminal off.",
|
|
319
433
|
"editor.heading": "This assistant",
|
|
320
|
-
"editor.hint": "This
|
|
434
|
+
"editor.hint": "Editing {name}. This selection controls what you edit here; choose an assistant in the mode menu when starting a new session.",
|
|
321
435
|
"assistant.empty": "No assistants yet — create one with the field below.",
|
|
322
436
|
"assistant.loadingList": "Loading assistants…",
|
|
323
437
|
"assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
|
|
@@ -338,7 +452,7 @@ try {
|
|
|
338
452
|
"btn.resetHint": "Restores the prompt saved when this assistant was created. The editor changes. Save writes it.",
|
|
339
453
|
"btn.delete": "Delete",
|
|
340
454
|
"btn.cancel": "Cancel",
|
|
341
|
-
"msg.created": "Created.
|
|
455
|
+
"msg.created": "Created. Continue editing here, then choose it in the mode menu for a new session.",
|
|
342
456
|
"msg.duplicated": "Duplicated. The two are independent from here on.",
|
|
343
457
|
"msg.deleted": "Deleted.",
|
|
344
458
|
"msg.createFailed": "Could not create",
|
|
@@ -378,7 +492,7 @@ try {
|
|
|
378
492
|
"api.saved": "Saved ({name}, {mode}). The next model step rereads the saved prompt.",
|
|
379
493
|
"api.conflict": "The prompt on disk has changed, so this save was not written. Reload, then decide whether to overwrite.",
|
|
380
494
|
"api.savedWithExclusiveRows": "Saved ({name}, {mode}). Turned off rows that cannot be on together: {rows}.",
|
|
381
|
-
"api.created": "Created {name}.
|
|
495
|
+
"api.created": "Created {name}. Continue editing here, then choose it in the mode menu for a new session.",
|
|
382
496
|
"api.savedPrompt": "System prompt saved ({name}). The next model step rereads this file.",
|
|
383
497
|
"api.duplicated": "Copied from {from}. The two are independent from now on.",
|
|
384
498
|
"api.deleted": "Deleted {name}. Sessions already using it are unchanged.",
|
|
@@ -417,6 +531,8 @@ try {
|
|
|
417
531
|
"warn.approvalGateMissing": "This version has no approval event. In-session prompt edits are not confirmed.",
|
|
418
532
|
"warn.unresolvableRows": "Some rows cannot start here, so this mode is absent from the new-session menu. Fix for this line turns off only those rows.",
|
|
419
533
|
"warn.presetBroken": "This mode cannot start, so it will not appear for a new session. Use Fix for this line. The reason is on the next line.",
|
|
534
|
+
"warn.presetRegistrationSyncFailed": "This assistant's saved configuration has not been confirmed in the host registry. Address existing warnings or check the host log; you do not need to save again.",
|
|
535
|
+
"warn.presetRegistrationRestartSuggested": "This assistant's saved configuration has not been confirmed in the host registry. If the plugin was just updated or restored, consider restarting DeepSeek Harness, then check again; you do not need to save again.",
|
|
420
536
|
"warn.badPromptVariable": "The prompt uses a variable that is not available. Fix it before saving.",
|
|
421
537
|
"warn.duplicateRowIds": "A row id is repeated. A switch applies only to the first one.",
|
|
422
538
|
"warn.predicateFailed": "A platform condition could not be evaluated. That row is treated as off.",
|
|
@@ -461,7 +577,7 @@ try {
|
|
|
461
577
|
"tag.essential": "Core",
|
|
462
578
|
"tag.followPlatform": "Platform",
|
|
463
579
|
"btn.save": "Save",
|
|
464
|
-
"btn.saving": "
|
|
580
|
+
"btn.saving": "Saving…",
|
|
465
581
|
"btn.reload": "Reload",
|
|
466
582
|
"btn.reloadDiscard": "Discard edits and reload",
|
|
467
583
|
"btn.reloadConfirm": "Discard unsaved edits and reload?",
|
|
@@ -520,6 +636,8 @@ try {
|
|
|
520
636
|
"row.tool-plugin-manager.label": "Plugin manager (install / toggle)",
|
|
521
637
|
"row.tool-plugin-manager.note": "Lets the model install or toggle plugins.",
|
|
522
638
|
"row.tool-presentation.label": "PTC tool presentation",
|
|
639
|
+
"row.tool-executor-ptc.label": "PTC program executor",
|
|
640
|
+
"row.tool-executor-ptc.note": "Runs programs and calls tools through run_code, and provides the tool SDK to the model.",
|
|
523
641
|
"row.present.label": "Deliverables (present)",
|
|
524
642
|
"row.command-goal.label": "Goal command",
|
|
525
643
|
"row.tool-goal.label": "Goals",
|
|
@@ -606,6 +724,47 @@ try {
|
|
|
606
724
|
*/
|
|
607
725
|
let localeRef
|
|
608
726
|
|
|
727
|
+
const UPDATE_PACKAGE = "dsh-custom-mode"
|
|
728
|
+
let updateServices = { manager: null, navigation: null, list: false, versions: false, update: false, accessKnown: false, revision: 0 }
|
|
729
|
+
const updateListeners = new Set()
|
|
730
|
+
const publishUpdateServices = (patch) => {
|
|
731
|
+
updateServices = { ...updateServices, ...patch, revision: updateServices.revision + 1 }
|
|
732
|
+
for (const listener of updateListeners) listener()
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Optional Cordis scopes own these reads; old hosts never block the editor. */
|
|
736
|
+
function mountUpdateServices(ctx) {
|
|
737
|
+
if (typeof ctx.inject !== "function") return
|
|
738
|
+
ctx.inject(["remote", "remote.pluginManager"], (scope) => {
|
|
739
|
+
const manager = scope.remote.pluginManager
|
|
740
|
+
if (manager === null || manager === undefined) return
|
|
741
|
+
publishUpdateServices({ manager, list: false, versions: false, update: false, accessKnown: false })
|
|
742
|
+
scope.effect(() => () => {
|
|
743
|
+
if (updateServices.manager === manager) publishUpdateServices({ manager: null, list: false, versions: false, update: false, accessKnown: false })
|
|
744
|
+
})
|
|
745
|
+
scope.inject(["remoteAccess"], (accessScope) => {
|
|
746
|
+
if (typeof accessScope.remoteAccess.whenAccess !== "function") return
|
|
747
|
+
if (updateServices.manager === manager) publishUpdateServices({ accessKnown: true })
|
|
748
|
+
for (const [key, action] of [["list", "listBundles"], ["versions", "listVersions"], ["update", "updateBundle"]]) {
|
|
749
|
+
if (typeof manager[action] !== "function") continue
|
|
750
|
+
accessScope.effect(() => accessScope.remoteAccess.whenAccess("pluginManager/" + action, () => {
|
|
751
|
+
if (updateServices.manager === manager) publishUpdateServices({ [key]: true })
|
|
752
|
+
return () => {
|
|
753
|
+
if (updateServices.manager === manager) publishUpdateServices({ [key]: false })
|
|
754
|
+
}
|
|
755
|
+
}))
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
})
|
|
759
|
+
ctx.inject(["pluginNavigation"], (scope) => {
|
|
760
|
+
const navigation = scope.get("pluginNavigation")
|
|
761
|
+
publishUpdateServices({ navigation })
|
|
762
|
+
scope.effect(() => () => {
|
|
763
|
+
if (updateServices.navigation === navigation) publishUpdateServices({ navigation: null })
|
|
764
|
+
})
|
|
765
|
+
})
|
|
766
|
+
}
|
|
767
|
+
|
|
609
768
|
/**
|
|
610
769
|
* Plain-element stand-ins for the shell's atoms.
|
|
611
770
|
*
|
|
@@ -779,6 +938,11 @@ try {
|
|
|
779
938
|
".cpfe-pills button{white-space:nowrap}",
|
|
780
939
|
".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
|
|
781
940
|
".cpfe-history{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}",
|
|
941
|
+
// Native Menu and Button keep their appearance; only the trigger's
|
|
942
|
+
// own text shrinks so long names cannot wrap outside its fixed height.
|
|
943
|
+
".cpfe-selector-menu,.cpfe-selector{min-width:0;max-width:100%}",
|
|
944
|
+
".cpfe-selector-label{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
945
|
+
".cpfe-selector-chevron{flex:0 0 auto}",
|
|
782
946
|
".cpfe-history-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
783
947
|
".cpfe-history-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
784
948
|
".cpfe-pill-wrap{display:inline-flex;align-items:center;gap:4px}",
|
|
@@ -789,7 +953,9 @@ try {
|
|
|
789
953
|
".cpfe-branch{display:flex;flex-direction:column;gap:16px;margin-top:8px;padding-left:12px;border-left:1px solid var(--dsw-alias-border-l1)}",
|
|
790
954
|
".cpfe-node{display:flex;flex-direction:column;gap:4px}",
|
|
791
955
|
".cpfe-node-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
792
|
-
|
|
956
|
+
// Native Input applies className to its padded wrapper span, so its
|
|
957
|
+
// outer box (not only the inner input) must fit the plugin column.
|
|
958
|
+
".cpfe-field{display:flex;box-sizing:border-box;width:100%;min-width:0;max-width:100%;margin-bottom:8px}",
|
|
793
959
|
".cpfe-row-head{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
794
960
|
".cpfe-row-switch{flex:0 0 auto}",
|
|
795
961
|
".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
|
|
@@ -802,7 +968,15 @@ try {
|
|
|
802
968
|
".cpfe-detail-value{min-width:0;overflow-wrap:anywhere}",
|
|
803
969
|
".cpfe-sec-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px}",
|
|
804
970
|
".cpfe-sec-head .cpfe-actions{margin-top:0}",
|
|
805
|
-
".cpfe-
|
|
971
|
+
".cpfe-update{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:16px 0;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:16px}",
|
|
972
|
+
".cpfe-update-meta{flex:1 1 220px;min-width:0}",
|
|
973
|
+
".cpfe-update-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;max-width:100%}",
|
|
974
|
+
// The shared atoms expose no Link or anchor-shaped Button. Keep a real anchor;
|
|
975
|
+
// use official settings UI typography/link tokens, not Markdown prose styles.
|
|
976
|
+
".cpfe-update-guide{max-width:100%;overflow-wrap:anywhere;font:var(--dsw-font-s-strong-14);color:var(--dsw-alias-link);text-decoration:none}",
|
|
977
|
+
"@media (hover:hover){.cpfe-update-guide:hover{text-decoration:underline}}",
|
|
978
|
+
".cpfe-update-note{overflow-wrap:anywhere}",
|
|
979
|
+
".cpfe-newinput{box-sizing:border-box;flex:1 1 200px;min-width:0;max-width:100%}",
|
|
806
980
|
// 单列、行高统一(对齐官方插件页的形态);之前的自适应多列网格会让行高参差不齐。
|
|
807
981
|
// ★ 对齐官方设置行(实测 0.1.7-rc.2 的行:padding 16px 0、无背景、无圆角,行间只有一条 1px
|
|
808
982
|
// 分隔线)。原来是"圆角卡片 + 描边 + 灰底" —— 那正是廉价感最重的部分。
|
|
@@ -857,11 +1031,15 @@ try {
|
|
|
857
1031
|
".cpfe-warn svg{flex:0 0 auto;margin-top:2px}",
|
|
858
1032
|
].join("")
|
|
859
1033
|
|
|
860
|
-
/**
|
|
1034
|
+
/** Keep one stylesheet, replacing stale content when an HMR bundle arrives. */
|
|
861
1035
|
function ensureStyles() {
|
|
862
1036
|
if (typeof document === "undefined") return
|
|
863
1037
|
const tagId = "dsh-custom-mode/system-prompt"
|
|
864
|
-
|
|
1038
|
+
const existing = document.querySelector('style[data-plugin-css="' + tagId + '"]')
|
|
1039
|
+
if (existing !== null) {
|
|
1040
|
+
if (existing.textContent !== CSS) existing.textContent = CSS
|
|
1041
|
+
return
|
|
1042
|
+
}
|
|
865
1043
|
const tag = document.createElement("style")
|
|
866
1044
|
tag.dataset.plugin = "dsh-custom-mode"
|
|
867
1045
|
tag.dataset.pluginCss = tagId
|
|
@@ -880,6 +1058,7 @@ try {
|
|
|
880
1058
|
delete: ROUTE + "/delete",
|
|
881
1059
|
reorder: ROUTE + "/reorder",
|
|
882
1060
|
repair: ROUTE + "/repair",
|
|
1061
|
+
sync: ROUTE + "/sync",
|
|
883
1062
|
}
|
|
884
1063
|
|
|
885
1064
|
/** Every assistant this feature manages. */
|
|
@@ -1208,14 +1387,42 @@ try {
|
|
|
1208
1387
|
return t(fallbackKey)
|
|
1209
1388
|
}
|
|
1210
1389
|
|
|
1211
|
-
const [list,
|
|
1390
|
+
const [list, setListState] = react.useState(null)
|
|
1391
|
+
const heldRoster = react.useRef(list)
|
|
1392
|
+
const setList = (value) => { heldRoster.current = value; setListState(value) }
|
|
1212
1393
|
const [selected, setSelected] = react.useState("")
|
|
1213
1394
|
/** id → { payload, saved, value }: loaded state, baseline and live draft. */
|
|
1214
|
-
const [entries,
|
|
1395
|
+
const [entries, setEntriesState] = react.useState({})
|
|
1396
|
+
const heldEntries = react.useRef(entries)
|
|
1397
|
+
const setEntries = (next) => {
|
|
1398
|
+
heldEntries.current = typeof next === "function" ? next(heldEntries.current) : next
|
|
1399
|
+
setEntriesState(heldEntries.current)
|
|
1400
|
+
}
|
|
1215
1401
|
const [status, setStatus] = react.useState("")
|
|
1216
1402
|
const [failed, setFailed] = react.useState(false)
|
|
1217
1403
|
const [busy, setBusy] = react.useState(false)
|
|
1218
|
-
const [
|
|
1404
|
+
const [pending, setPending] = react.useState("")
|
|
1405
|
+
const [syncNeeded, setSyncNeededState] = react.useState(false)
|
|
1406
|
+
const [refreshNeeded, setRefreshNeededState] = react.useState(null)
|
|
1407
|
+
const navigationState = react.useRef({ syncNeeded: false, refreshNeeded: null, newName: "" })
|
|
1408
|
+
const setSyncNeeded = (value) => { navigationState.current.syncNeeded = value; setSyncNeededState(value) }
|
|
1409
|
+
const setRefreshNeeded = (value) => { navigationState.current.refreshNeeded = value; setRefreshNeededState(value) }
|
|
1410
|
+
// A ref closes the interval before React renders disabled controls. Only the
|
|
1411
|
+
// owning operation may apply a response or release the busy state.
|
|
1412
|
+
const operation = react.useRef({ alive: true, current: null })
|
|
1413
|
+
const [newName, setNewNameState] = react.useState("")
|
|
1414
|
+
const setNewName = (value) => { navigationState.current.newName = value; setNewNameState(value) }
|
|
1415
|
+
const [runtimeVersion, setRuntimeVersion] = react.useState("")
|
|
1416
|
+
const [updateResult, setUpdateResult] = react.useState(null)
|
|
1417
|
+
const [updateNotice, setUpdateNotice] = react.useState("")
|
|
1418
|
+
const [updateChecking, setUpdateChecking] = react.useState(false)
|
|
1419
|
+
const [, setUpdateRevision] = react.useState(0)
|
|
1420
|
+
const updateOperation = react.useRef({ alive: true, current: null })
|
|
1421
|
+
const [orphanView, setOrphanView] = react.useState("")
|
|
1422
|
+
const [orphanDialog, setOrphanDialog] = react.useState(null)
|
|
1423
|
+
const [orphanAcknowledged, setOrphanAcknowledged] = react.useState(false)
|
|
1424
|
+
const orphanConfirmation = react.useRef(null)
|
|
1425
|
+
const orphanAcknowledgement = react.useRef(false)
|
|
1219
1426
|
const [postureChoices, setPostureChoices] = react.useState([])
|
|
1220
1427
|
const [posture, setPosture] = react.useState("develop")
|
|
1221
1428
|
const [deleteOpen, setDeleteOpen] = react.useState(false)
|
|
@@ -1236,12 +1443,11 @@ try {
|
|
|
1236
1443
|
/** 本机这条线上有无法解析的行时,一键把它们关掉(服务端复用保存同一条排版手术)。 */
|
|
1237
1444
|
const repairRowsNow = async () => {
|
|
1238
1445
|
if (draft === null) return
|
|
1239
|
-
|
|
1240
|
-
setFailed(false)
|
|
1241
|
-
try {
|
|
1446
|
+
return runOperation("status.repairing", "msg.saveFailed", async (token) => {
|
|
1242
1447
|
const result = await postJson(ROUTES.repair, { id: selected })
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1448
|
+
if (!ownsOperation(token)) return
|
|
1449
|
+
showResult(result, "msg.repaired", "msg.saveFailed")
|
|
1450
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1245
1451
|
// ★ 把修复折进**草稿**(issue #8)。
|
|
1246
1452
|
//
|
|
1247
1453
|
// 修复在磁盘上是生效的,但草稿里那几行仍然是"启用"。而 `reload()` 对脏草稿是
|
|
@@ -1255,17 +1461,9 @@ try {
|
|
|
1255
1461
|
for (const rowId of repairedIds) nextOverrides[rowId] = false
|
|
1256
1462
|
update({ overrides: nextOverrides }, { force: true })
|
|
1257
1463
|
}
|
|
1258
|
-
await reload()
|
|
1259
|
-
} else {
|
|
1260
|
-
setFailed(true)
|
|
1261
|
-
setStatus(apiText(result, "msg.saveFailed"))
|
|
1464
|
+
await refreshAfterWrite(result, "msg.repaired", selected, token, () => reload(selected, undefined, token))
|
|
1262
1465
|
}
|
|
1263
|
-
}
|
|
1264
|
-
setFailed(true)
|
|
1265
|
-
setStatus(t("msg.saveFailed"))
|
|
1266
|
-
} finally {
|
|
1267
|
-
setBusy(false)
|
|
1268
|
-
}
|
|
1466
|
+
})
|
|
1269
1467
|
}
|
|
1270
1468
|
|
|
1271
1469
|
const toggleRowExpanded = (id) =>
|
|
@@ -1309,6 +1507,52 @@ try {
|
|
|
1309
1507
|
|
|
1310
1508
|
const describeError = (error) => String((error && error.message) || error)
|
|
1311
1509
|
|
|
1510
|
+
const ownsOperation = (token) => operation.current.alive && operation.current.current === token
|
|
1511
|
+
const runOperation = async (pendingKey, failureKey, action) => {
|
|
1512
|
+
if (!operation.current.alive || operation.current.current !== null) return
|
|
1513
|
+
const token = {}
|
|
1514
|
+
operation.current.current = token
|
|
1515
|
+
setBusy(true)
|
|
1516
|
+
setPending(pendingKey)
|
|
1517
|
+
try {
|
|
1518
|
+
await action(token)
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (ownsOperation(token)) {
|
|
1521
|
+
setFailed(true)
|
|
1522
|
+
if (pendingKey === "status.reading" && list === null) setListFailed(true)
|
|
1523
|
+
setStatus(fillPlaceholders(t("status.detail"), { message: t(failureKey), detail: describeError(error) }))
|
|
1524
|
+
}
|
|
1525
|
+
} finally {
|
|
1526
|
+
if (ownsOperation(token)) {
|
|
1527
|
+
operation.current.current = null
|
|
1528
|
+
setBusy(false)
|
|
1529
|
+
setPending("")
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const showResult = (result, successKey, failureKey) => {
|
|
1535
|
+
setFailed(result?.ok !== true)
|
|
1536
|
+
setStatus(apiText(result, result?.ok === true ? successKey : failureKey))
|
|
1537
|
+
if (result?.synchronized === false) setSyncNeeded(true)
|
|
1538
|
+
else if (result?.synchronized === true) setSyncNeeded(false)
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Once the host confirms a mutation, a failed follow-up GET cannot turn
|
|
1542
|
+
// that fact into a failed mutation or invite the user to repeat it.
|
|
1543
|
+
const refreshAfterWrite = async (result, fallbackKey, targetId, token, refresh) => {
|
|
1544
|
+
try {
|
|
1545
|
+
await refresh()
|
|
1546
|
+
if (ownsOperation(token)) setRefreshNeeded(null)
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
if (!ownsOperation(token)) return
|
|
1549
|
+
setRefreshNeeded({ result, fallbackKey, targetId })
|
|
1550
|
+
setFailed(true)
|
|
1551
|
+
const message = fillPlaceholders(t("msg.completedReadFailed"), { result: apiText(result, fallbackKey) })
|
|
1552
|
+
setStatus(fillPlaceholders(t("status.detail"), { message, detail: describeError(error) }))
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1312
1556
|
/**
|
|
1313
1557
|
* Fetch one assistant into `entries`, keeping any unsaved draft it already has.
|
|
1314
1558
|
*
|
|
@@ -1316,17 +1560,17 @@ try {
|
|
|
1316
1560
|
* `force` is "re-read even if this entry is already in memory". Neither is set by
|
|
1317
1561
|
* ordinary switching: moving between assistants must never race a half-typed prompt.
|
|
1318
1562
|
*/
|
|
1319
|
-
const open = async (id, options) => {
|
|
1563
|
+
const open = async (id, options, token) => {
|
|
1564
|
+
if (!ownsOperation(token)) return
|
|
1320
1565
|
if (typeof id !== "string" || id === "") return
|
|
1321
1566
|
const force = options !== null && typeof options === "object" && options.force === true
|
|
1322
1567
|
const discard = options !== null && typeof options === "object" && options.discard === true
|
|
1323
1568
|
const held = entries[id]
|
|
1324
1569
|
if (force !== true && held !== undefined) return
|
|
1325
1570
|
const result = await fetchState(id)
|
|
1571
|
+
if (!ownsOperation(token)) return
|
|
1326
1572
|
if (result === null || result === undefined || result.ok !== true) {
|
|
1327
|
-
|
|
1328
|
-
setStatus(apiText(result, "msg.readFailed"))
|
|
1329
|
-
return
|
|
1573
|
+
throw new Error(apiText(result, "msg.readFailed"))
|
|
1330
1574
|
}
|
|
1331
1575
|
const fresh = draftOf(result)
|
|
1332
1576
|
setEntries((previous) => {
|
|
@@ -1334,21 +1578,21 @@ try {
|
|
|
1334
1578
|
const keep = discard !== true && before !== undefined && !sameDraft(before.value, before.saved) ? before.value : fresh
|
|
1335
1579
|
return { ...previous, [id]: { payload: result, saved: fresh, value: keep } }
|
|
1336
1580
|
})
|
|
1337
|
-
setFailed(false)
|
|
1338
1581
|
}
|
|
1339
1582
|
|
|
1340
1583
|
/** Re-read the assistant list and force-reload the one it settles on. */
|
|
1341
|
-
const reload = async (preferId, options) => {
|
|
1584
|
+
const reload = async (preferId, options, token) => {
|
|
1585
|
+
if (!ownsOperation(token)) return
|
|
1342
1586
|
const result = await fetchList()
|
|
1587
|
+
if (!ownsOperation(token)) return
|
|
1343
1588
|
if (result === null || result === undefined || result.ok !== true) {
|
|
1344
|
-
setFailed(true)
|
|
1345
1589
|
setListFailed(true)
|
|
1346
|
-
|
|
1347
|
-
return
|
|
1590
|
+
throw new Error(apiText(result, "msg.readFailed"))
|
|
1348
1591
|
}
|
|
1349
1592
|
const assistants = Array.isArray(result.assistants) ? result.assistants : []
|
|
1350
1593
|
const choices = Array.isArray(result.postures) ? result.postures.filter((item) => item !== null && typeof item === "object" && typeof item.id === "string") : []
|
|
1351
1594
|
setListFailed(false)
|
|
1595
|
+
if (typeof result.version === "string" && result.version !== "") setRuntimeVersion(result.version)
|
|
1352
1596
|
setList(assistants)
|
|
1353
1597
|
setPostureChoices(choices)
|
|
1354
1598
|
setPosture((current) => (choices.some((item) => item.id === current) ? current : choices.length > 0 ? choices[0].id : "develop"))
|
|
@@ -1362,10 +1606,11 @@ try {
|
|
|
1362
1606
|
: ""
|
|
1363
1607
|
setSelected(wanted)
|
|
1364
1608
|
if (wanted === "") {
|
|
1365
|
-
|
|
1609
|
+
// An empty remote roster does not discard the only in-memory copy of another draft.
|
|
1610
|
+
setEntries((previous) => Object.fromEntries(Object.entries(previous).filter(([, held]) => !sameDraft(held.value, held.saved))))
|
|
1366
1611
|
return
|
|
1367
1612
|
}
|
|
1368
|
-
await open(wanted, { force: true, discard: options !== null && typeof options === "object" && options.discard === true })
|
|
1613
|
+
await open(wanted, { force: true, discard: options !== null && typeof options === "object" && options.discard === true }, token)
|
|
1369
1614
|
}
|
|
1370
1615
|
|
|
1371
1616
|
/**
|
|
@@ -1374,11 +1619,13 @@ try {
|
|
|
1374
1619
|
* A save can rename an assistant but never changes ids, so the list needs a new
|
|
1375
1620
|
* fetch while the selection, the payloads and every draft stay exactly as they are.
|
|
1376
1621
|
*/
|
|
1377
|
-
const refreshNames = async () => {
|
|
1622
|
+
const refreshNames = async (token) => {
|
|
1378
1623
|
const result = await fetchList()
|
|
1624
|
+
if (!ownsOperation(token)) return
|
|
1379
1625
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1626
|
+
if (typeof result.version === "string" && result.version !== "") setRuntimeVersion(result.version)
|
|
1380
1627
|
setList(Array.isArray(result.assistants) ? result.assistants : [])
|
|
1381
|
-
}
|
|
1628
|
+
} else throw new Error(apiText(result, "msg.readFailed"))
|
|
1382
1629
|
}
|
|
1383
1630
|
|
|
1384
1631
|
/**
|
|
@@ -1397,25 +1644,157 @@ try {
|
|
|
1397
1644
|
}, [])
|
|
1398
1645
|
|
|
1399
1646
|
react.useEffect(() => {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
setBusy(true)
|
|
1403
|
-
try {
|
|
1404
|
-
await reload("")
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
if (alive) {
|
|
1407
|
-
setFailed(true)
|
|
1408
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
|
|
1409
|
-
}
|
|
1410
|
-
} finally {
|
|
1411
|
-
if (alive) setBusy(false)
|
|
1412
|
-
}
|
|
1413
|
-
})()
|
|
1647
|
+
operation.current.alive = true
|
|
1648
|
+
runOperation("status.reading", "msg.readFailed", (token) => reload("", undefined, token))
|
|
1414
1649
|
return () => {
|
|
1415
|
-
alive = false
|
|
1650
|
+
operation.current.alive = false
|
|
1651
|
+
operation.current.current = null
|
|
1416
1652
|
}
|
|
1417
1653
|
}, [])
|
|
1418
1654
|
|
|
1655
|
+
react.useEffect(() => {
|
|
1656
|
+
updateOperation.current.alive = true
|
|
1657
|
+
const changed = () => {
|
|
1658
|
+
updateOperation.current.current = null
|
|
1659
|
+
setUpdateChecking(false)
|
|
1660
|
+
setUpdateResult(null)
|
|
1661
|
+
setUpdateNotice("")
|
|
1662
|
+
setUpdateRevision((value) => value + 1)
|
|
1663
|
+
}
|
|
1664
|
+
updateListeners.add(changed)
|
|
1665
|
+
return () => {
|
|
1666
|
+
updateListeners.delete(changed)
|
|
1667
|
+
updateOperation.current.alive = false
|
|
1668
|
+
updateOperation.current.current = null
|
|
1669
|
+
}
|
|
1670
|
+
}, [])
|
|
1671
|
+
|
|
1672
|
+
const hasNativeUpdates = (services) => typeof services.manager?.listBundles === "function" &&
|
|
1673
|
+
typeof services.manager?.listVersions === "function" && typeof services.manager?.updateBundle === "function"
|
|
1674
|
+
const ownsUpdateCheck = (token, services) => updateOperation.current.alive &&
|
|
1675
|
+
updateOperation.current.current === token && updateServices.revision === services.revision
|
|
1676
|
+
const checkUpdates = async () => {
|
|
1677
|
+
if (!updateOperation.current.alive || operation.current.current !== null || updateOperation.current.current !== null) return
|
|
1678
|
+
const services = updateServices
|
|
1679
|
+
if (!hasNativeUpdates(services) || !services.list || !services.versions) return
|
|
1680
|
+
const token = {}
|
|
1681
|
+
updateOperation.current.current = token
|
|
1682
|
+
setUpdateChecking(true)
|
|
1683
|
+
setUpdateResult(null)
|
|
1684
|
+
setUpdateNotice("")
|
|
1685
|
+
const show = (key, details) => setUpdateResult({ key, ...details, revision: services.revision })
|
|
1686
|
+
try {
|
|
1687
|
+
const inventory = await services.manager.listBundles()
|
|
1688
|
+
if (!ownsUpdateCheck(token, services)) return
|
|
1689
|
+
if (inventory?.ok !== true || !Array.isArray(inventory.value)) throw new Error("inventory")
|
|
1690
|
+
const bundle = inventory.value.find((item) => item?.name === UPDATE_PACKAGE)
|
|
1691
|
+
if (bundle === undefined) { show("update.notFound"); return }
|
|
1692
|
+
if (bundle.updatable !== true || bundle.installed !== true || bundle.availability !== "profile" ||
|
|
1693
|
+
bundle.official !== false || bundle.readOnlyReason !== undefined || typeof bundle.source !== "string" ||
|
|
1694
|
+
typeof bundle.version !== "string" || bundle.version === "") {
|
|
1695
|
+
show("update.managed", { installed: bundle.version }); return
|
|
1696
|
+
}
|
|
1697
|
+
const answer = await services.manager.listVersions(UPDATE_PACKAGE, {})
|
|
1698
|
+
if (!ownsUpdateCheck(token, services)) return
|
|
1699
|
+
if (answer?.ok !== true || answer.value === null || typeof answer.value !== "object") throw new Error("versions")
|
|
1700
|
+
const listing = answer.value
|
|
1701
|
+
if (listing.status === "refused") {
|
|
1702
|
+
show(listing.problem === "network" ? "update.network" : listing.problem === "not-updatable" ? "update.managed" : "update.refused")
|
|
1703
|
+
return
|
|
1704
|
+
}
|
|
1705
|
+
if (listing.status !== "listed" || typeof listing.installed !== "string" || listing.installed !== bundle.version ||
|
|
1706
|
+
!(listing.registry === null || typeof listing.registry === "string") || !Array.isArray(listing.attempts) ||
|
|
1707
|
+
!Number.isInteger(listing.omitted) || listing.omitted < 0 || !Array.isArray(listing.versions) ||
|
|
1708
|
+
!listing.versions.every((item) => item !== null && typeof item === "object" && typeof item.version === "string" &&
|
|
1709
|
+
item.version !== "" && typeof item.bundle === "boolean" && typeof item.prerelease === "boolean" && Array.isArray(item.tags))) {
|
|
1710
|
+
throw new Error("incomplete listing")
|
|
1711
|
+
}
|
|
1712
|
+
// Match the official default candidate and trust the Host's semver ordering.
|
|
1713
|
+
const candidate = listing.versions.find((item) => item.bundle && item.incompatible === undefined && item.deprecated === undefined && !item.prerelease)
|
|
1714
|
+
show(candidate ? "update.available" : listing.omitted > 0 ? "update.partial" : listing.versions.length > 0 ? "update.filtered" : "update.current", {
|
|
1715
|
+
candidate: candidate?.version, installed: listing.installed, omitted: listing.omitted,
|
|
1716
|
+
})
|
|
1717
|
+
} catch {
|
|
1718
|
+
if (ownsUpdateCheck(token, services)) show("update.failed")
|
|
1719
|
+
} finally {
|
|
1720
|
+
if (ownsUpdateCheck(token, services)) {
|
|
1721
|
+
updateOperation.current.current = null
|
|
1722
|
+
setUpdateChecking(false)
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** Latest refs also guard a click before React commits the last keystroke. */
|
|
1728
|
+
const updateNavigationBlock = () => {
|
|
1729
|
+
if (operation.current.current !== null || busy || updateOperation.current.current !== null) return "update.finishOperation"
|
|
1730
|
+
if (navigationState.current.syncNeeded || navigationState.current.refreshNeeded !== null) return "update.finishRecovery"
|
|
1731
|
+
if (navigationState.current.newName !== "" || Object.values(heldEntries.current).some((held) => !sameDraft(held.value, held.saved))) return "update.saveDrafts"
|
|
1732
|
+
return ""
|
|
1733
|
+
}
|
|
1734
|
+
const guardUpdateNavigation = () => {
|
|
1735
|
+
if (!updateOperation.current.alive) return false
|
|
1736
|
+
const block = updateNavigationBlock()
|
|
1737
|
+
if (block !== "") { setUpdateNotice(block); return false }
|
|
1738
|
+
return true
|
|
1739
|
+
}
|
|
1740
|
+
const openPluginDetails = () => {
|
|
1741
|
+
if (!guardUpdateNavigation()) return
|
|
1742
|
+
const services = updateServices
|
|
1743
|
+
if (((hasNativeUpdates(services) || services.accessKnown) && !services.list) || typeof services.navigation?.openBundle !== "function" ||
|
|
1744
|
+
(updateResult?.candidate && (updateResult.revision !== services.revision || !services.update))) {
|
|
1745
|
+
setUpdateNotice("update.permission"); return
|
|
1746
|
+
}
|
|
1747
|
+
try { services.navigation.openBundle(UPDATE_PACKAGE) }
|
|
1748
|
+
catch { setUpdateNotice("update.navigationFailed") }
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const orphanDrafts = () => {
|
|
1752
|
+
if (!Array.isArray(heldRoster.current)) return []
|
|
1753
|
+
const visible = new Set(heldRoster.current.map((item) => item.id))
|
|
1754
|
+
return Object.entries(heldEntries.current).filter(([id, held]) => !visible.has(id) && !sameDraft(held.value, held.saved))
|
|
1755
|
+
}
|
|
1756
|
+
const closeOrphanConfirmation = () => {
|
|
1757
|
+
orphanConfirmation.current = null
|
|
1758
|
+
orphanAcknowledgement.current = false
|
|
1759
|
+
setOrphanAcknowledged(false)
|
|
1760
|
+
setOrphanDialog(null)
|
|
1761
|
+
}
|
|
1762
|
+
const orphanActionBlock = () => operation.current.current !== null || busy || updateOperation.current.current !== null
|
|
1763
|
+
? "update.finishOperation"
|
|
1764
|
+
: navigationState.current.syncNeeded || navigationState.current.refreshNeeded !== null ? "update.finishRecovery" : ""
|
|
1765
|
+
const orphanSnapshotCurrent = (snapshot) => heldEntries.current === snapshot.entries && heldRoster.current === snapshot.roster &&
|
|
1766
|
+
navigationState.current.newName === snapshot.newName && orphanDrafts().some(([id]) => id === snapshot.id)
|
|
1767
|
+
const discardOrphan = (snapshot) => {
|
|
1768
|
+
if (!updateOperation.current.alive || orphanConfirmation.current !== snapshot || !orphanAcknowledgement.current) return
|
|
1769
|
+
const blocked = orphanActionBlock()
|
|
1770
|
+
if (blocked || !orphanSnapshotCurrent(snapshot)) {
|
|
1771
|
+
closeOrphanConfirmation()
|
|
1772
|
+
setUpdateNotice(blocked || "update.orphanChanged")
|
|
1773
|
+
return
|
|
1774
|
+
}
|
|
1775
|
+
setEntries((previous) => { const next = { ...previous }; delete next[snapshot.id]; return next })
|
|
1776
|
+
closeOrphanConfirmation()
|
|
1777
|
+
setOrphanView("")
|
|
1778
|
+
setUpdateNotice("update.orphanDiscarded")
|
|
1779
|
+
}
|
|
1780
|
+
const askDiscardOrphan = (id, shown) => {
|
|
1781
|
+
if (!updateOperation.current.alive) return
|
|
1782
|
+
const snapshot = { ...shown, id }
|
|
1783
|
+
const blocked = orphanActionBlock()
|
|
1784
|
+
if (blocked || !orphanSnapshotCurrent(snapshot)) { setUpdateNotice(blocked || "update.orphanChanged"); return }
|
|
1785
|
+
const value = snapshot.entries[id].value
|
|
1786
|
+
snapshot.name = value.name || id
|
|
1787
|
+
orphanConfirmation.current = snapshot
|
|
1788
|
+
orphanAcknowledgement.current = false
|
|
1789
|
+
setOrphanAcknowledged(false)
|
|
1790
|
+
if (renderable(A.RiskConfirmation)) setOrphanDialog(snapshot)
|
|
1791
|
+
else if (typeof window !== "undefined" && typeof window.confirm === "function" &&
|
|
1792
|
+
window.confirm(fillPlaceholders(t("update.orphanConfirm"), { name: snapshot.name, id }))) {
|
|
1793
|
+
orphanAcknowledgement.current = true
|
|
1794
|
+
discardOrphan(snapshot)
|
|
1795
|
+
} else closeOrphanConfirmation()
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1419
1798
|
/** Change one field of the selected assistant's draft. */
|
|
1420
1799
|
const update = (patch, options) => {
|
|
1421
1800
|
// ★ 忙碌期间**不接受**对草稿的修改(issue #6)。
|
|
@@ -1430,7 +1809,7 @@ try {
|
|
|
1430
1809
|
//
|
|
1431
1810
|
// `{ force: true }` 是给**内部**调用用的(例如「按本线修复」把结果折进草稿):
|
|
1432
1811
|
// 那不是用户输入,不受这条守护约束。
|
|
1433
|
-
if (
|
|
1812
|
+
if (operation.current.current !== null && options?.force !== true) return
|
|
1434
1813
|
setEntries((previous) => {
|
|
1435
1814
|
const current = previous[selected]
|
|
1436
1815
|
if (current === undefined) return previous
|
|
@@ -1440,36 +1819,25 @@ try {
|
|
|
1440
1819
|
}
|
|
1441
1820
|
|
|
1442
1821
|
const pick = async (id) => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
setBusy(false)
|
|
1452
|
-
}
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
1455
|
-
setSelected(id)
|
|
1456
|
-
setDeleteOpen(false)
|
|
1457
|
-
setAcknowledged(false)
|
|
1458
|
-
setStatus("")
|
|
1459
|
-
setBusy(true)
|
|
1460
|
-
try {
|
|
1461
|
-
await open(id)
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
setFailed(true)
|
|
1464
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
|
|
1465
|
-
} finally {
|
|
1466
|
-
setBusy(false)
|
|
1467
|
-
}
|
|
1822
|
+
return runOperation("status.reading", "msg.readFailed", async (token) => {
|
|
1823
|
+
setSelected(id)
|
|
1824
|
+
setDeleteOpen(false)
|
|
1825
|
+
setAcknowledged(false)
|
|
1826
|
+
setStatus("")
|
|
1827
|
+
setFailed(false)
|
|
1828
|
+
await open(id, { force: id === selected }, token)
|
|
1829
|
+
})
|
|
1468
1830
|
}
|
|
1469
1831
|
|
|
1832
|
+
const reread = (discard = false) => runOperation("status.reading", "msg.readFailed", async (token) => {
|
|
1833
|
+
setStatus("")
|
|
1834
|
+
setFailed(false)
|
|
1835
|
+
await reload(selected, { discard }, token)
|
|
1836
|
+
})
|
|
1837
|
+
|
|
1470
1838
|
/** Create a new assistant, or duplicate the selected one. */
|
|
1471
1839
|
const create = async (options) => {
|
|
1472
|
-
if (
|
|
1840
|
+
if (operation.current.current !== null) return
|
|
1473
1841
|
const from = options !== null && typeof options === "object" ? options.from : undefined
|
|
1474
1842
|
const name =
|
|
1475
1843
|
options !== null && typeof options === "object" && typeof options.name === "string"
|
|
@@ -1480,27 +1848,18 @@ try {
|
|
|
1480
1848
|
setStatus(t("msg.nameRequired"))
|
|
1481
1849
|
return
|
|
1482
1850
|
}
|
|
1483
|
-
|
|
1484
|
-
try {
|
|
1851
|
+
return runOperation("status.creating", "msg.createFailed", async (token) => {
|
|
1485
1852
|
const body = { name, locale: activeLanguage(localeRef) }
|
|
1486
1853
|
if (from !== undefined) body.from = from
|
|
1487
1854
|
else if (postureChoices.some((item) => item.id === posture)) body.posture = posture
|
|
1488
1855
|
const result = await postJson(ROUTES.create, body)
|
|
1489
|
-
if (
|
|
1856
|
+
if (!ownsOperation(token)) return
|
|
1857
|
+
showResult(result, from === undefined ? "msg.created" : "msg.duplicated", "msg.createFailed")
|
|
1858
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1490
1859
|
setNewName("")
|
|
1491
|
-
|
|
1492
|
-
setStatus(apiText(result, from === undefined ? "msg.created" : "msg.duplicated"))
|
|
1493
|
-
await reload(result.id)
|
|
1494
|
-
} else {
|
|
1495
|
-
setFailed(true)
|
|
1496
|
-
setStatus(apiText(result, "msg.createFailed"))
|
|
1860
|
+
await refreshAfterWrite(result, from === undefined ? "msg.created" : "msg.duplicated", result.id, token, () => reload(result.id, undefined, token))
|
|
1497
1861
|
}
|
|
1498
|
-
}
|
|
1499
|
-
setFailed(true)
|
|
1500
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.createFailed"), detail: describeError(error) }))
|
|
1501
|
-
} finally {
|
|
1502
|
-
setBusy(false)
|
|
1503
|
-
}
|
|
1862
|
+
})
|
|
1504
1863
|
}
|
|
1505
1864
|
|
|
1506
1865
|
const duplicate = () => create({ from: draft.id, name: copyName(t("assistant.copyName"), draft.name || draft.id) })
|
|
@@ -1512,23 +1871,14 @@ try {
|
|
|
1512
1871
|
* (the default) and only the roster order changes.
|
|
1513
1872
|
*/
|
|
1514
1873
|
const move = async (direction) => {
|
|
1515
|
-
|
|
1516
|
-
try {
|
|
1874
|
+
return runOperation("status.reordering", "msg.reorderFailed", async (token) => {
|
|
1517
1875
|
const result = await postJson(ROUTES.reorder, { id: draft.id, direction })
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
await reload(draft.id)
|
|
1522
|
-
} else {
|
|
1523
|
-
setFailed(true)
|
|
1524
|
-
setStatus(apiText(result, "msg.reorderFailed"))
|
|
1876
|
+
if (!ownsOperation(token)) return
|
|
1877
|
+
showResult(result, "msg.reordered", "msg.reorderFailed")
|
|
1878
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1879
|
+
await refreshAfterWrite(result, "msg.reordered", draft.id, token, () => reload(draft.id, undefined, token))
|
|
1525
1880
|
}
|
|
1526
|
-
}
|
|
1527
|
-
setFailed(true)
|
|
1528
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.reorderFailed"), detail: describe(error) }))
|
|
1529
|
-
} finally {
|
|
1530
|
-
setBusy(false)
|
|
1531
|
-
}
|
|
1881
|
+
})
|
|
1532
1882
|
}
|
|
1533
1883
|
|
|
1534
1884
|
/** Download the prompt being edited (client-side only — the host is not involved). */
|
|
@@ -1548,18 +1898,20 @@ try {
|
|
|
1548
1898
|
const loadVersion = async () => {
|
|
1549
1899
|
const picked = typeof draft.historyPick === "string" ? draft.historyPick : ""
|
|
1550
1900
|
if (picked === "" || draft.id === undefined) return
|
|
1551
|
-
|
|
1901
|
+
return runOperation("status.reading", "msg.loadFailed", async (token) => {
|
|
1552
1902
|
// 版本键是序号(时间戳会在同一毫秒内撞车 —— 单测就是那样抓到它的)。
|
|
1553
1903
|
const response = await fetch(ROUTES.history + "?id=" + encodeURIComponent(draft.id) + "&n=" + encodeURIComponent(picked))
|
|
1554
1904
|
const payload = await asJson(response)
|
|
1905
|
+
if (!ownsOperation(token)) return
|
|
1555
1906
|
if (payload === null || payload.ok !== true) {
|
|
1907
|
+
setFailed(true)
|
|
1556
1908
|
setStatus(t("msg.loadFailed"))
|
|
1557
1909
|
return
|
|
1558
1910
|
}
|
|
1559
|
-
update({ prompt: payload.text })
|
|
1560
|
-
|
|
1561
|
-
setStatus(
|
|
1562
|
-
}
|
|
1911
|
+
update({ prompt: payload.text }, { force: true })
|
|
1912
|
+
setFailed(false)
|
|
1913
|
+
setStatus(t("msg.loaded"))
|
|
1914
|
+
})
|
|
1563
1915
|
}
|
|
1564
1916
|
|
|
1565
1917
|
const exportPrompt = () => {
|
|
@@ -1588,61 +1940,54 @@ try {
|
|
|
1588
1940
|
* path (and the same `{{…}}` validation) as anything typed by hand — an import can
|
|
1589
1941
|
* never silently publish a prompt the renderer would refuse.
|
|
1590
1942
|
*/
|
|
1591
|
-
const importPrompt = (event) => {
|
|
1943
|
+
const importPrompt = async (event) => {
|
|
1592
1944
|
const input = event !== null && event !== undefined ? event.target : undefined
|
|
1593
1945
|
const file = input !== null && input !== undefined && input.files !== undefined ? input.files[0] : undefined
|
|
1594
1946
|
// Reset so picking the same file twice in a row fires `change` again.
|
|
1595
1947
|
if (input !== null && input !== undefined) input.value = ""
|
|
1948
|
+
if (operation.current.current !== null) return
|
|
1596
1949
|
if (file === undefined || file === null) return
|
|
1597
1950
|
if (file.size > 1_000_000) {
|
|
1598
1951
|
setFailed(true)
|
|
1599
1952
|
setStatus(t("msg.importTooLarge"))
|
|
1600
1953
|
return
|
|
1601
1954
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1955
|
+
return runOperation("status.importing", "msg.importFailed", async (token) => {
|
|
1956
|
+
const text = await new Promise((resolve, reject) => {
|
|
1957
|
+
const reader = new FileReader()
|
|
1958
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "")
|
|
1959
|
+
reader.onerror = () => reject(reader.error || new Error(t("msg.importFailed")))
|
|
1960
|
+
reader.onabort = () => reject(new Error(t("msg.importFailed")))
|
|
1961
|
+
reader.readAsText(file, "utf-8")
|
|
1962
|
+
})
|
|
1963
|
+
if (!ownsOperation(token)) return
|
|
1605
1964
|
if (text.trim() === "") {
|
|
1606
1965
|
setFailed(true)
|
|
1607
1966
|
setStatus(t("msg.importEmpty"))
|
|
1608
1967
|
return
|
|
1609
1968
|
}
|
|
1610
|
-
update({ prompt: text })
|
|
1969
|
+
update({ prompt: text }, { force: true })
|
|
1611
1970
|
setFailed(false)
|
|
1612
1971
|
setStatus(t("msg.imported"))
|
|
1613
|
-
}
|
|
1614
|
-
reader.onerror = () => {
|
|
1615
|
-
setFailed(true)
|
|
1616
|
-
setStatus(t("msg.importFailed"))
|
|
1617
|
-
}
|
|
1618
|
-
reader.readAsText(file, "utf-8")
|
|
1972
|
+
})
|
|
1619
1973
|
}
|
|
1620
1974
|
|
|
1621
1975
|
const remove = async (id) => {
|
|
1622
|
-
|
|
1623
|
-
try {
|
|
1976
|
+
return runOperation("status.deleting", "msg.deleteFailed", async (token) => {
|
|
1624
1977
|
const result = await postJson(ROUTES.delete, { id })
|
|
1625
|
-
if (
|
|
1978
|
+
if (!ownsOperation(token)) return
|
|
1979
|
+
showResult(result, "msg.deleted", "msg.deleteFailed")
|
|
1980
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1626
1981
|
setDeleteOpen(false)
|
|
1627
1982
|
setAcknowledged(false)
|
|
1628
|
-
setFailed(false)
|
|
1629
|
-
setStatus(apiText(result, "msg.deleted"))
|
|
1630
1983
|
setEntries((previous) => {
|
|
1631
1984
|
const next = { ...previous }
|
|
1632
1985
|
delete next[id]
|
|
1633
1986
|
return next
|
|
1634
1987
|
})
|
|
1635
|
-
await reload("")
|
|
1636
|
-
} else {
|
|
1637
|
-
setFailed(true)
|
|
1638
|
-
setStatus(apiText(result, "msg.deleteFailed"))
|
|
1988
|
+
await refreshAfterWrite(result, "msg.deleted", "", token, () => reload("", undefined, token))
|
|
1639
1989
|
}
|
|
1640
|
-
}
|
|
1641
|
-
setFailed(true)
|
|
1642
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.deleteFailed"), detail: describeError(error) }))
|
|
1643
|
-
} finally {
|
|
1644
|
-
setBusy(false)
|
|
1645
|
-
}
|
|
1990
|
+
})
|
|
1646
1991
|
}
|
|
1647
1992
|
|
|
1648
1993
|
/**
|
|
@@ -1669,9 +2014,10 @@ try {
|
|
|
1669
2014
|
}
|
|
1670
2015
|
|
|
1671
2016
|
const save = async () => {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
2017
|
+
if (draft === null) return
|
|
2018
|
+
return runOperation("status.saving", "msg.saveFailed", async (token) => {
|
|
2019
|
+
const savedHistory = entry?.saved?.history
|
|
2020
|
+
const newest = Array.isArray(savedHistory) && savedHistory.length > 0 ? savedHistory[0].n : undefined
|
|
1675
2021
|
const result = await postJson(ROUTES.state, {
|
|
1676
2022
|
id: draft.id,
|
|
1677
2023
|
mode: draft.mode,
|
|
@@ -1681,6 +2027,9 @@ try {
|
|
|
1681
2027
|
description: draft.description,
|
|
1682
2028
|
...(typeof newest === "number" ? { baseN: newest } : {}),
|
|
1683
2029
|
})
|
|
2030
|
+
if (!ownsOperation(token)) return
|
|
2031
|
+
if (result?.synchronized === false) setSyncNeeded(true)
|
|
2032
|
+
else if (result?.synchronized === true) setSyncNeeded(false)
|
|
1684
2033
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1685
2034
|
setFailed(false)
|
|
1686
2035
|
const compositionChanged =
|
|
@@ -1693,42 +2042,66 @@ try {
|
|
|
1693
2042
|
// The server normalises what it stores (trimmed name, recomputed overrides),
|
|
1694
2043
|
// so the just-saved draft is replaced by what it actually wrote. Without this
|
|
1695
2044
|
// the page would show 「已保存」 and 「未保存」 at the same time.
|
|
1696
|
-
|
|
1697
|
-
|
|
2045
|
+
await refreshAfterWrite(spoken, "msg.saved", draft.id, token, async () => {
|
|
2046
|
+
const fresh = await fetchState(draft.id)
|
|
2047
|
+
if (!ownsOperation(token)) return
|
|
2048
|
+
if (fresh?.ok !== true) throw new Error(apiText(fresh, "msg.readFailed"))
|
|
1698
2049
|
const normalized = draftOf(fresh)
|
|
1699
2050
|
setEntries((previous) => ({
|
|
1700
2051
|
...previous,
|
|
1701
2052
|
[draft.id]: { payload: fresh, saved: normalized, value: normalized },
|
|
1702
2053
|
}))
|
|
1703
|
-
await refreshNames()
|
|
1704
|
-
}
|
|
1705
|
-
await reload(draft.id, { discard: true })
|
|
1706
|
-
}
|
|
2054
|
+
await refreshNames(token)
|
|
2055
|
+
})
|
|
1707
2056
|
} else {
|
|
1708
2057
|
setFailed(true)
|
|
1709
2058
|
setStatus(apiText(result, "msg.saveFailed"))
|
|
1710
2059
|
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2060
|
+
})
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const retrySync = () => runOperation("status.synchronizing", "msg.syncFailed", async (token) => {
|
|
2064
|
+
const result = await postJson(ROUTES.sync, {})
|
|
2065
|
+
if (!ownsOperation(token)) return
|
|
2066
|
+
showResult(result, "msg.synchronized", "msg.syncFailed")
|
|
2067
|
+
if (result?.ok === true) {
|
|
2068
|
+
await refreshAfterWrite(result, "msg.synchronized", selected, token, () => reload(selected, undefined, token))
|
|
1716
2069
|
}
|
|
2070
|
+
})
|
|
2071
|
+
|
|
2072
|
+
const retryRead = () => {
|
|
2073
|
+
if (refreshNeeded === null) return
|
|
2074
|
+
const { result, fallbackKey, targetId } = refreshNeeded
|
|
2075
|
+
return runOperation("status.reading", "msg.readFailed", (token) =>
|
|
2076
|
+
refreshAfterWrite(result, fallbackKey, targetId, token, async () => {
|
|
2077
|
+
await reload(targetId, undefined, token)
|
|
2078
|
+
if (ownsOperation(token)) showResult(result, fallbackKey, fallbackKey)
|
|
2079
|
+
}),
|
|
2080
|
+
)
|
|
1717
2081
|
}
|
|
1718
2082
|
|
|
1719
2083
|
const assistants = list === null ? [] : list
|
|
1720
2084
|
const position = assistants.findIndex((item) => item.id === selected)
|
|
1721
|
-
const statusClass =
|
|
2085
|
+
const statusClass = busy
|
|
2086
|
+
? "cpfe-status"
|
|
2087
|
+
: failed
|
|
1722
2088
|
? "cpfe-status cpfe-err"
|
|
1723
2089
|
: dirty
|
|
1724
2090
|
? "cpfe-status cpfe-dirty"
|
|
1725
2091
|
: "cpfe-status cpfe-ok"
|
|
1726
|
-
const shown = dirty && status === "" ? t("msg.unsaved") : status
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2092
|
+
const shown = pending !== "" ? t(pending) : dirty && status === "" ? t("msg.unsaved") : status
|
|
2093
|
+
const feedback = [
|
|
2094
|
+
refreshNeeded !== null
|
|
2095
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: retryRead }, t("btn.retryRead"))
|
|
2096
|
+
: null,
|
|
2097
|
+
refreshNeeded === null && !editorReady && failed && list !== null && selected !== ""
|
|
2098
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: () => reread() }, t("btn.reload"))
|
|
2099
|
+
: null,
|
|
2100
|
+
syncNeeded
|
|
2101
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: retrySync }, t("btn.retrySync"))
|
|
2102
|
+
: null,
|
|
2103
|
+
react.createElement("span", { className: statusClass, role: "status", "aria-live": "polite", "aria-atomic": true }, shown),
|
|
2104
|
+
]
|
|
1732
2105
|
const createGroup = react.createElement(
|
|
1733
2106
|
"section",
|
|
1734
2107
|
{ className: "cpfe-create" },
|
|
@@ -1766,24 +2139,6 @@ try {
|
|
|
1766
2139
|
"div",
|
|
1767
2140
|
{ className: "cpfe-node" },
|
|
1768
2141
|
react.createElement("span", { className: "cpfe-node-label" }, t("tree.base")),
|
|
1769
|
-
react.createElement(
|
|
1770
|
-
"div",
|
|
1771
|
-
{ className: "cpfe-pills cpfe-locked", role: "radiogroup", "aria-disabled": "true", "aria-label": t("tree.base") },
|
|
1772
|
-
lockedModes.map((mode) =>
|
|
1773
|
-
react.createElement(
|
|
1774
|
-
A.Pill,
|
|
1775
|
-
{
|
|
1776
|
-
key: mode.id,
|
|
1777
|
-
active: mode.id === "standard",
|
|
1778
|
-
disabled: true,
|
|
1779
|
-
"aria-pressed": mode.id === "standard",
|
|
1780
|
-
title: mode.id === "standard" ? t("tree.baseHold") : t("tree.baseSkip"),
|
|
1781
|
-
onClick: () => {},
|
|
1782
|
-
},
|
|
1783
|
-
t("base." + mode.id + ".label", mode.label),
|
|
1784
|
-
),
|
|
1785
|
-
),
|
|
1786
|
-
),
|
|
1787
2142
|
react.createElement("p", { className: "cpfe-note" }, t("tree.baseLocked")),
|
|
1788
2143
|
),
|
|
1789
2144
|
react.createElement(
|
|
@@ -1792,6 +2147,7 @@ try {
|
|
|
1792
2147
|
react.createElement(A.Input, {
|
|
1793
2148
|
className: "cpfe-newinput",
|
|
1794
2149
|
value: newName,
|
|
2150
|
+
disabled: busy,
|
|
1795
2151
|
placeholder: t("assistant.newPlaceholder"),
|
|
1796
2152
|
"aria-label": t("assistant.newPlaceholder"),
|
|
1797
2153
|
onKeyDown: (event) => {
|
|
@@ -1810,13 +2166,78 @@ try {
|
|
|
1810
2166
|
disabled: busy,
|
|
1811
2167
|
onClick: () => create(),
|
|
1812
2168
|
},
|
|
1813
|
-
|
|
2169
|
+
pending === "status.creating" ? t("btn.creating") : t("btn.create"),
|
|
1814
2170
|
),
|
|
1815
2171
|
),
|
|
2172
|
+
react.createElement("p", { className: "cpfe-note" }, t("create.nextStep")),
|
|
1816
2173
|
),
|
|
1817
2174
|
)
|
|
1818
2175
|
|
|
1819
2176
|
// ── assistant list ────────────────────────────────────────────────────
|
|
2177
|
+
const nativeUpdates = hasNativeUpdates(updateServices)
|
|
2178
|
+
const updateAllowed = nativeUpdates && updateServices.list && updateServices.versions
|
|
2179
|
+
const checkedUpdate = updateResult?.revision === updateServices.revision ? updateResult : null
|
|
2180
|
+
const navigationBlock = updateNavigationBlock()
|
|
2181
|
+
const updateText = updateChecking ? t("update.checking") : checkedUpdate
|
|
2182
|
+
? fillPlaceholders(t(checkedUpdate.key), { version: checkedUpdate.candidate })
|
|
2183
|
+
: !nativeUpdates ? t("update.unavailable") : !updateAllowed ? t("update.permission") : t("update.idle")
|
|
2184
|
+
const shownOrphans = orphanDrafts()
|
|
2185
|
+
const orphanSnapshot = { entries: heldEntries.current, roster: heldRoster.current, newName: navigationState.current.newName }
|
|
2186
|
+
const updateSection = react.createElement(
|
|
2187
|
+
"section", { className: "cpfe-update", "aria-label": t("update.heading"), "aria-busy": updateChecking },
|
|
2188
|
+
react.createElement("div", { className: "cpfe-update-meta" },
|
|
2189
|
+
react.createElement("h2", { className: "cpfe-h" }, t("update.heading")),
|
|
2190
|
+
react.createElement("p", { className: "cpfe-version" }, fillPlaceholders(t("update.running"), { version: runtimeVersion || t("update.unknown") })),
|
|
2191
|
+
react.createElement("p", { id: "cpfe-update-status", className: "cpfe-sub cpfe-update-note", role: "status", "aria-live": "polite", "aria-atomic": true }, updateText),
|
|
2192
|
+
checkedUpdate?.installed && checkedUpdate.installed !== runtimeVersion
|
|
2193
|
+
? react.createElement("p", { className: "cpfe-sub cpfe-update-note" }, fillPlaceholders(t("update.disk"), { version: checkedUpdate.installed })) : null,
|
|
2194
|
+
checkedUpdate?.omitted > 0
|
|
2195
|
+
? react.createElement("p", { className: "cpfe-sub" }, fillPlaceholders(t("update.omitted"), { count: checkedUpdate.omitted })) : null,
|
|
2196
|
+
checkedUpdate?.candidate && !updateServices.update ? react.createElement("p", { className: "cpfe-sub" }, t("update.updatePermission")) : null,
|
|
2197
|
+
updateNotice || navigationBlock ? react.createElement("p", { className: "cpfe-sub", role: "status" }, t(updateNotice || navigationBlock)) : null,
|
|
2198
|
+
shownOrphans.length > 0 ? react.createElement("div", { className: "cpfe-warns cpfe-update-note" },
|
|
2199
|
+
react.createElement("p", { className: "cpfe-sub" }, t("update.orphanHint")),
|
|
2200
|
+
...shownOrphans.map(([id, held]) => react.createElement("div", { key: id, className: "cpfe-node" },
|
|
2201
|
+
react.createElement("span", { className: "cpfe-row-head" }, fillPlaceholders(t("update.orphanName"), { name: held.value.name || id, id })),
|
|
2202
|
+
react.createElement(A.Button, { variant: "ghost", className: "cpfe-disclosure", "aria-expanded": orphanView === id,
|
|
2203
|
+
onClick: () => setOrphanView(orphanView === id ? "" : id) }, t(orphanView === id ? "update.orphanHide" : "update.orphanView")),
|
|
2204
|
+
orphanView === id ? react.createElement("div", { className: "cpfe-node" },
|
|
2205
|
+
react.createElement("p", { className: "cpfe-sub" }, t("update.orphanCopy")),
|
|
2206
|
+
react.createElement("textarea", { className: "cpfe-editor cpfe-orphan-editor", readOnly: true, spellCheck: false,
|
|
2207
|
+
"aria-label": fillPlaceholders(t("update.orphanName"), { name: held.value.name || id, id }),
|
|
2208
|
+
value: JSON.stringify({ id, name: held.value.name, description: held.value.description, mode: held.value.mode, overrides: held.value.overrides, prompt: held.value.prompt }, null, 2) }),
|
|
2209
|
+
react.createElement(A.Button, { variant: "outline", className: "cpfe-disclosure", disabled: orphanActionBlock() !== "",
|
|
2210
|
+
onClick: () => askDiscardOrphan(id, orphanSnapshot) }, t("update.orphanDiscard")),
|
|
2211
|
+
) : null,
|
|
2212
|
+
)),
|
|
2213
|
+
) : null,
|
|
2214
|
+
),
|
|
2215
|
+
react.createElement("div", { className: "cpfe-update-actions" },
|
|
2216
|
+
react.createElement(A.Button, {
|
|
2217
|
+
variant: "outline", disabled: busy || updateChecking || !updateAllowed,
|
|
2218
|
+
"aria-describedby": "cpfe-update-status", onClick: checkUpdates,
|
|
2219
|
+
}, t(updateChecking ? "update.checking" : "update.check")),
|
|
2220
|
+
react.createElement(A.Button, {
|
|
2221
|
+
variant: "outline",
|
|
2222
|
+
disabled: navigationBlock !== "" || ((nativeUpdates || updateServices.accessKnown) && !updateServices.list) || typeof updateServices.navigation?.openBundle !== "function" ||
|
|
2223
|
+
(!!checkedUpdate?.candidate && !updateServices.update),
|
|
2224
|
+
title: navigationBlock ? t(navigationBlock) : undefined,
|
|
2225
|
+
onClick: openPluginDetails,
|
|
2226
|
+
}, t(checkedUpdate?.candidate ? "update.inPlugins" : "update.details")),
|
|
2227
|
+
react.createElement("a", {
|
|
2228
|
+
className: "cpfe-update-guide",
|
|
2229
|
+
href: "https://github.com/BOWLUNA/dsh-custom-mode/blob/main/README" + (activeLanguage(localeRef) === "zh" ? ".zh" : "") + ".md#plugin-updates",
|
|
2230
|
+
target: "_blank", rel: "noopener noreferrer", "aria-disabled": navigationBlock !== "",
|
|
2231
|
+
onClick: (event) => { if (!guardUpdateNavigation()) event.preventDefault() },
|
|
2232
|
+
}, t("update.guide")),
|
|
2233
|
+
),
|
|
2234
|
+
)
|
|
2235
|
+
|
|
2236
|
+
const selectedAssistantLabel = (assistants.find((item) => item.id === selected) || {}).name || selected || t("assistant.heading")
|
|
2237
|
+
const pickedHistoryEntry = draft === null || draft.historyPick === "" ? undefined : draft.history.find((item) => String(item.n) === draft.historyPick)
|
|
2238
|
+
const historySelectionLabel = pickedHistoryEntry === undefined
|
|
2239
|
+
? t("history.pick")
|
|
2240
|
+
: formatWhen(pickedHistoryEntry.at) + " · " + t("history.by." + pickedHistoryEntry.by)
|
|
1820
2241
|
const assistantList = react.createElement(
|
|
1821
2242
|
"section",
|
|
1822
2243
|
null,
|
|
@@ -1828,7 +2249,7 @@ try {
|
|
|
1828
2249
|
null,
|
|
1829
2250
|
react.createElement("p", { className: "cpfe-sub" }, listFailed ? t("msg.readFailed") : t("assistant.loadingList")),
|
|
1830
2251
|
listFailed
|
|
1831
|
-
? react.createElement(A.Button, { disabled: busy, onClick: () =>
|
|
2252
|
+
? react.createElement(A.Button, { disabled: busy, onClick: () => reread() }, t("btn.reload"))
|
|
1832
2253
|
: null,
|
|
1833
2254
|
)
|
|
1834
2255
|
: assistants.length === 0
|
|
@@ -1842,6 +2263,7 @@ try {
|
|
|
1842
2263
|
renderable(A.Menu)
|
|
1843
2264
|
? react.createElement(A.Menu, {
|
|
1844
2265
|
open: assistantOpen,
|
|
2266
|
+
className: "cpfe-selector-menu",
|
|
1845
2267
|
// ★ portal:不把浮层渲染在我们的容器里。设置面板有裁剪与叠层,非 portal 的
|
|
1846
2268
|
// 菜单会被裁掉表面,只剩文字"压"在下面的输入框上(实测截图如此)。
|
|
1847
2269
|
portal: true,
|
|
@@ -1853,15 +2275,16 @@ try {
|
|
|
1853
2275
|
{
|
|
1854
2276
|
variant: "toolbar",
|
|
1855
2277
|
className: "cpfe-selector",
|
|
2278
|
+
title: selectedAssistantLabel,
|
|
1856
2279
|
disabled: busy,
|
|
1857
2280
|
onClick: () => setAssistantOpen((open) => open !== true),
|
|
1858
2281
|
},
|
|
1859
|
-
|
|
2282
|
+
react.createElement("span", { className: "cpfe-selector-label" }, selectedAssistantLabel),
|
|
1860
2283
|
// 官方设置项的下拉是「文字在左、chevron 在右」(实测「通用设置」页的
|
|
1861
2284
|
// 「工作区内修改 / 中文 / 详细」都是这个形态)⇒ 图标放在 children 末尾,
|
|
1862
2285
|
// 不走 `icon` prop —— 那个会渲染到文字**前面**,成为页面上唯一一个反向的下拉。
|
|
1863
2286
|
renderable(A.IconChevronDownOutline14)
|
|
1864
|
-
? react.createElement(A.IconChevronDownOutline14, { size: 14 })
|
|
2287
|
+
? react.createElement(A.IconChevronDownOutline14, { size: 14, className: "cpfe-selector-chevron", "aria-hidden": true })
|
|
1865
2288
|
: null,
|
|
1866
2289
|
),
|
|
1867
2290
|
items: assistants.map((item) => ({
|
|
@@ -2166,6 +2589,7 @@ try {
|
|
|
2166
2589
|
renderable(A.Menu)
|
|
2167
2590
|
? react.createElement(A.Menu, {
|
|
2168
2591
|
open: historyOpen,
|
|
2592
|
+
className: "cpfe-selector-menu",
|
|
2169
2593
|
portal: true,
|
|
2170
2594
|
align: "start",
|
|
2171
2595
|
side: "bottom",
|
|
@@ -2175,19 +2599,13 @@ try {
|
|
|
2175
2599
|
{
|
|
2176
2600
|
variant: "toolbar",
|
|
2177
2601
|
className: "cpfe-selector",
|
|
2602
|
+
title: historySelectionLabel,
|
|
2178
2603
|
disabled: busy,
|
|
2179
2604
|
onClick: () => setHistoryOpen((open) => open !== true),
|
|
2180
2605
|
},
|
|
2181
|
-
|
|
2182
|
-
? t("history.pick")
|
|
2183
|
-
: (() => {
|
|
2184
|
-
const entry = draft.history.find((item) => String(item.n) === draft.historyPick)
|
|
2185
|
-
return entry === undefined
|
|
2186
|
-
? t("history.pick")
|
|
2187
|
-
: formatWhen(entry.at) + " · " + t("history.by." + entry.by)
|
|
2188
|
-
})(),
|
|
2606
|
+
react.createElement("span", { className: "cpfe-selector-label" }, historySelectionLabel),
|
|
2189
2607
|
renderable(A.IconChevronDownOutline14)
|
|
2190
|
-
? react.createElement(A.IconChevronDownOutline14, { size: 14 })
|
|
2608
|
+
? react.createElement(A.IconChevronDownOutline14, { size: 14, className: "cpfe-selector-chevron", "aria-hidden": true })
|
|
2191
2609
|
: null,
|
|
2192
2610
|
),
|
|
2193
2611
|
items: draft.history.map((entry) => ({
|
|
@@ -2236,9 +2654,10 @@ try {
|
|
|
2236
2654
|
"div",
|
|
2237
2655
|
// `cpfe-busy` 是 issue #6 的兜底:面板里**所有**会改草稿的控件在忙碌期间一律不可交互。
|
|
2238
2656
|
// 逐个控件加 `disabled` 会漏(本轮就漏了行开关与描述框),一个根类 + 一条 CSS 反而漏不掉。
|
|
2239
|
-
{ className: busy === true ? "cpfe cpfe-busy" : "cpfe" },
|
|
2657
|
+
{ className: busy === true ? "cpfe cpfe-busy" : "cpfe", "aria-busy": busy === true },
|
|
2658
|
+
updateSection,
|
|
2240
2659
|
assistantList,
|
|
2241
|
-
null,
|
|
2660
|
+
editorReady ? null : react.createElement("div", { className: "cpfe-bar" }, ...feedback),
|
|
2242
2661
|
// 配了却不生效的项:主动点名,而不是让用户对着"我明明写了"发呆。
|
|
2243
2662
|
// `draft` 在没选中任何助手时是 null(列表还没加载完 / 一个都没有)—— 这里必须先守卫,
|
|
2244
2663
|
// 否则整块设置页崩掉(实测:浏览器验收当场报 Cannot read properties of null)。
|
|
@@ -2296,7 +2715,7 @@ try {
|
|
|
2296
2715
|
"section",
|
|
2297
2716
|
null,
|
|
2298
2717
|
react.createElement("h2", { className: "cpfe-h" }, t("editor.heading")),
|
|
2299
|
-
react.createElement(SectionHint, { text: t("editor.hint") }),
|
|
2718
|
+
react.createElement(SectionHint, { text: fillPlaceholders(t("editor.hint"), { name: draft.name || draft.id }) }),
|
|
2300
2719
|
react.createElement(
|
|
2301
2720
|
"div",
|
|
2302
2721
|
{ className: "cpfe-branch" },
|
|
@@ -2309,10 +2728,10 @@ try {
|
|
|
2309
2728
|
{
|
|
2310
2729
|
variant: "primary",
|
|
2311
2730
|
icon: A.IconCheckOutline14 === undefined ? null : react.createElement(A.IconCheckOutline14),
|
|
2312
|
-
disabled: busy || editorReady === false || dirty === false,
|
|
2731
|
+
disabled: busy || syncNeeded || editorReady === false || dirty === false,
|
|
2313
2732
|
onClick: save,
|
|
2314
2733
|
},
|
|
2315
|
-
|
|
2734
|
+
pending === "status.saving" ? t("btn.saving") : t("btn.save"),
|
|
2316
2735
|
),
|
|
2317
2736
|
react.createElement(
|
|
2318
2737
|
A.Button,
|
|
@@ -2325,24 +2744,14 @@ try {
|
|
|
2325
2744
|
// one place that does.
|
|
2326
2745
|
onClick: () => {
|
|
2327
2746
|
if (dirty && typeof window !== "undefined" && window.confirm(t("btn.reloadConfirm")) !== true) return
|
|
2328
|
-
|
|
2747
|
+
reread(true)
|
|
2329
2748
|
},
|
|
2330
2749
|
},
|
|
2331
2750
|
dirty ? t("btn.reloadDiscard") : t("btn.reload"),
|
|
2332
2751
|
),
|
|
2333
|
-
|
|
2752
|
+
...feedback,
|
|
2334
2753
|
),
|
|
2335
2754
|
...(editorReady ? editorSections.filter((section) => section.key === "name") : []),
|
|
2336
|
-
editorReady
|
|
2337
|
-
? react.createElement(
|
|
2338
|
-
"p",
|
|
2339
|
-
{
|
|
2340
|
-
className: "cpfe-version",
|
|
2341
|
-
title: String(payload.compositionPath ?? "") + "\n" + t("meta.versionHint"),
|
|
2342
|
-
},
|
|
2343
|
-
"v" + String(payload.version ?? "?"),
|
|
2344
|
-
)
|
|
2345
|
-
: null,
|
|
2346
2755
|
...(editorReady ? editorSections.filter((section) => section.key === "mode") : []),
|
|
2347
2756
|
editorReady
|
|
2348
2757
|
? react.createElement(
|
|
@@ -2382,6 +2791,21 @@ try {
|
|
|
2382
2791
|
createGroup,
|
|
2383
2792
|
// The confirmation is a portal: rendering it here keeps every piece of this page's
|
|
2384
2793
|
// state in one component.
|
|
2794
|
+
renderable(A.RiskConfirmation) && orphanDialog !== null
|
|
2795
|
+
? react.createElement(A.RiskConfirmation, {
|
|
2796
|
+
open: true, title: t("update.orphanDiscard"),
|
|
2797
|
+
description: fillPlaceholders(t("update.orphanConfirm"), { name: orphanDialog.name, id: orphanDialog.id }),
|
|
2798
|
+
acknowledgeLabel: t("update.orphanAcknowledge"), acknowledged: orphanAcknowledged,
|
|
2799
|
+
onAcknowledgedChange: (value) => {
|
|
2800
|
+
if (!updateOperation.current.alive || orphanConfirmation.current !== orphanDialog) return
|
|
2801
|
+
orphanAcknowledgement.current = value === true
|
|
2802
|
+
setOrphanAcknowledged(value === true)
|
|
2803
|
+
},
|
|
2804
|
+
cancelLabel: t("btn.cancel"), closeLabel: t("btn.cancel"), confirmLabel: t("update.orphanDiscard"),
|
|
2805
|
+
disabled: orphanActionBlock() !== "", onCancel: () => { if (updateOperation.current.alive && orphanConfirmation.current === orphanDialog) closeOrphanConfirmation() },
|
|
2806
|
+
onConfirm: () => discardOrphan(orphanDialog),
|
|
2807
|
+
})
|
|
2808
|
+
: null,
|
|
2385
2809
|
renderable(A.RiskConfirmation) && editorReady
|
|
2386
2810
|
? react.createElement(A.RiskConfirmation, {
|
|
2387
2811
|
open: deleteOpen,
|
|
@@ -2439,6 +2863,8 @@ try {
|
|
|
2439
2863
|
console.warn("dsh-custom-mode: slots service unavailable; settings page not registered")
|
|
2440
2864
|
return
|
|
2441
2865
|
}
|
|
2866
|
+
try { mountUpdateServices(ctx) }
|
|
2867
|
+
catch { console.warn("dsh-custom-mode: optional native update services unavailable") }
|
|
2442
2868
|
slots.inject("settings.section", () => {
|
|
2443
2869
|
// `locale: NS` is the shipped contract: the shell then hands the
|
|
2444
2870
|
// component a bound `t` in props.
|