Repository navigation
Expand file tree
/
Copy pathclient.js
More file actions
2477 lines (2408 loc) · 132 KB
/
Copy pathclient.js
File metadata and controls
2477 lines (2408 loc) · 132 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Browser half: 「自定义模式」settings page — the assistant manager.
*
* Blocks:
* 0. 助手 — which assistant is selected.
* 1. 当前助手 — prompt, name, five live base modes, plugin switches.
* 2. 新建助手 — Coding / Writing / Chat stay selectable. The official
* modes are shown beside them, and only Standard is lit.
*
* Three decisions worth knowing before editing this file:
*
* 1. **Controls come from the shell, not from here.** `@deepseek-ai/dsh-client-ui-primitives`
* is in the shell's seed table (`require` resolves it like `react`), so Button / Input /
* Switch / Tag / Pill / RiskConfirmation / icons are the same atoms every official page
* uses. They are styled by the shell's own CSS through `--dsw-*` tokens, which is what makes
* this page follow the theme, density and future restyling instead of drifting from it.
* The atoms are probed, never assumed: a shell that does not seed the module falls back to
* plain elements below, so an older dsh gets a plainer page rather than a missing one.
*
* 2. **Drafts are per assistant and never discarded by switching.** `entries[id]` holds the
* loaded payload, the saved baseline and the current value; selecting another assistant
* keeps the old draft, and the list marks it 「未保存」. Losing a hand-written system prompt
* to a stray click is the one failure this page must not have.
*
* 3. **Deleting goes through the shell's RiskConfirmation.** It removes a directory and a
* prompt permanently, so it asks for an explicit acknowledgement instead of a second click
* next to the primary button.
*
* Hand-written in the client bundle format the module system expects
* (`window.__ModuleLoader__.load({ id, factory })`). It talks to its host half over one private
* HTTP route and its sub-paths with `fetch`, so it needs no Remote namespace.
*
* Failure policy: everything is wrapped so a broken editor can never take the page down. Worst
* case is that this settings page does not appear.
*/
try {
window.__ModuleLoader__.load({
id: "dsh-custom-mode",
factory: (require) => {
var module = { exports: {} }
var exports = module.exports
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
const react = require("react")
// 路由注册在平台的共享 `/api` 频道上(由载体在分发前施加信任与鉴权),
// 所以这里的路径必须带 `/api` 前缀。
const ROUTE = "/api/custom-mode"
const NS = "settings.customMode"
/** The shell's shared atom library. See decision 1 in the module comment. */
const ATOMS_MODULE = "@deepseek-ai/dsh-client-ui-primitives"
/**
* Copy for this page, generated from locales.mjs (single source of
* truth, checked for key parity by test/locales.test.mjs).
*/
const ZH = {
"nav": "自定义模式",
"assistant.heading": "助手",
"assistant.hint": "每个助手单独保存系统提示词、基础模式和插件开关。",
"posture.hint": "这一支只在点「新增助手」时写入。",
"tree.posture": "起步",
"tree.base": "工具底",
"tree.baseHold": "新建时固定用官方标准模式。",
"tree.baseSkip": "这一支点不了。建好后到「当前助手」里改。",
"tree.baseLocked": "只有官方标准模式是亮的。其余四档是暗的,新建时点不了。",
"posture.develop.label": "开发",
"posture.develop.note": "官方标准模式,工具按出厂。",
"posture.write.label": "写作",
"posture.write.note": "官方标准模式。终端关闭,文件读写打开。",
"posture.chat.label": "聊天",
"posture.chat.note": "官方标准模式。文件和终端关闭。",
"editor.heading": "当前助手",
"editor.hint": "这一支改上面选中的助手。基础模式五档都可以点。",
"assistant.empty": "还没有助手,用下面的输入框新建一个。",
"assistant.loadingList": "正在读取助手列表…",
"assistant.newPlaceholder": "新助手的名字(例如:写作助手)",
"assistant.copyName": "「{name}」副本",
"assistant.broken": "本机无法启动",
"assistant.brokenRows": "{count} 行在本机不可用",
"assistant.brokenHint": "此模式在本机无法启动,不会出现在新会话菜单里。点「按本线修复」只关闭那些行。",
"assistant.switchHint": "切换助手保留各自的未保存草稿;列表中的「未保存」标记即为此。",
"btn.create": "新增助手",
"create.heading": "新建助手",
"btn.creating": "创建中…",
"btn.duplicate": "复制",
"btn.moveUp": "上移",
"btn.moveDown": "下移",
"btn.import": "导入",
"btn.export": "导出",
"btn.reset": "恢复出厂",
"btn.resetHint": "把编辑器换回此助手新建时保存的提示词。只改草稿,保存后才写入。",
"btn.delete": "删除",
"btn.cancel": "取消",
"msg.created": "已创建。刷新后,新会话的模式菜单里会出现它。",
"msg.duplicated": "已复制。两份配置彼此独立。",
"msg.deleted": "已删除。",
"msg.createFailed": "创建失败",
"msg.deleteFailed": "删除失败",
"msg.nameRequired": "请先给新助手起个名字。",
"msg.readOnlyHint": "仅管理本插件创建的助手;手写 preset 不计入,也不会被改写。",
"msg.reordered": "顺序已保存。新会话的模式菜单按此排列。",
"msg.reorderFailed": "调整顺序失败",
"msg.imported": "已导入到编辑器。尚未保存。",
"msg.importFailed": "导入失败",
"api.badDirection": "未知的排序方向:{direction}",
"api.unknownAssistant": "找不到助手「{id}」。此页只管理本插件创建的助手。",
"api.alreadyFirst": "「{name}」已经在最前面。",
"api.alreadyLast": "「{name}」已经在最后面。",
"api.badVariableName": "变量 {variable} 不合法。名字用小写字母、数字和下划线,且以字母开头。",
"api.unknownVariable": "{variable} 不能使用。可用:{known}。",
"mode.unavailable": "本机读不到出厂组成,基础模式暂不能切换。",
"rows.unavailable": "本机读不到出厂组成,插件开关暂不能改。磁盘上的现有设置仍然有效。",
"mode.pendingRows": "基础模式已改为「{mode}」。保存后,工具底换成这套官方预设。",
"aria.expand": "展开详情",
"aria.collapse": "收起详情",
"detail.id": "行 id",
"detail.hostReason": "平台原始信息:",
"detail.shipped": "出厂状态",
"detail.shippedOn": "启用",
"detail.shippedOff": "关闭",
"detail.note": "说明",
"detail.state": "开关状态",
"detail.explicitOn": "已手动启用",
"detail.explicitOff": "已手动停用",
"detail.untouched": "未改动(跟随官方默认)",
"detail.platform": "平台条件",
"btn.repair": "按本线修复",
"msg.repaired": "已修复",
"meta.version": "插件版本",
"meta.versionHint": "未钉版本时,可能装到较旧的发布。要换版本,按 README 钉版本重装,然后重启。",
"api.saved": "已保存({name},{mode})。下一步组装时会重读已保存的提示词。",
"api.conflict": "磁盘上的提示词已经变了,这次没有写入。先重新读取,再决定要不要覆盖。",
"api.savedWithExclusiveRows": "已保存({name},{mode})。已关闭不能同时启用的行:{rows}。",
"api.created": "已创建「{name}」。刷新后,新会话的模式菜单里会出现它。",
"api.savedPrompt": "已保存系统提示词({name})。下一步组装时会重读这个文件。",
"api.duplicated": "已从「{from}」复制。两份互不影响。",
"api.deleted": "已删除「{name}」。正在使用它的会话不受影响。",
"api.reordered": "顺序已保存。新会话的模式菜单按此排列。",
"api.nameRequired": "请先给新助手起个名字。",
"api.postureUnknown": "没有这个起步:{posture}。",
"api.posturePromptMissing": "读不到「{posture}」的起步提示词。",
"api.nameTooLong": "名字太长了(上限 {max} 个字符)。",
"api.descriptionTooLong": "描述太长了(上限 {max} 个字符)。",
"api.promptEmpty": "不能保存空的系统提示词。",
"api.badMode": "未知的基础模式:{mode}",
"api.dirExists": "目录已存在,请换一个名字:{path}",
"api.compositionMissing": "找不到组成文件:{path}",
"api.writeFailed": "写入失败:{detail}",
"api.promptWriteFailed": "写入提示词失败:{detail}",
"api.renderFailed": "生成组成文件失败:{detail}",
"api.selfCheckFailed": "生成结果自检失败,已放弃写入:{detail}",
"api.internalError": "宿主内部错误:{detail}",
"api.bodyTooLarge": "请求体过大(上限 {max} 字节)",
"api.repairNotNeeded": "没有需要修复的行。",
"api.repaired": "已关闭 {count} 个本机无法启动的行({ids})。",
"api.badAssistantId": "助手标识不合法:{id}(只能用 a-z、0-9 与连字符)",
"api.seedFailed": "复制模式模板失败:{detail}",
"api.metaWriteFailed": "写入 preset.yml 失败:{detail}",
"api.promptReadFailed": "读取提示词失败:{detail}",
"api.noRemoveApi": "当前版本无法删除助手。",
"api.noDirectory": "删除失败:磁盘上已没有这个目录。刷新后再试。",
"api.deleteFailed": "删除失败:{detail}",
"api.versionMissing": "找不到这个版本(历史可能已被上限裁剪)。",
"api.badJson": "请求体不是合法 JSON",
"api.savedPromptOnly": "已保存系统提示词。基础模式和插件开关未改。",
"api.baseCompositionUnavailable": "读不到基础模式「{mode}」的出厂组成。开关未改。系统提示词可以单独保存。",
"warn.approvalGateMissing": "当前版本没有审批事件。会话内改写提示词不会请求确认。",
"warn.unresolvableRows": "有的行在本机无法启动,此模式不会出现在新会话菜单里。点「按本线修复」只关闭那些行。",
"warn.presetBroken": "此模式无法启动,不会出现在新会话菜单里。点「按本线修复」。原因见下一行。",
"warn.badPromptVariable": "提示词里有不可用的变量。改掉后再保存,否则此模式的请求会失败。",
"warn.duplicateRowIds": "组成文件里有重复的行 id。开关只作用于第一处。",
"warn.predicateFailed": "有一行的平台条件无法计算,已按关闭处理。",
"warn.exclusiveRowsActive": "两套终端不能同时打开,否则此模式无法启动。保存时会关掉其中一套。",
"warn.baseCompositionUnavailable": "读不到出厂组成。基础模式和插件开关暂不能改。系统提示词仍可保存。",
"warn.personaOffWithPrompt": "「身份」已关闭,这段提示词不会生效。",
"warn.toolOff": "custom_prompt 已关闭。会话里不能改提示词,只能在此页改。",
"warn.noDescription": "没有描述时,模式菜单显示「暂无描述」。",
"warn.noName": "没有名字时,模式菜单显示目录 id。",
"history.label": "改动历史",
"history.pick": "选择版本…",
"history.load": "载入",
"history.hint": "载入只改草稿。保存后才写入。",
"history.by.settings": "此页保存",
"history.by.external": "外部改动",
"msg.loadFailed": "载入这一版失败",
"msg.importEmpty": "这个文件是空的。",
"msg.importTooLarge": "文件太大(上限 1MB)。",
"msg.exported": "已导出为文件。",
"msg.exportFailed": "导出失败",
"delete.title": "删除「{name}」?",
"delete.description": "将删除此助手的目录和系统提示词,不能撤销。正在使用它的会话不受影响。",
"delete.acknowledge": "我知道提示词会被删除",
"delete.confirm": "删除",
"delete.close": "关闭",
"delete.plainConfirm": "删除「{name}」?不能撤销。",
"name.heading": "名称",
"name.hint": "显示在模式菜单里。内部标识不变。",
"name.placeholder": "自定义模式",
"name.descriptionPlaceholder": "描述,显示在模式菜单里,可留空",
"mode.heading": "基础模式",
"mode.hint": "五档都可以点。改的是当前这位助手。提示词仍以本页为准。",
"rows.heading": "插件开关",
"rows.filterPlaceholder": "按名称或 id 筛选",
"cap.heading": "插件开关",
"cap.expand": "显示插件开关",
"cap.collapse": "隐藏插件开关",
"cap.hint": "关闭的行,这个助手不会使用。",
"rows.hint": "关闭的行,这个助手不会使用。",
"prompt.heading": "系统提示词",
"prompt.hint": "保存后,下一步组装模型输入时重读这个文件。",
"status.enabled": "已启用",
"status.disabled": "已停用",
"status.changed": "已改",
"tag.essential": "核心",
"tag.followPlatform": "随平台",
"btn.save": "保存",
"btn.saving": "处理中…",
"btn.reload": "重新读取",
"btn.reloadDiscard": "放弃修改并重新读取",
"btn.reloadConfirm": "放弃未保存的修改并重新读取?",
"msg.unsaved": "有未保存的修改",
"msg.readFailed": "读取失败",
"msg.saveFailed": "保存失败",
"msg.saved": "已保存。下一步组装时会重读这个文件。",
"status.detail": "{message}:{detail}",
"row.persona.label": "身份(系统提示词)",
"row.persona.note": "关闭后,此提示词不会生效。",
"row.custom-prompt-tool.label": "custom_prompt 工具",
"row.custom-prompt-tool.note": "关掉后无法用对话改提示词(设置页仍可用)",
"row.agent-instructions.label": "项目指令 AGENTS.md",
"row.agent-instructions.note": "读取 AGENTS.md / CLAUDE.md",
"row.tool-bash.label": "Shell(bash)",
"row.tool-pwsh.label": "Shell(pwsh)",
"row.tool-fs.label": "文件读写",
"row.tool-fs.note": "关掉后 agent 无法读写文件",
"row.tool-fs-search.label": "文件搜索(glob/grep)",
"row.tool-jobs.label": "后台任务",
"row.planning.label": "计划模式(分组)",
"row.planning.note": "关闭后,计划能力一并去掉。",
"row.plan-mode.label": "计划模式实现",
"row.compaction.label": "上下文压缩(分组)",
"row.compaction.note": "含 isolate realm",
"row.compaction-basic.label": "基础压缩",
"row.command-compact.label": "/compact 命令",
"row.tool-result-pruner.label": "工具结果裁剪",
"row.delegation.label": "委派与工作流(分组)",
"row.delegation.note": "关闭后,子代理和工作流一并去掉。",
"row.tool-subagent.label": "子代理(spawn)",
"row.tool-subagent-fork.label": "子代理(fork)",
"row.tool-subagent-control.label": "子代理控制",
"row.tool-subagent-list-agents.label": "列出子代理",
"row.tool-subagent-codex.label": "Codex 子代理",
"row.tool-subagent-codex.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
"row.tool-subagent-claude-code.label": "Claude Code 子代理",
"row.tool-subagent-claude-code.note": "需要先安装对应 Bundle 才能用;出厂状态见「详情」",
"row.time-context.label": "时间上下文(模型看得到时间)",
"row.time-context.note": "在部分步骤附上当前时间。",
"row.tool-schedule.label": "定时提醒工具",
"row.tool-schedule.note": "让 agent 创建/列出/修改/删除持久提醒;存储与投递由宿主的 schedule 服务负责",
"row.workflow-ptc.label": "工作流引擎",
"row.workflow-worker-thread.label": "工作流 Worker 线程",
"row.workflow-worker-thread.note": "把工作流跑在独立的 worker 线程里",
"row.tool-workflow.label": "工作流工具",
"row.tool-ralph.label": "Ralph 工作流",
"row.tool-ralph.note": "Ralph 工作流工具;出厂状态见「详情」",
"row.tool-web.label": "网页检索与抓取",
"row.tool-skill.label": "技能工具",
"row.skill-filesystem.label": "技能发现",
"row.tool-cordis.label": "Cordis 运行时工具",
"row.tool-cordis.note": "可读写 harness 运行时",
"row.tool-plugin-manager.label": "插件管理(安装 / 启停)",
"row.tool-plugin-manager.note": "供模型安装或开关插件。",
"row.tool-presentation.label": "PTC 工具呈现",
"row.present.label": "交付文件(present)",
"row.command-goal.label": "目标命令",
"row.tool-goal.label": "目标",
"row.tool-todo.label": "待办清单",
"row.tool-ask-user.label": "向用户提问",
"row.persistent-shell.label": "持久 Shell",
"row.pty.label": "PTY 终端",
"row.terminal-bash.label": "终端(bash)",
"row.persistent-bash.label": "持久 bash",
"row.terminal-pwsh.label": "终端(pwsh)",
"row.persistent-pwsh.label": "持久 pwsh",
"base.standard.label": "标准模式",
"base.standard.note": "出厂的完整工具集。",
"base.ptc.label": "PTC 模式",
"base.ptc.note": "标准模式,另开 PTC 工具呈现。",
"base.minimal.label": "极简模式",
"base.minimal.note": "只有 Shell 与终端。提示词仍以本页为准。",
"base.cordis.label": "创造模式",
"base.cordis.note": "标准模式,另开 Cordis 运行时工具。",
"base.all.label": "全部出厂行",
"base.all.note": "四套官方预设的行合在一处,均可开关。"
}
const EN = {
"nav": "Custom mode",
"assistant.heading": "Assistants",
"assistant.hint": "Each assistant keeps its own system prompt, base mode and plugin switches.",
"posture.hint": "This branch is written only when you click New assistant.",
"tree.posture": "Starter",
"tree.base": "Tool base",
"tree.baseHold": "A new assistant always starts on the official Standard mode.",
"tree.baseSkip": "Cannot be chosen on this branch. Change it later under This assistant.",
"tree.baseLocked": "Only the official Standard mode is lit. The other four are dim and cannot be chosen when creating.",
"posture.develop.label": "Coding",
"posture.develop.note": "Official Standard mode, tools as shipped.",
"posture.write.label": "Writing",
"posture.write.note": "Official Standard mode. Terminal off, file access on.",
"posture.chat.label": "Chat",
"posture.chat.note": "Official Standard mode. Files and terminal off.",
"editor.heading": "This assistant",
"editor.hint": "This branch edits the assistant selected above. All five base modes can be chosen.",
"assistant.empty": "No assistants yet — create one with the field below.",
"assistant.loadingList": "Loading assistants…",
"assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
"assistant.copyName": "{name} (copy)",
"assistant.broken": "Will not start here",
"assistant.brokenRows": "{count} row(s) unusable here",
"assistant.brokenHint": "This mode cannot start here, so it will not appear for a new session. Fix for this line turns off only those rows.",
"assistant.switchHint": "Switching assistants keeps each draft; the \"Unsaved\" marker in the list is exactly that.",
"btn.create": "New assistant",
"create.heading": "New assistant",
"btn.creating": "Creating…",
"btn.duplicate": "Duplicate",
"btn.moveUp": "Move up",
"btn.moveDown": "Move down",
"btn.import": "Import",
"btn.export": "Export",
"btn.reset": "Factory prompt",
"btn.resetHint": "Restores the prompt saved when this assistant was created. The editor changes. Save writes it.",
"btn.delete": "Delete",
"btn.cancel": "Cancel",
"msg.created": "Created. Refresh once, then it appears in the mode menu for a new session.",
"msg.duplicated": "Duplicated. The two are independent from here on.",
"msg.deleted": "Deleted.",
"msg.createFailed": "Could not create",
"msg.deleteFailed": "Could not delete",
"msg.nameRequired": "Give the new assistant a name first.",
"msg.readOnlyHint": "Manages only the assistants this plugin created; a hand-written preset is not listed and is never rewritten.",
"msg.reordered": "Order saved. New sessions list modes in this order.",
"msg.reorderFailed": "Could not reorder",
"msg.imported": "Imported into the editor. Not saved yet.",
"msg.importFailed": "Import failed",
"api.badDirection": "Unknown reorder direction: {direction}",
"api.unknownAssistant": "No assistant \"{id}\". This page only manages assistants it created.",
"api.alreadyFirst": "{name} is already first.",
"api.alreadyLast": "{name} is already last.",
"api.badVariableName": "{variable} is not a valid variable. Use a lower-case name.",
"api.unknownVariable": "{variable} cannot be used. Available: {known}.",
"mode.unavailable": "No shipped composition is available, so the base mode cannot be changed.",
"rows.unavailable": "No shipped composition is available, so the plugin switches cannot be edited right now; the row states already on disk still apply.",
"mode.pendingRows": "Base mode is now {mode}. On save, the tool base becomes this shipped preset.",
"aria.expand": "Show details",
"aria.collapse": "Hide details",
"detail.id": "Row id",
"detail.hostReason": "Host message: ",
"detail.shipped": "Shipped",
"detail.shippedOn": "enabled",
"detail.shippedOff": "disabled",
"detail.note": "Note",
"detail.state": "Switch",
"detail.explicitOn": "Set to on by you",
"detail.explicitOff": "Set to off by you",
"detail.untouched": "Untouched (follows the shipped default)",
"detail.platform": "Platform condition",
"btn.repair": "Fix for this line",
"msg.repaired": "Repaired",
"meta.version": "Plugin version",
"meta.versionHint": "Without a pinned version, an older release may be installed. To change it, reinstall with the version pin from the README, then restart.",
"api.saved": "Saved ({name}, {mode}). The next model step rereads the saved prompt.",
"api.conflict": "The prompt on disk has changed, so this save was not written. Reload, then decide whether to overwrite.",
"api.savedWithExclusiveRows": "Saved ({name}, {mode}). Turned off rows that cannot be on together: {rows}.",
"api.created": "Created {name}. Refresh once, then it appears in the mode menu for a new session.",
"api.savedPrompt": "System prompt saved ({name}). The next model step rereads this file.",
"api.duplicated": "Copied from {from}. The two are independent from now on.",
"api.deleted": "Deleted {name}. Sessions already using it are unchanged.",
"api.reordered": "Order saved. New sessions list modes in this order.",
"api.nameRequired": "Give the new assistant a name first.",
"api.postureUnknown": "No such starter: {posture}.",
"api.posturePromptMissing": "Could not read the starter prompt for {posture}.",
"api.nameTooLong": "That name is too long (limit {max} characters).",
"api.descriptionTooLong": "That description is too long (limit {max} characters).",
"api.promptEmpty": "An empty system prompt cannot be saved.",
"api.badMode": "Unknown base mode: {mode}",
"api.dirExists": "That directory already exists — pick another name: {path}",
"api.compositionMissing": "Composition file not found: {path}",
"api.writeFailed": "Write failed: {detail}",
"api.promptWriteFailed": "Writing the prompt failed: {detail}",
"api.renderFailed": "Rendering the composition failed: {detail}",
"api.selfCheckFailed": "The generated composition failed its self-check, so nothing was written: {detail}",
"api.internalError": "Unexpected host error: {detail}",
"api.bodyTooLarge": "Request body too large (limit {max} bytes)",
"api.repairNotNeeded": "Nothing to repair.",
"api.repaired": "Turned off {count} row(s) that cannot start here ({ids}).",
"api.badAssistantId": "Invalid assistant id: {id} (a-z, 0-9 and hyphens only)",
"api.seedFailed": "Copying the mode template failed: {detail}",
"api.metaWriteFailed": "Writing preset.yml failed: {detail}",
"api.promptReadFailed": "Reading the prompt failed: {detail}",
"api.noRemoveApi": "This version cannot delete an assistant.",
"api.noDirectory": "Delete failed: this assistant's directory is gone from disk, so the list on screen is stale. Refresh and try again.",
"api.deleteFailed": "Delete failed: {detail}",
"api.versionMissing": "That version is gone (the history is capped).",
"api.badJson": "The request body is not valid JSON",
"api.savedPromptOnly": "System prompt saved. Base mode and plugin switches were not changed.",
"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).",
"warn.personaOffWithPrompt": "Identity is off, so this prompt has no effect.",
"warn.toolOff": "custom_prompt is off. The prompt can be edited on this page only.",
"warn.noDescription": "Without a description, the mode menu shows \"no description yet\".",
"warn.approvalGateMissing": "This version has no approval event. In-session prompt edits are not confirmed.",
"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.",
"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.",
"warn.badPromptVariable": "The prompt uses a variable that is not available. Fix it before saving.",
"warn.duplicateRowIds": "A row id is repeated. A switch applies only to the first one.",
"warn.predicateFailed": "A platform condition could not be evaluated. That row is treated as off.",
"warn.exclusiveRowsActive": "The two terminals cannot both be on, or this mode will not start. Saving turns one of them off.",
"warn.noName": "Without a name, the mode menu shows the directory id.",
"warn.baseCompositionUnavailable": "No shipped composition is available. Base mode and plugin switches cannot be changed. The system prompt can still be saved.",
"history.label": "Change history",
"history.pick": "Choose a version…",
"history.load": "Load",
"history.hint": "Loading edits the draft only. Save writes it.",
"history.by.settings": "saved here",
"history.by.external": "external edit",
"msg.loadFailed": "Could not load that version",
"msg.importEmpty": "That file is empty.",
"msg.importTooLarge": "That file is too large (1 MB limit).",
"msg.exported": "Exported to a file.",
"msg.exportFailed": "Export failed",
"delete.title": "Delete \"{name}\"?",
"delete.description": "This deletes the assistant directory and its system prompt. It cannot be undone. Sessions already using it are unchanged.",
"delete.acknowledge": "I understand this assistant's prompt will be deleted permanently",
"delete.confirm": "Delete",
"delete.close": "Close",
"delete.plainConfirm": "Delete \"{name}\"? This cannot be undone.",
"name.heading": "Name",
"name.hint": "Shown in the mode menu. The internal id stays the same.",
"name.placeholder": "Custom mode",
"name.descriptionPlaceholder": "Description, shown in the mode menu. Optional.",
"mode.heading": "Base mode",
"mode.hint": "All five can be chosen. This edits the assistant selected above. The prompt on this page still applies.",
"rows.heading": "Plugin switches",
"rows.filterPlaceholder": "Filter by name or id",
"cap.heading": "Plugin switches",
"cap.expand": "Show plugin switches",
"cap.collapse": "Hide plugin switches",
"cap.hint": "A row that is off is not used by this assistant.",
"rows.hint": "A row that is off is not used by this assistant.",
"prompt.heading": "System prompt",
"prompt.hint": "After you save, the next model step rereads this file.",
"status.enabled": "Enabled",
"status.disabled": "Disabled",
"status.changed": "changed",
"tag.essential": "Core",
"tag.followPlatform": "Platform",
"btn.save": "Save",
"btn.saving": "Working…",
"btn.reload": "Reload",
"btn.reloadDiscard": "Discard edits and reload",
"btn.reloadConfirm": "Discard unsaved edits and reload?",
"msg.unsaved": "Unsaved changes",
"msg.readFailed": "Load failed",
"msg.saveFailed": "Save failed",
"msg.saved": "Saved. The next model step rereads this file.",
"status.detail": "{message}: {detail}",
"row.persona.label": "Identity (system prompt)",
"row.persona.note": "Off: this prompt has no effect.",
"row.custom-prompt-tool.label": "custom_prompt tool",
"row.custom-prompt-tool.note": "Without it the prompt can still be edited here, but not by asking the agent",
"row.agent-instructions.label": "Project instructions (AGENTS.md)",
"row.agent-instructions.note": "Reads AGENTS.md / CLAUDE.md",
"row.tool-bash.label": "Shell (bash)",
"row.tool-pwsh.label": "Shell (pwsh)",
"row.tool-fs.label": "File access",
"row.tool-fs.note": "Without it the agent cannot read or write files",
"row.tool-fs-search.label": "File search (glob/grep)",
"row.tool-jobs.label": "Background jobs",
"row.planning.label": "Plan mode (group)",
"row.planning.note": "Off: planning is removed with it.",
"row.plan-mode.label": "Plan mode implementation",
"row.compaction.label": "Context compaction (group)",
"row.compaction.note": "Carries an isolate realm",
"row.compaction-basic.label": "Basic compaction",
"row.command-compact.label": "/compact command",
"row.tool-result-pruner.label": "Tool-result pruning",
"row.delegation.label": "Delegation and workflows (group)",
"row.delegation.note": "Off: subagents and workflows are removed with it.",
"row.tool-subagent.label": "Subagent (spawn)",
"row.tool-subagent-fork.label": "Subagent (fork)",
"row.tool-subagent-control.label": "Subagent control",
"row.tool-subagent-list-agents.label": "List subagents",
"row.tool-subagent-codex.label": "Codex subagent",
"row.tool-subagent-codex.note": "Needs its Bundle installed first; the shipped state is in the details",
"row.tool-subagent-claude-code.label": "Claude Code subagent",
"row.tool-subagent-claude-code.note": "Needs its Bundle installed first; the shipped state is in the details",
"row.time-context.label": "Time context (the model can see the clock)",
"row.time-context.note": "Adds the current time on some steps.",
"row.tool-schedule.label": "Scheduled reminders tool",
"row.tool-schedule.note": "Lets the agent create, list, edit and delete durable reminders; the host schedule service owns storage and delivery",
"row.workflow-ptc.label": "Workflow engine",
"row.workflow-worker-thread.label": "Workflow worker thread",
"row.workflow-worker-thread.note": "Runs workflows on a separate worker thread",
"row.tool-workflow.label": "Workflow tool",
"row.tool-ralph.label": "Ralph workflow",
"row.tool-ralph.note": "The Ralph workflow tool; the shipped state is in the details",
"row.tool-web.label": "Web search and fetch",
"row.tool-skill.label": "Skill tool",
"row.skill-filesystem.label": "Skill discovery",
"row.tool-cordis.label": "Cordis runtime tools",
"row.tool-cordis.note": "Can read and modify the running harness",
"row.tool-plugin-manager.label": "Plugin manager (install / toggle)",
"row.tool-plugin-manager.note": "Lets the model install or toggle plugins.",
"row.tool-presentation.label": "PTC tool presentation",
"row.present.label": "Deliverables (present)",
"row.command-goal.label": "Goal command",
"row.tool-goal.label": "Goals",
"row.tool-todo.label": "Todo list",
"row.tool-ask-user.label": "Ask the user",
"row.persistent-shell.label": "Persistent shell",
"row.pty.label": "PTY terminal",
"row.terminal-bash.label": "Terminal (bash)",
"row.persistent-bash.label": "Persistent bash",
"row.terminal-pwsh.label": "Terminal (pwsh)",
"row.persistent-pwsh.label": "Persistent pwsh",
"base.standard.label": "Standard mode",
"base.standard.note": "The shipped tool set.",
"base.ptc.label": "PTC mode",
"base.ptc.note": "Standard, plus PTC tool presentation.",
"base.minimal.label": "Minimal mode",
"base.minimal.note": "Shell and terminal only. The prompt on this page still applies.",
"base.cordis.label": "Creator mode",
"base.cordis.note": "Standard, plus Cordis runtime tools.",
"base.all.label": "All shipped rows",
"base.all.note": "Rows from all four shipped presets. Each row can be switched."
}
const TRANSLATIONS = { zh: ZH, en: EN }
/**
* The language the shell is showing right now.
*
* Read from the service SNAPSHOT at call time rather than captured once, so a language
* switch — which re-renders every outlet — picks up the new value on the next render.
*/
/** `error` as a readable string, without assuming it is an Error. */
function describe(error) {
return String((error && error.message) || error)
}
function activeLanguage(locale) {
let id = ""
try {
const snapshot = locale !== undefined && typeof locale.getLocale === "function" ? locale.getLocale() : undefined
if (snapshot !== undefined && typeof snapshot.active === "string") id = snapshot.active
} catch (error) {
/* a shell without a readable snapshot falls through */
}
if (id === "" && typeof navigator !== "undefined" && typeof navigator.language === "string") id = navigator.language
const norm = id.toLowerCase()
if (norm.startsWith("zh")) return "zh"
if (norm.startsWith("en")) return "en"
const primary = norm.split("-")[0]
if (Object.prototype.hasOwnProperty.call(TRANSLATIONS, norm)) return norm
if (primary !== "" && Object.prototype.hasOwnProperty.call(TRANSLATIONS, primary)) return primary
// 壳目前内置 zh / en,语言包可以再注册别的 id。没有对应词典时用英文,避免整页中文或裸键。
return "en"
}
/**
* Translate one key, with this bundle's own dictionaries as a FLOOR.
*
* The shell's bound `t` is preferred — it is what makes a language switch re-render the
* page and what keeps this text in the same table as every other plugin's. But the page
* must never depend on that registration having succeeded: `locale.register()` refuses a
* namespace it already holds (an HMR swap applies this bundle twice), and a registration
* can be gone by the time the already-mounted page renders — both observed as a page full
* of raw keys like `assistant.heading`. The dictionaries are inlined in this file anyway
* (they are what test/locales.test.mjs diffs against locales.mjs), so they are used when
* the shell cannot answer. A raw key is therefore impossible whenever it is in the table.
*/
function translate(key, fallback, shellT) {
if (typeof shellT === "function") {
const value = shellT(key)
if (typeof value === "string" && value !== "" && value !== key) return value
}
const dict = activeLanguage(localeRef) === "en" ? EN : ZH
if (typeof dict[key] === "string" && dict[key] !== "") return dict[key]
if (typeof ZH[key] === "string" && ZH[key] !== "") return ZH[key]
return fallback === undefined ? key : fallback
}
/**
* The locale service, kept for {@link translate}'s language lookup and the nav label.
*
* A module-scope slot rather than a parameter because the nav label is a thunk the shell
* calls outside any component, and it must read the CURRENT language each time.
*/
let localeRef
/**
* Plain-element stand-ins for the shell's atoms.
*
* Only reached on a shell that does not seed {@link ATOMS_MODULE}. Their prop contracts
* match the real ones exactly (`Switch.onChange` hands over the next boolean, not an
* event), so no component below needs to know which set it got.
*/
function fallbackAtoms() {
const join = (...parts) => parts.filter((part) => typeof part === "string" && part !== "").join(" ")
const Button = (props) => {
const { variant, size, icon, children, className, ...rest } = props
return react.createElement(
"button",
{
...rest,
type: rest.type === undefined ? "button" : rest.type,
className: join(
"cpfe-btn",
variant === "primary" ? "cpfe-btn-primary" : variant === "ghost" ? "cpfe-btn-ghost" : "cpfe-btn-outline",
size === "sm" ? "cpfe-btn-sm" : "",
className,
),
},
icon === undefined || icon === null ? null : icon,
children,
)
}
const Input = (props) => {
const { icon, className, ...rest } = props
return react.createElement("input", { ...rest, className: join("cpfe-input", className) })
}
const Switch = (props) => {
const { checked, onChange, label, disabled, title, className } = props
// Like the real atom: `label` is the accessible name ONLY, never visible text —
// the caller draws the row title, so both paths look the same.
return react.createElement(
"label",
{ className: join("cpfe-switch", className), title },
react.createElement("input", {
type: "checkbox",
checked,
disabled,
"aria-label": label,
onChange: (event) => onChange(event.target.checked),
}),
)
}
const Tag = (props) =>
react.createElement(
"span",
{ className: join("cpfe-tag", props.tone === undefined ? "" : "cpfe-tag-" + props.tone, props.className) },
props.children,
)
const Pill = (props) => {
const { active, className, children, ...rest } = props
return react.createElement(
"button",
{
...rest,
type: "button",
"aria-pressed": active === true,
className: join("cpfe-pill", active === true ? "cpfe-pill-on" : "", className),
},
children,
)
}
return { Button, Input, Switch, Tag, Pill }
}
/**
* Whether an atom can be handed to `createElement`.
*
* ★ **This is where the page's native look was lost.** The probe used to be
* `typeof atoms.Button === "function"`, but the shell's components are `forwardRef(...)` /
* `memo(...)` **objects** (`Button = forwardRef(function Button(){…})`, measured), so the check
* failed and *every* control silently fell back to the plain stand-ins — hand-painted buttons,
* `input[type=checkbox]` instead of the shell's switch, a native `window.confirm` instead of its
* risk dialog. Measured on 0.1.7-rc.2 (2026-09-25): `require(ATOMS_MODULE)` returns **279 exports**
* including `Button`, `RiskConfirmation`, `Modal`, `Menu`, `SegmentedControl` and the icon set —
* the module was in the platform seed table the whole time.
* React renders any object carrying `$$typeof`, so that is what "usable" means here.
*/
const renderable = (atom) =>
typeof atom === "function" || (atom !== null && typeof atom === "object" && atom.$$typeof !== undefined)
/**
* The page asks for icons under short names (`IconPlusOutline16`); the shell exports
* `IconPlusOutlineRegular`. Without this map the calls were `undefined` and **no icon ever
* rendered** — which is part of why the header row looked hand-made.
*/
const ICON_ALIAS = {
IconChevronDownOutline14: "IconChevronDownOutlineRegular",
IconChevronUpOutline14: "IconChevronUpOutlineRegular",
IconChevronLeftOutline14: "IconChevronLeftOutlineRegular",
IconChevronRightOutline14: "IconChevronRightOutlineRegular",
IconPlusOutline16: "IconPlusOutlineRegular",
IconCheckOutline14: "IconCheckOutlineRegular",
IconCopyOutline16: "IconCopyOutlineRegular",
IconDownloadOutline16: "IconDownloadOutlineRegular",
IconRefreshOutline16: "IconRefreshOutlineRegular",
IconTrashOutline16: "IconTrashOutlineRegular",
IconWarningOutline14: "IconWarningOutlineRegular",
}
function withIconAliases(atoms) {
const out = Object.assign({}, atoms)
for (const short of Object.keys(ICON_ALIAS)) {
if (out[short] === undefined && renderable(atoms[ICON_ALIAS[short]])) out[short] = atoms[ICON_ALIAS[short]]
}
return out
}
/** Resolve the atom set once, at factory time. */
function loadAtoms() {
try {
const atoms = require(ATOMS_MODULE)
if (atoms !== null && typeof atoms === "object" && renderable(atoms.Button)) return withIconAliases(atoms)
console.info(
"dsh-custom-mode: 壳提供了 " +
ATOMS_MODULE +
",但里面没有可用的 Button,改用内置的朴素控件(功能一致,外观更简)。",
)
} catch (error) {
console.info(
"dsh-custom-mode: 当前壳没有在种子表里提供 " +
ATOMS_MODULE +
",改用内置的朴素控件(功能一致,外观更简)。",
)
}
return fallbackAtoms()
}
const A = loadAtoms()
/**
* Stylesheet: LAYOUT ONLY.
*
* Controls are the shell's, so nothing here restyles a button, input or switch while the
* atoms are available — that is what keeps this page consistent with every other one. The
* `cpfe-btn*` / `cpfe-input` / `cpfe-switch*` / `cpfe-tag*` / `cpfe-pill*` rules exist for
* the fallback path above, and `--dsw-*` tokens are used for the few own surfaces (cards,
* grids, the status line).
*/
const CSS = [
".cpfe{--g:8px;display:flex;flex-direction:column;gap:16px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px;font:inherit;color:inherit}",
".cpfe > section{padding-top:16px;border-top:1px solid var(--dsw-alias-border-l1)}",
".cpfe > section:first-child{padding-top:0;border-top:0}",
// 分节标题:度量抄官方设置面板里的同类文本。
// 实测(**0.1.7-rc.2 与 0.2.0-rc.2 各量一次,结论相同**):官方 `_title`(设置行标题)
// 与 `groupTitle`(插件页分组标题)都是 14px/22px **w400**;官方唯一的 14px w500 是插件
// 卡片标题(`cardTitle`,行高 20px,形态不同),页级标题则是 16px/24px w500。
// 本页是设置面板里的分节,取 w400。
// ⚠️ 本仓长期记作「官方区块标题 14px/22px w500」—— 那一条在 2026-10-02 的复量中被证伪。
".cpfe-h{margin:0 0 2px;font-size:14px;line-height:22px;font-weight:400;color:var(--dsw-alias-label-primary)}",
".cpfe-sub{margin:0 0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
// 区块引言:官方是标题下的一行 12px 灰字 —— 没有折叠按钮,也没有那个蓝色圆点。
".cpfe-hint{display:flex;flex-direction:column;gap:2px;margin:0 0 6px}",
".cpfe-hint-line{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
".cpfe-hint-detail{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
// The native InlineEditor saves on Enter and autofocuses; document editing must keep
// Enter as a newline and manual Save. Use its exact surface/font tokens for these
// controlled textareas, with only document height and width as layout differences.
".cpfe-desc,.cpfe-editor{box-sizing:border-box;min-width:0;margin:0;padding:3px 8px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);outline:none;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);font-family:Inter,var(--dsw-font-family);overflow-y:auto}",
".cpfe-desc:focus,.cpfe-editor:focus{border-color:var(--dsw-alias-state-business-primary)}",
".cpfe-desc{min-height:40px;max-height:120px;resize:vertical}",
".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
".cpfe-note{display:block;margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
".cpfe-mono{display:block;margin:0;font:inherit;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
".cpfe-pills{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
".cpfe-pills > *{flex:0 0 auto;white-space:nowrap}",
".cpfe-pills button{white-space:nowrap}",
".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
".cpfe-history{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}",
".cpfe-history-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
".cpfe-history-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
".cpfe-pill-wrap{display:inline-flex;align-items:center;gap:4px}",
".cpfe-actions{display:flex;flex-wrap:wrap;gap:var(--g);align-items:center;margin-top:4px}",
".cpfe-newrow{display:flex;gap:var(--g);align-items:center;flex-wrap:wrap;margin-top:10px}",
".cpfe-postures{display:flex;flex-direction:column;gap:6px}",
".cpfe-create{display:flex;flex-direction:column;gap:6px}",
".cpfe-branch{display:flex;flex-direction:column;gap:16px;margin-top:8px;padding-left:12px;border-left:1px solid var(--dsw-alias-border-l1)}",
".cpfe-node{display:flex;flex-direction:column;gap:4px}",
".cpfe-node-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
".cpfe-field{display:flex;width:100%;margin-bottom:8px}",
".cpfe-row-head{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
".cpfe-row-switch{flex:0 0 auto}",
".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
".cpfe-row-note{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
".cpfe-row-toggle{flex:0 0 auto}",
".cpfe-row-open{border-color:var(--dsw-alias-border-l2)}",
".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)}",
".cpfe-detail-line{display:flex;gap:8px;min-width:0}",
".cpfe-detail-key{flex:0 0 9.5em;color:var(--dsw-alias-label-secondary)}",
".cpfe-detail-value{min-width:0;overflow-wrap:anywhere}",
".cpfe-sec-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px}",
".cpfe-sec-head .cpfe-actions{margin-top:0}",
".cpfe-newinput{flex:1;min-width:200px}",
// 单列、行高统一(对齐官方插件页的形态);之前的自适应多列网格会让行高参差不齐。
// ★ 对齐官方设置行(实测 0.1.7-rc.2 的行:padding 16px 0、无背景、无圆角,行间只有一条 1px
// 分隔线)。原来是"圆角卡片 + 描边 + 灰底" —— 那正是廉价感最重的部分。
".cpfe-rows{display:flex;flex-direction:column;gap:0;margin-top:0}",
".cpfe-line{display:flex;flex-direction:column;min-width:0;border-bottom:1px solid var(--dsw-alias-border-l1)}",
".cpfe-line:last-child{border-bottom:0}",
".cpfe-group{display:flex;flex-direction:column;min-width:0}",
".cpfe-kids{display:flex;flex-direction:column;margin-left:0;padding-left:16px}",
".cpfe-row-meta{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}",
".cpfe-row-badges{display:flex;align-items:center;gap:4px;flex:0 1 auto;min-width:0}",
".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:74px;padding:16px 0;border:0;border-radius:0;background:none}",
".cpfe-editor{width:100%;min-height:120px;resize:vertical}",
".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap;margin-top:8px}",
".cpfe-disclosure{align-self:flex-start;margin-top:8px}",
".cpfe .cpfe-danger{margin-left:auto}",
".cpfe-status{font-size:13px;line-height:20px}",
".cpfe-ok{color:var(--dsw-alias-label-secondary)}",
".cpfe-err{color:var(--dsw-alias-state-error-primary)}",
".cpfe-dirty{color:var(--dsw-alias-state-warn-primary)}",
".cpfe-meta-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0}",
".cpfe-version{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
".cpfe-path{font:inherit;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
// ── fallback-path controls (unused when the shell provides the atoms) ──
".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}",
".cpfe-btn:disabled{opacity:.4;cursor:not-allowed}",
".cpfe-btn-sm{height:28px;padding:0 10px;font-size:12px;line-height:18px;border-radius:var(--dsw-radius-sm)}",
".cpfe-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);border-color:transparent;font-weight:500}",
".cpfe-btn-ghost{border-color:transparent;background:transparent}",
".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}",
".cpfe-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
// 忙碌期间禁掉草稿类控件的交互(issue #6)。
// 状态层已经拦住了改动(`update()` 在 busy 时直接返回),但对**受控组件**来说,
// 光拦状态还不够:DOM 会先显示用户敲进去的字/翻过去的开关,等下一次重渲染才被拉回来 ——
// 那就是"我明明改了,它自己弹回去了"。`pointer-events` 让点击根本到不了控件,
// 视觉上也给一个明确的"此刻不能动"。
".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-pill,.cpfe-busy .cpfe-selector{pointer-events:none}",
".cpfe-busy .cpfe-editor{cursor:progress}",
".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)}",
".cpfe-tag-success,.cpfe-tag-info{color:var(--dsw-alias-state-success-primary)}",
".cpfe-tag-warning{color:var(--dsw-alias-state-warn-primary)}",
".cpfe-tag-danger{color:var(--dsw-alias-state-error-primary)}",
".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}",
".cpfe-pill-on{border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-label-primary)}",
// Menu surfaces and trigger Buttons keep the native atoms' own styles.
// ── 告警区 ──────────────────────────────────────────────────────────────
// 这一段此前**一条 CSS 都没有**(2026-10-02 逐表核对:样式表里 `cpfe-warn` 零规则),
// 于是出现告警时它是一串没有排版的裸 `<p>`:没有行距、图标与文字不对齐、
// 与页面其余部分(12px/18px)的字号也对不上。度量沿用页面其余部分。
".cpfe-warns{display:flex;flex-direction:column;gap:12px}",
".cpfe-warn-row{display:flex;flex-direction:column;gap:6px;align-items:flex-start}",
".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)}",
".cpfe-warn svg{flex:0 0 auto;margin-top:2px}",
].join("")
/** Apply the page stylesheet once, keyed so a re-mount never duplicates it. */
function ensureStyles() {
if (typeof document === "undefined") return
const tagId = "dsh-custom-mode/system-prompt"
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return
const tag = document.createElement("style")
tag.dataset.plugin = "dsh-custom-mode"
tag.dataset.pluginCss = tagId
tag.textContent = CSS
document.head.appendChild(tag)
}
const asJson = (response) => response.json()
/** The endpoints, all under the one prefix route the host half registers. */
const ROUTES = {
list: ROUTE,
state: ROUTE + "/state",
history: ROUTE + "/history",
create: ROUTE + "/create",
delete: ROUTE + "/delete",
reorder: ROUTE + "/reorder",
repair: ROUTE + "/repair",
}
/** Every assistant this feature manages. */
async function fetchList() {
return asJson(await fetch(ROUTES.list, { method: "GET", headers: { accept: "application/json" } }))
}
/** One assistant: base mode, row tree, switches, prompt, display metadata. */
async function fetchState(id) {
return asJson(
await fetch(ROUTES.state + "?id=" + encodeURIComponent(id), {
method: "GET",
headers: { accept: "application/json" },
}),
)
}
/** POST a JSON body to one endpoint and read the JSON verdict. */
async function postJson(url, payload) {
return asJson(
await fetch(url, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify(payload),
}),
)
}
/** A safe download name: no separators or characters a filesystem rejects. */
function fileNameFor(name) {
const cleaned = String(name === undefined || name === null ? "" : name)
.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "-")
.replace(/^-+|-+$/g, "")
.trim()
return cleaned === "" ? "prompt" : cleaned.slice(0, 60)
}
/** The editable draft for one assistant, derived from its loaded state. */
/** 时间戳给人看:转本地时间;解析不了就原样显示(历史文件是纯文本,什么都有可能)。 */
function formatWhen(at) {
const parsed = new Date(at)
if (Number.isNaN(parsed.getTime())) return String(at).slice(0, 16)
return parsed.toLocaleString()
}
function draftOf(state) {
return {
id: state.id,
mode: state.mode,
// 出厂组成取不到时的降级标记(服务端给,只读)。它不是用户可改的字段,sameDraft 不比较它。
baseUnavailable:
state.baseUnavailable === undefined || state.baseUnavailable === null ? null : state.baseUnavailable,
overrides: { ...state.overrides },
prompt: state.prompt,
name: typeof state.name === "string" ? state.name : "",
description: typeof state.description === "string" ? state.description : "",
// 「恢复出厂提示词」要用它。它不是草稿的一部分,`sameDraft` 不比较它 ——
// 漏掉这一行按钮会一直置灰(实测:真点了没反应,浏览器验收抓到)。
factoryPrompt: typeof state.factoryPrompt === "string" ? state.factoryPrompt : null,
// 改动历史:列表来自 state(只有元数据),正文点「载入这一版」时按需取。
history: Array.isArray(state.history) ? state.history : [],
historyPick: "",
broken: typeof state.broken === "string" && state.broken !== "" ? state.broken : null,
// 「配置了却不生效」的告警码;文案按当前语言渲染。
warnings: Array.isArray(state.warnings) ? state.warnings : [],
}
}
/** Whether a draft still differs from the state it was loaded from. */
function sameDraft(left, right) {
if (left === undefined || right === undefined || left === null || right === null) return left === right
return (
left.mode === right.mode &&
left.prompt === right.prompt &&
left.name === right.name &&
left.description === right.description &&
JSON.stringify(left.overrides) === JSON.stringify(right.overrides)
)
}
/** `「名称」副本`-style copy name, from a locale template. */
function copyName(template, name) {
return String(template).replace("{name}", name)
}
/**
* A section's one-line introduction, sitting under its heading.
*
* It takes ONE string and renders it as one line — the shape the official settings pages use:
* a single 12px/18px tertiary line under the title, with no disclosure control and no bullet.
*
* It used to take `hint` + `detail` and pick `detail` when present, for a collapsible
* introduction that no longer exists. Every caller passed both, so the short form was
* **never rendered** — five dictionary entries (`assistant.short`, `name.short`, `mode.short`,
* `rows.short`, `prompt.short`) that looked alive in a grep but reached no pixel. Both are gone.
*/
function SectionHint(props) {
return react.createElement(
"div",
{ className: "cpfe-hint" },
react.createElement("span", { className: "cpfe-hint-line" }, props.text),
)
}
/**
* One row of the plugin switch list.
*
* Laid out like the shell's own plugin page: **one compact line per row** — title, status tags, a
* one-line truncated description and the switch on the right — with everything else (the row id, the
* full note, the tri-state detail, the platform condition) behind a per-row disclosure. Measured before
* this change: 33 rows at 274px wide and 84–117px tall with a bare `tool-bash` line each, i.e. uneven
* heights and a lot of vertical noise for information most users never read.
*/
function filterRows(rows, query, translateRow) {
const needle = String(query || "").trim().toLowerCase()
if (needle === "") return rows
const out = []
for (const row of rows) {
const title = String(translateRow("row." + row.id + ".label", row.label)).toLowerCase()
const self = title.includes(needle) || row.id.toLowerCase().includes(needle)
const children = Array.isArray(row.children) ? row.children : []
const kids = self ? children : filterRows(children, query, translateRow)