dsh-custom-mode 0.1.6-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +652 -0
- package/composition.mjs +558 -0
- package/cordis.patch.yml +17 -0
- package/index.mjs +377 -0
- package/locales.mjs +219 -0
- package/meta.mjs +94 -0
- package/package.json +56 -0
- package/paths.mjs +38 -0
package/client.js
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half: 「自定义模式」settings page.
|
|
3
|
+
*
|
|
4
|
+
* Three blocks:
|
|
5
|
+
* 1. 基础模式 — standard / ptc / minimal / cordis.
|
|
6
|
+
* 2. 插件开关 — one switch per composition row; groups show their children.
|
|
7
|
+
* 3. 系统提示词 — the editable prompt text.
|
|
8
|
+
*
|
|
9
|
+
* Hand-written in the client bundle format the module system expects
|
|
10
|
+
* (`window.__ModuleLoader__.load({ id, factory })`). `require("react")` resolves
|
|
11
|
+
* from the shell's static seed table, so no bundler is involved. It talks to its
|
|
12
|
+
* host half over one private HTTP route with `fetch`, so it needs no Remote
|
|
13
|
+
* namespace.
|
|
14
|
+
*
|
|
15
|
+
* Failure policy: everything is wrapped so a broken editor can never take the
|
|
16
|
+
* page down. Worst case is that this settings page does not appear.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
window.__ModuleLoader__.load({
|
|
21
|
+
id: "dsh-custom-mode",
|
|
22
|
+
factory: (require) => {
|
|
23
|
+
var module = { exports: {} }
|
|
24
|
+
var exports = module.exports
|
|
25
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
|
|
26
|
+
|
|
27
|
+
const ROUTE = "/custom-mode"
|
|
28
|
+
|
|
29
|
+
const NS = "settings.customMode"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Copy for this page, generated from editor/locales.mjs (single source of
|
|
33
|
+
* truth, checked for key parity by test/locales.test.mjs).
|
|
34
|
+
*/
|
|
35
|
+
const ZH = {
|
|
36
|
+
"nav": "自定义模式",
|
|
37
|
+
"name.heading": "模式名称",
|
|
38
|
+
"name.hint": "改名只影响显示(模式选择器和这里的导航项),内部标识保持不变,已有会话不受影响。新建会话即可看到新名称。",
|
|
39
|
+
"name.placeholder": "自定义模式",
|
|
40
|
+
"name.descriptionPlaceholder": "模式描述(显示在模式选择器里,可留空)",
|
|
41
|
+
"mode.heading": "基础模式",
|
|
42
|
+
"mode.hint": "选一个官方模式作为底子,下面再按行微调。改完保存后,新建会话即生效,不需要重启。",
|
|
43
|
+
"rows.heading": "插件开关",
|
|
44
|
+
"rows.hint": "逐行控制这个模式挂载哪些插件,和官方插件列表一样按行铺开。没拨过的行保持官方默认(含平台判断);你手动拨了就以你的为准。",
|
|
45
|
+
"prompt.heading": "系统提示词",
|
|
46
|
+
"prompt.hint": "这一步编辑的文本会在每次模型调用前重新读取,保存后下一步即生效。仅影响使用本模式的会话。",
|
|
47
|
+
"status.enabled": "已启用",
|
|
48
|
+
"status.disabled": "已停用",
|
|
49
|
+
"status.changed": "已改",
|
|
50
|
+
"tag.essential": "基础能力",
|
|
51
|
+
"tag.followPlatform": "跟随平台",
|
|
52
|
+
"btn.save": "保存",
|
|
53
|
+
"btn.saving": "处理中…",
|
|
54
|
+
"btn.reload": "重新读取",
|
|
55
|
+
"msg.unsaved": "有未保存的修改",
|
|
56
|
+
"msg.loading": "正在读取…",
|
|
57
|
+
"msg.notLoaded": "(尚未读取)",
|
|
58
|
+
"msg.reread": "已重新读取",
|
|
59
|
+
"msg.readFailed": "读取失败",
|
|
60
|
+
"msg.saveFailed": "保存失败",
|
|
61
|
+
"msg.saved": "已保存。新建会话即生效,当前会话保持原配置。",
|
|
62
|
+
"row.persona.label": "身份(系统提示词)",
|
|
63
|
+
"row.persona.note": "提示词注入点;关掉后本模式用回部署默认身份",
|
|
64
|
+
"row.custom-prompt-tool.label": "custom_prompt 工具",
|
|
65
|
+
"row.custom-prompt-tool.note": "关掉后无法用对话改提示词(设置页仍可用)",
|
|
66
|
+
"row.agent-instructions.label": "项目指令 AGENTS.md",
|
|
67
|
+
"row.agent-instructions.note": "读取 AGENTS.md / CLAUDE.md",
|
|
68
|
+
"row.tool-bash.label": "Shell(bash)",
|
|
69
|
+
"row.tool-pwsh.label": "Shell(pwsh)",
|
|
70
|
+
"row.tool-fs.label": "文件读写",
|
|
71
|
+
"row.tool-fs.note": "关掉后 agent 无法读写文件",
|
|
72
|
+
"row.tool-fs-search.label": "文件搜索(glob/grep)",
|
|
73
|
+
"row.tool-jobs.label": "后台任务",
|
|
74
|
+
"row.planning.label": "计划模式(分组)",
|
|
75
|
+
"row.planning.note": "含 isolate realm,关掉等于移除整个计划能力",
|
|
76
|
+
"row.plan-mode.label": "计划模式实现",
|
|
77
|
+
"row.compaction.label": "上下文压缩(分组)",
|
|
78
|
+
"row.compaction.note": "含 isolate realm",
|
|
79
|
+
"row.compaction-basic.label": "基础压缩",
|
|
80
|
+
"row.command-compact.label": "/compact 命令",
|
|
81
|
+
"row.tool-result-pruner.label": "工具结果裁剪",
|
|
82
|
+
"row.delegation.label": "委派与工作流(分组)",
|
|
83
|
+
"row.delegation.note": "含 isolate realm;关掉等于移除子代理与工作流",
|
|
84
|
+
"row.tool-subagent.label": "子代理(spawn)",
|
|
85
|
+
"row.tool-subagent-fork.label": "子代理(fork)",
|
|
86
|
+
"row.tool-subagent-control.label": "子代理控制",
|
|
87
|
+
"row.tool-subagent-list-agents.label": "列出子代理",
|
|
88
|
+
"row.tool-subagent-codex.label": "Codex 子代理",
|
|
89
|
+
"row.tool-subagent-codex.note": "默认关闭:需要先安装对应 Bundle",
|
|
90
|
+
"row.tool-subagent-claude-code.label": "Claude Code 子代理",
|
|
91
|
+
"row.tool-subagent-claude-code.note": "默认关闭:需要先安装对应 Bundle",
|
|
92
|
+
"row.workflow-ptc.label": "工作流引擎",
|
|
93
|
+
"row.tool-workflow.label": "工作流工具",
|
|
94
|
+
"row.tool-ralph.label": "Ralph 工作流",
|
|
95
|
+
"row.tool-ralph.note": "默认关闭",
|
|
96
|
+
"row.tool-web.label": "网页检索与抓取",
|
|
97
|
+
"row.tool-skill.label": "技能工具",
|
|
98
|
+
"row.skill-filesystem.label": "技能发现",
|
|
99
|
+
"row.tool-cordis.label": "Cordis 运行时工具",
|
|
100
|
+
"row.tool-cordis.note": "可读写 harness 运行时",
|
|
101
|
+
"row.tool-presentation.label": "PTC 工具呈现",
|
|
102
|
+
"row.present.label": "交付文件(present)",
|
|
103
|
+
"row.command-goal.label": "目标命令",
|
|
104
|
+
"row.tool-goal.label": "目标",
|
|
105
|
+
"row.tool-todo.label": "待办清单",
|
|
106
|
+
"row.tool-ask-user.label": "向用户提问",
|
|
107
|
+
"row.persistent-shell.label": "持久 Shell",
|
|
108
|
+
"row.pty.label": "PTY 终端",
|
|
109
|
+
"row.terminal-bash.label": "终端(bash)",
|
|
110
|
+
"row.persistent-bash.label": "持久 bash",
|
|
111
|
+
"row.terminal-pwsh.label": "终端(pwsh)",
|
|
112
|
+
"row.persistent-pwsh.label": "持久 pwsh",
|
|
113
|
+
"base.standard.label": "标准模式",
|
|
114
|
+
"base.standard.note": "完整编码能力:Shell、文件、检索、技能、计划、目标、子代理、工作流",
|
|
115
|
+
"base.ptc.label": "PTC 模式",
|
|
116
|
+
"base.ptc.note": "在标准模式基础上启用 PTC 工具呈现(tool-presentation)",
|
|
117
|
+
"base.minimal.label": "极简模式",
|
|
118
|
+
"base.minimal.note": "只有 Shell 与终端,共 7 行;没有文件、检索、技能、子代理",
|
|
119
|
+
"base.cordis.label": "Cordis 模式",
|
|
120
|
+
"base.cordis.note": "标准模式 + 读写运行时的 Cordis 工具集,可让 agent 自己改 harness"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const EN = {
|
|
124
|
+
"nav": "Custom mode",
|
|
125
|
+
"name.heading": "Mode name",
|
|
126
|
+
"name.hint": "Renaming changes only what is displayed (the mode picker and this nav entry); the internal id stays the same and existing sessions are unaffected. Start a new session to see it.",
|
|
127
|
+
"name.placeholder": "Custom mode",
|
|
128
|
+
"name.descriptionPlaceholder": "Mode description (shown in the mode picker, optional)",
|
|
129
|
+
"mode.heading": "Base mode",
|
|
130
|
+
"mode.hint": "Choose an official mode as the base, then fine-tune individual rows below. After saving, a new session picks it up — no restart needed.",
|
|
131
|
+
"rows.heading": "Plugin switches",
|
|
132
|
+
"rows.hint": "Control row by row which plugins this mode mounts, laid out like the official plugin list. Untouched rows keep the official default (platform conditions included); your manual choice wins.",
|
|
133
|
+
"prompt.heading": "System prompt",
|
|
134
|
+
"prompt.hint": "This text is re-read before every model call, so a save applies on the next step. It affects only sessions using this mode.",
|
|
135
|
+
"status.enabled": "Enabled",
|
|
136
|
+
"status.disabled": "Disabled",
|
|
137
|
+
"status.changed": "changed",
|
|
138
|
+
"tag.essential": "core",
|
|
139
|
+
"tag.followPlatform": "follows platform",
|
|
140
|
+
"btn.save": "Save",
|
|
141
|
+
"btn.saving": "Working…",
|
|
142
|
+
"btn.reload": "Reload",
|
|
143
|
+
"msg.unsaved": "Unsaved changes",
|
|
144
|
+
"msg.loading": "Loading…",
|
|
145
|
+
"msg.notLoaded": "(not loaded)",
|
|
146
|
+
"msg.reread": "Reloaded",
|
|
147
|
+
"msg.readFailed": "Load failed",
|
|
148
|
+
"msg.saveFailed": "Save failed",
|
|
149
|
+
"msg.saved": "Saved. A new session picks it up; the current one keeps its configuration.",
|
|
150
|
+
"row.persona.label": "Identity (system prompt)",
|
|
151
|
+
"row.persona.note": "Where the prompt is injected; turning it off falls back to the deployment identity",
|
|
152
|
+
"row.custom-prompt-tool.label": "custom_prompt tool",
|
|
153
|
+
"row.custom-prompt-tool.note": "Without it the prompt can still be edited here, but not by asking the agent",
|
|
154
|
+
"row.agent-instructions.label": "Project instructions (AGENTS.md)",
|
|
155
|
+
"row.agent-instructions.note": "Reads AGENTS.md / CLAUDE.md",
|
|
156
|
+
"row.tool-bash.label": "Shell (bash)",
|
|
157
|
+
"row.tool-pwsh.label": "Shell (pwsh)",
|
|
158
|
+
"row.tool-fs.label": "File access",
|
|
159
|
+
"row.tool-fs.note": "Without it the agent cannot read or write files",
|
|
160
|
+
"row.tool-fs-search.label": "File search (glob/grep)",
|
|
161
|
+
"row.tool-jobs.label": "Background jobs",
|
|
162
|
+
"row.planning.label": "Plan mode (group)",
|
|
163
|
+
"row.planning.note": "Carries an isolate realm; turning it off removes planning entirely",
|
|
164
|
+
"row.plan-mode.label": "Plan mode implementation",
|
|
165
|
+
"row.compaction.label": "Context compaction (group)",
|
|
166
|
+
"row.compaction.note": "Carries an isolate realm",
|
|
167
|
+
"row.compaction-basic.label": "Basic compaction",
|
|
168
|
+
"row.command-compact.label": "/compact command",
|
|
169
|
+
"row.tool-result-pruner.label": "Tool-result pruning",
|
|
170
|
+
"row.delegation.label": "Delegation and workflows (group)",
|
|
171
|
+
"row.delegation.note": "Carries an isolate realm; turning it off removes subagents and workflows",
|
|
172
|
+
"row.tool-subagent.label": "Subagent (spawn)",
|
|
173
|
+
"row.tool-subagent-fork.label": "Subagent (fork)",
|
|
174
|
+
"row.tool-subagent-control.label": "Subagent control",
|
|
175
|
+
"row.tool-subagent-list-agents.label": "List subagents",
|
|
176
|
+
"row.tool-subagent-codex.label": "Codex subagent",
|
|
177
|
+
"row.tool-subagent-codex.note": "Off by default: install the matching Bundle first",
|
|
178
|
+
"row.tool-subagent-claude-code.label": "Claude Code subagent",
|
|
179
|
+
"row.tool-subagent-claude-code.note": "Off by default: install the matching Bundle first",
|
|
180
|
+
"row.workflow-ptc.label": "Workflow engine",
|
|
181
|
+
"row.tool-workflow.label": "Workflow tool",
|
|
182
|
+
"row.tool-ralph.label": "Ralph workflow",
|
|
183
|
+
"row.tool-ralph.note": "Off by default",
|
|
184
|
+
"row.tool-web.label": "Web search and fetch",
|
|
185
|
+
"row.tool-skill.label": "Skill tool",
|
|
186
|
+
"row.skill-filesystem.label": "Skill discovery",
|
|
187
|
+
"row.tool-cordis.label": "Cordis runtime tools",
|
|
188
|
+
"row.tool-cordis.note": "Can read and modify the running harness",
|
|
189
|
+
"row.tool-presentation.label": "PTC tool presentation",
|
|
190
|
+
"row.present.label": "Deliverables (present)",
|
|
191
|
+
"row.command-goal.label": "Goal command",
|
|
192
|
+
"row.tool-goal.label": "Goals",
|
|
193
|
+
"row.tool-todo.label": "Todo list",
|
|
194
|
+
"row.tool-ask-user.label": "Ask the user",
|
|
195
|
+
"row.persistent-shell.label": "Persistent shell",
|
|
196
|
+
"row.pty.label": "PTY terminal",
|
|
197
|
+
"row.terminal-bash.label": "Terminal (bash)",
|
|
198
|
+
"row.persistent-bash.label": "Persistent bash",
|
|
199
|
+
"row.terminal-pwsh.label": "Terminal (pwsh)",
|
|
200
|
+
"row.persistent-pwsh.label": "Persistent pwsh",
|
|
201
|
+
"base.standard.label": "Standard",
|
|
202
|
+
"base.standard.note": "Full coding agent: shell, files, search, skills, planning, goals, subagents, workflows",
|
|
203
|
+
"base.ptc.label": "PTC",
|
|
204
|
+
"base.ptc.note": "Standard plus PTC tool presentation (tool-presentation)",
|
|
205
|
+
"base.minimal.label": "Minimal",
|
|
206
|
+
"base.minimal.note": "Shell and terminal only, 7 rows; no files, search, skills or subagents",
|
|
207
|
+
"base.cordis.label": "Cordis",
|
|
208
|
+
"base.cordis.note": "Standard plus the Cordis toolset, letting the agent modify its own harness"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const TRANSLATIONS = { zh: ZH, en: EN }
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Display name for the settings nav entry.
|
|
215
|
+
*
|
|
216
|
+
* The slot label is a thunk, which the shell re-reads on every projection, so
|
|
217
|
+
* renaming the mode updates the nav without re-registering the slot.
|
|
218
|
+
*/
|
|
219
|
+
let currentLabel = ""
|
|
220
|
+
|
|
221
|
+
const CSS = [
|
|
222
|
+
".cpfe{--g:8px;display:flex;flex-direction:column;gap:24px;width:100%;max-width:900px;box-sizing:border-box;padding-bottom:16px}",
|
|
223
|
+
".cpfe-h{margin:0 0 4px;font-size:15px;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
224
|
+
".cpfe-sub{margin:0 0 12px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
|
|
225
|
+
".cpfe-modes{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--g)}",
|
|
226
|
+
".cpfe-mode{display:flex;gap:10px;align-items:flex-start;box-sizing:border-box;min-height:64px;padding:12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;transition:border-color .12s}",
|
|
227
|
+
".cpfe-mode:hover{border-color:var(--dsw-alias-label-secondary)}",
|
|
228
|
+
".cpfe-mode-on{border-color:var(--dsw-alias-label-primary)}",
|
|
229
|
+
".cpfe-mode input{margin:3px 0 0;cursor:pointer;flex:none}",
|
|
230
|
+
".cpfe-mode-b{display:flex;flex-direction:column;gap:2px;min-width:0}",
|
|
231
|
+
".cpfe-mode-t{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
|
|
232
|
+
".cpfe-mode-n{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}",
|
|
233
|
+
".cpfe-rows{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--g);align-items:stretch}",
|
|
234
|
+
".cpfe-group{grid-column:1/-1;display:flex;flex-direction:column;gap:var(--g)}",
|
|
235
|
+
".cpfe-kids{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--g);padding-left:16px;border-left:.5px solid var(--dsw-alias-border-l1)}",
|
|
236
|
+
".cpfe-row{display:flex;gap:10px;align-items:flex-start;box-sizing:border-box;min-height:64px;padding:10px 12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);transition:border-color .12s}",
|
|
237
|
+
".cpfe-row:hover{border-color:var(--dsw-alias-label-secondary)}",
|
|
238
|
+
".cpfe-row input{margin:3px 0 0;cursor:pointer;flex:none}",
|
|
239
|
+
".cpfe-row-b{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}",
|
|
240
|
+
".cpfe-row-t{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary)}",
|
|
241
|
+
".cpfe-row-d{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
242
|
+
".cpfe-note{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
|
|
243
|
+
".cpfe-badge{font-size:11px;line-height:17px;padding:0 6px;border-radius:4px;white-space:nowrap}",
|
|
244
|
+
".cpfe-on{color:var(--dsw-alias-state-success-primary)}",
|
|
245
|
+
".cpfe-off{color:var(--dsw-alias-label-secondary)}",
|
|
246
|
+
".cpfe-tag{font-size:11px;line-height:16px;padding:0 5px;border-radius:4px;border:.5px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}",
|
|
247
|
+
".cpfe-warn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}",
|
|
248
|
+
".cpfe-ess{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}",
|
|
249
|
+
".cpfe-input{box-sizing:border-box;width:100%;height:34px;margin-bottom:8px;padding:0 12px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
|
|
250
|
+
".cpfe-editor{box-sizing:border-box;width:100%;min-height:240px;resize:vertical;padding:12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:20px}",
|
|
251
|
+
".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap}",
|
|
252
|
+
".cpfe-btn{appearance:none;cursor:pointer;padding:0 14px;height:32px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
|
|
253
|
+
".cpfe-btn:disabled{opacity:.5;cursor:default}",
|
|
254
|
+
".cpfe-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);border-color:transparent;font-weight:500}",
|
|
255
|
+
".cpfe-status{font-size:13px;line-height:20px}",
|
|
256
|
+
".cpfe-ok{color:var(--dsw-alias-label-secondary)}",
|
|
257
|
+
".cpfe-err{color:var(--dsw-alias-state-error-primary)}",
|
|
258
|
+
".cpfe-dirty{color:var(--dsw-alias-state-warn-primary)}",
|
|
259
|
+
".cpfe-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
260
|
+
].join("")
|
|
261
|
+
|
|
262
|
+
/** Apply the page stylesheet once, keyed so a re-mount never duplicates it. */
|
|
263
|
+
function ensureStyles() {
|
|
264
|
+
if (typeof document === "undefined") return
|
|
265
|
+
const tagId = "dsh-custom-mode/system-prompt"
|
|
266
|
+
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return
|
|
267
|
+
const tag = document.createElement("style")
|
|
268
|
+
tag.dataset.plugin = "dsh-custom-mode"
|
|
269
|
+
tag.dataset.pluginCss = tagId
|
|
270
|
+
tag.textContent = CSS
|
|
271
|
+
document.head.appendChild(tag)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const asJson = (response) => response.json()
|
|
275
|
+
|
|
276
|
+
/** Everything the page needs, in one round trip. */
|
|
277
|
+
async function fetchState() {
|
|
278
|
+
return asJson(await fetch(ROUTE, { method: "GET", headers: { accept: "application/json" } }))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Persist mode + per-row overrides + prompt text. */
|
|
282
|
+
async function saveState(payload) {
|
|
283
|
+
return asJson(
|
|
284
|
+
await fetch(ROUTE, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
287
|
+
body: JSON.stringify(payload),
|
|
288
|
+
}),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Render one row's switches, recursing into a group's children. */
|
|
293
|
+
function RowList(props) {
|
|
294
|
+
const react = require("react")
|
|
295
|
+
const { rows, overrides, onToggle, depth, t } = props
|
|
296
|
+
return react.createElement(
|
|
297
|
+
"div",
|
|
298
|
+
{ className: depth === 0 ? "cpfe-rows" : "cpfe-kids" },
|
|
299
|
+
rows.map((row) => {
|
|
300
|
+
// A row is "on" unless its explicit override says off; without an
|
|
301
|
+
// override the shipped state decides (which is what "跟随默认" means).
|
|
302
|
+
const effective = overrides[row.id] !== undefined ? overrides[row.id] : !row.disabled
|
|
303
|
+
const changed = overrides[row.id] !== undefined
|
|
304
|
+
return react.createElement(
|
|
305
|
+
"div",
|
|
306
|
+
{ key: row.id, className: row.children.length > 0 ? "cpfe-group" : undefined },
|
|
307
|
+
react.createElement(
|
|
308
|
+
"label",
|
|
309
|
+
{ className: "cpfe-row" },
|
|
310
|
+
react.createElement("input", {
|
|
311
|
+
type: "checkbox",
|
|
312
|
+
checked: effective,
|
|
313
|
+
onChange: () => onToggle(row.id, !effective),
|
|
314
|
+
}),
|
|
315
|
+
react.createElement(
|
|
316
|
+
"span",
|
|
317
|
+
{ className: "cpfe-row-b" },
|
|
318
|
+
react.createElement(
|
|
319
|
+
"span",
|
|
320
|
+
{ className: "cpfe-row-t" },
|
|
321
|
+
t("row." + row.id + ".label", row.label),
|
|
322
|
+
react.createElement(
|
|
323
|
+
"span",
|
|
324
|
+
{ className: effective ? "cpfe-badge cpfe-on" : "cpfe-badge cpfe-off" },
|
|
325
|
+
effective ? t("status.enabled") : t("status.disabled"),
|
|
326
|
+
),
|
|
327
|
+
row.essential ? react.createElement("span", { className: "cpfe-tag cpfe-ess" }, t("tag.essential")) : null,
|
|
328
|
+
row.disabledExpression !== null
|
|
329
|
+
? react.createElement("span", { className: "cpfe-tag" }, t("tag.followPlatform"))
|
|
330
|
+
: null,
|
|
331
|
+
changed ? react.createElement("span", { className: "cpfe-tag" }, t("status.changed")) : null,
|
|
332
|
+
),
|
|
333
|
+
react.createElement("span", { className: "cpfe-row-d" }, row.id),
|
|
334
|
+
row.note !== null ? react.createElement("span", { className: "cpfe-note" }, t("row." + row.id + ".note", row.note)) : null,
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
row.children.length > 0
|
|
338
|
+
? react.createElement(RowList, {
|
|
339
|
+
rows: row.children,
|
|
340
|
+
overrides: overrides,
|
|
341
|
+
onToggle: onToggle,
|
|
342
|
+
depth: depth + 1,
|
|
343
|
+
t: t,
|
|
344
|
+
})
|
|
345
|
+
: null,
|
|
346
|
+
)
|
|
347
|
+
}),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Whether a preset name is just the shipped default rather than a real
|
|
353
|
+
* customisation.
|
|
354
|
+
*
|
|
355
|
+
* The nav entry has two jobs: follow the language when the user never named
|
|
356
|
+
* the mode, and show the user's own name verbatim once they have. Treating
|
|
357
|
+
* EITHER language's default as "default" is what lets a language switch
|
|
358
|
+
* relabel the entry, while any other value is displayed untouched.
|
|
359
|
+
*/
|
|
360
|
+
function isDefaultName(name) {
|
|
361
|
+
if (typeof name !== "string" || name === "") return true
|
|
362
|
+
return name === ZH.nav || name === EN.nav
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function CustomModeSection(props) {
|
|
366
|
+
const react = require("react")
|
|
367
|
+
// Compatibility: a shell that does not hand us a bound `t` (older or
|
|
368
|
+
// changed settings contract) must still render real copy. Falling back to
|
|
369
|
+
// the Chinese dictionary means the worst case is "Chinese text", never a
|
|
370
|
+
// page full of raw keys like `name.heading`.
|
|
371
|
+
const rawT =
|
|
372
|
+
props !== null && typeof props === "object" && typeof props.t === "function"
|
|
373
|
+
? props.t
|
|
374
|
+
: (key) => (Object.prototype.hasOwnProperty.call(ZH, key) ? ZH[key] : key)
|
|
375
|
+
/**
|
|
376
|
+
* Translate a key, falling back to the shipped text when the dictionary
|
|
377
|
+
* lacks it — an untranslated key degrades to Chinese, never to a bare key.
|
|
378
|
+
*/
|
|
379
|
+
const t = (key, fallback) => {
|
|
380
|
+
const value = rawT(key)
|
|
381
|
+
if (typeof value === "string" && value !== "" && value !== key) return value
|
|
382
|
+
return fallback === undefined ? key : fallback
|
|
383
|
+
}
|
|
384
|
+
const [state, setState] = react.useState(null)
|
|
385
|
+
const [draft, setDraft] = react.useState(null)
|
|
386
|
+
const [status, setStatus] = react.useState("")
|
|
387
|
+
const [failed, setFailed] = react.useState(false)
|
|
388
|
+
const [busy, setBusy] = react.useState(false)
|
|
389
|
+
|
|
390
|
+
const load = react.useCallback(async (announce) => {
|
|
391
|
+
setBusy(true)
|
|
392
|
+
try {
|
|
393
|
+
const result = await fetchState()
|
|
394
|
+
if (result && result.ok === true) {
|
|
395
|
+
setState(result)
|
|
396
|
+
currentLabel = isDefaultName(result.name) ? "" : result.name
|
|
397
|
+
setDraft({
|
|
398
|
+
mode: result.mode,
|
|
399
|
+
overrides: { ...result.overrides },
|
|
400
|
+
prompt: result.prompt,
|
|
401
|
+
name: typeof result.name === "string" ? result.name : "",
|
|
402
|
+
description: typeof result.description === "string" ? result.description : "",
|
|
403
|
+
})
|
|
404
|
+
setFailed(false)
|
|
405
|
+
if (announce === true) setStatus(t("msg.reread"))
|
|
406
|
+
} else {
|
|
407
|
+
setFailed(true)
|
|
408
|
+
setStatus((result && result.error) || t("msg.readFailed"))
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
setFailed(true)
|
|
412
|
+
setStatus(t("msg.readFailed") + ":" + String((error && error.message) || error))
|
|
413
|
+
} finally {
|
|
414
|
+
setBusy(false)
|
|
415
|
+
}
|
|
416
|
+
}, [])
|
|
417
|
+
|
|
418
|
+
react.useEffect(() => {
|
|
419
|
+
load(false)
|
|
420
|
+
}, [load])
|
|
421
|
+
|
|
422
|
+
if (draft === null || state === null) {
|
|
423
|
+
return react.createElement("p", { className: "cpfe-sub" }, busy ? t("msg.loading") : status || t("msg.notLoaded"))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const dirty =
|
|
427
|
+
draft.mode !== state.mode ||
|
|
428
|
+
draft.prompt !== state.prompt ||
|
|
429
|
+
draft.name !== state.name ||
|
|
430
|
+
draft.description !== state.description ||
|
|
431
|
+
JSON.stringify(draft.overrides) !== JSON.stringify(state.overrides)
|
|
432
|
+
|
|
433
|
+
const pickMode = (mode) => {
|
|
434
|
+
// Switching base mode keeps explicit row overrides: rows that exist in
|
|
435
|
+
// the new mode keep their state, ids absent from it are ignored.
|
|
436
|
+
setDraft({ ...draft, mode })
|
|
437
|
+
setStatus("")
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const toggle = (id, next) => {
|
|
441
|
+
setDraft({ ...draft, overrides: { ...draft.overrides, [id]: next } })
|
|
442
|
+
setStatus("")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const save = async () => {
|
|
446
|
+
setBusy(true)
|
|
447
|
+
try {
|
|
448
|
+
const result = await saveState({
|
|
449
|
+
mode: draft.mode,
|
|
450
|
+
overrides: draft.overrides,
|
|
451
|
+
prompt: draft.prompt,
|
|
452
|
+
name: draft.name,
|
|
453
|
+
description: draft.description,
|
|
454
|
+
})
|
|
455
|
+
if (result && result.ok === true) {
|
|
456
|
+
setFailed(false)
|
|
457
|
+
setStatus(result.note || "已保存。")
|
|
458
|
+
// Re-read so the row list matches the composition we just wrote.
|
|
459
|
+
const fresh = await fetchState()
|
|
460
|
+
if (fresh && fresh.ok === true) {
|
|
461
|
+
setState(fresh)
|
|
462
|
+
currentLabel = isDefaultName(fresh.name) ? "" : fresh.name
|
|
463
|
+
setDraft({
|
|
464
|
+
mode: fresh.mode,
|
|
465
|
+
overrides: { ...fresh.overrides },
|
|
466
|
+
prompt: fresh.prompt,
|
|
467
|
+
name: typeof fresh.name === "string" ? fresh.name : "",
|
|
468
|
+
description: typeof fresh.description === "string" ? fresh.description : "",
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
setFailed(true)
|
|
473
|
+
setStatus((result && result.error) || t("msg.saveFailed"))
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
setFailed(true)
|
|
477
|
+
setStatus(t("msg.saveFailed") + ":" + String((error && error.message) || error))
|
|
478
|
+
} finally {
|
|
479
|
+
setBusy(false)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const statusClass = failed ? "cpfe-status cpfe-err" : dirty ? "cpfe-status cpfe-dirty" : "cpfe-status cpfe-ok"
|
|
484
|
+
const shown = dirty && status === "" ? t("msg.unsaved") : status
|
|
485
|
+
|
|
486
|
+
return react.createElement(
|
|
487
|
+
"div",
|
|
488
|
+
{ className: "cpfe" },
|
|
489
|
+
react.createElement(
|
|
490
|
+
"section",
|
|
491
|
+
null,
|
|
492
|
+
react.createElement("h2", { className: "cpfe-h" }, t("name.heading")),
|
|
493
|
+
react.createElement(
|
|
494
|
+
"p",
|
|
495
|
+
{ className: "cpfe-sub" },
|
|
496
|
+
t("name.hint"),
|
|
497
|
+
),
|
|
498
|
+
react.createElement("input", {
|
|
499
|
+
className: "cpfe-input",
|
|
500
|
+
type: "text",
|
|
501
|
+
value: draft.name,
|
|
502
|
+
placeholder: t("name.placeholder"),
|
|
503
|
+
onChange: (event) => {
|
|
504
|
+
setDraft({ ...draft, name: event.target.value })
|
|
505
|
+
setStatus("")
|
|
506
|
+
},
|
|
507
|
+
}),
|
|
508
|
+
react.createElement("input", {
|
|
509
|
+
className: "cpfe-input",
|
|
510
|
+
type: "text",
|
|
511
|
+
value: draft.description,
|
|
512
|
+
placeholder: t("name.descriptionPlaceholder"),
|
|
513
|
+
onChange: (event) => {
|
|
514
|
+
setDraft({ ...draft, description: event.target.value })
|
|
515
|
+
setStatus("")
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
),
|
|
519
|
+
react.createElement(
|
|
520
|
+
"section",
|
|
521
|
+
null,
|
|
522
|
+
react.createElement("h2", { className: "cpfe-h" }, t("mode.heading")),
|
|
523
|
+
react.createElement(
|
|
524
|
+
"p",
|
|
525
|
+
{ className: "cpfe-sub" },
|
|
526
|
+
t("mode.hint"),
|
|
527
|
+
),
|
|
528
|
+
react.createElement(
|
|
529
|
+
"div",
|
|
530
|
+
{ className: "cpfe-modes" },
|
|
531
|
+
state.modes.map((mode) =>
|
|
532
|
+
react.createElement(
|
|
533
|
+
"label",
|
|
534
|
+
{ key: mode.id, className: draft.mode === mode.id ? "cpfe-mode cpfe-mode-on" : "cpfe-mode" },
|
|
535
|
+
react.createElement("input", {
|
|
536
|
+
type: "radio",
|
|
537
|
+
name: "cpfe-mode",
|
|
538
|
+
checked: draft.mode === mode.id,
|
|
539
|
+
onChange: () => pickMode(mode.id),
|
|
540
|
+
}),
|
|
541
|
+
react.createElement(
|
|
542
|
+
"span",
|
|
543
|
+
{ className: "cpfe-mode-b" },
|
|
544
|
+
react.createElement("span", { className: "cpfe-mode-t" }, t("base." + mode.id + ".label", mode.label)),
|
|
545
|
+
react.createElement("span", { className: "cpfe-mode-n" }, t("base." + mode.id + ".note", mode.note)),
|
|
546
|
+
),
|
|
547
|
+
),
|
|
548
|
+
),
|
|
549
|
+
),
|
|
550
|
+
),
|
|
551
|
+
react.createElement(
|
|
552
|
+
"section",
|
|
553
|
+
null,
|
|
554
|
+
react.createElement("h2", { className: "cpfe-h" }, t("rows.heading")),
|
|
555
|
+
react.createElement(
|
|
556
|
+
"p",
|
|
557
|
+
{ className: "cpfe-sub" },
|
|
558
|
+
t("rows.hint"),
|
|
559
|
+
),
|
|
560
|
+
react.createElement(RowList, {
|
|
561
|
+
rows: state.rows,
|
|
562
|
+
overrides: draft.overrides,
|
|
563
|
+
onToggle: toggle,
|
|
564
|
+
depth: 0,
|
|
565
|
+
t: t,
|
|
566
|
+
}),
|
|
567
|
+
),
|
|
568
|
+
react.createElement(
|
|
569
|
+
"section",
|
|
570
|
+
null,
|
|
571
|
+
react.createElement("h2", { className: "cpfe-h" }, t("prompt.heading")),
|
|
572
|
+
react.createElement(
|
|
573
|
+
"p",
|
|
574
|
+
{ className: "cpfe-sub" },
|
|
575
|
+
t("prompt.hint"),
|
|
576
|
+
),
|
|
577
|
+
react.createElement("textarea", {
|
|
578
|
+
className: "cpfe-editor",
|
|
579
|
+
value: draft.prompt,
|
|
580
|
+
spellCheck: false,
|
|
581
|
+
onChange: (event) => {
|
|
582
|
+
setDraft({ ...draft, prompt: event.target.value })
|
|
583
|
+
setStatus("")
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
),
|
|
587
|
+
react.createElement(
|
|
588
|
+
"div",
|
|
589
|
+
{ className: "cpfe-bar" },
|
|
590
|
+
react.createElement(
|
|
591
|
+
"button",
|
|
592
|
+
{ className: "cpfe-btn cpfe-primary", disabled: busy || dirty === false, onClick: save },
|
|
593
|
+
busy ? t("btn.saving") : t("btn.save"),
|
|
594
|
+
),
|
|
595
|
+
react.createElement("button", { className: "cpfe-btn", disabled: busy, onClick: () => load(true) }, t("btn.reload")),
|
|
596
|
+
react.createElement("span", { className: statusClass }, shown),
|
|
597
|
+
react.createElement("span", { className: "cpfe-path" }, state.compositionPath),
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function apply(ctx) {
|
|
603
|
+
try {
|
|
604
|
+
ensureStyles()
|
|
605
|
+
const locale = ctx.get("locale")
|
|
606
|
+
// Bound once so the nav label thunk can translate at projection time.
|
|
607
|
+
const navT = locale === undefined ? null : locale.bind(NS)
|
|
608
|
+
if (locale !== undefined) {
|
|
609
|
+
ctx.effect(() => locale.register(NS, TRANSLATIONS), "custom-mode: dictionaries")
|
|
610
|
+
}
|
|
611
|
+
const slots = ctx.get("slots")
|
|
612
|
+
if (slots === undefined) {
|
|
613
|
+
console.warn("dsh-custom-mode: slots service unavailable; settings page not registered")
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
slots.inject("settings.section", () => {
|
|
617
|
+
// `locale: NS` is the shipped contract: the shell then hands the
|
|
618
|
+
// component a bound `t` in props.
|
|
619
|
+
return slots.register(
|
|
620
|
+
{
|
|
621
|
+
name: "settings.section",
|
|
622
|
+
id: "custom-system-prompt",
|
|
623
|
+
order: 21,
|
|
624
|
+
label: () => currentLabel || (navT === null ? ZH.nav : navT("nav")),
|
|
625
|
+
locale: NS,
|
|
626
|
+
},
|
|
627
|
+
CustomModeSection,
|
|
628
|
+
)
|
|
629
|
+
})
|
|
630
|
+
} catch (error) {
|
|
631
|
+
console.error("dsh-custom-mode: apply() failed", error)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Declared like every shipped settings-section plugin: the registry guards
|
|
636
|
+
// service reads, and the settings shell owns the target slot.
|
|
637
|
+
const inject = ["slots", "locale"]
|
|
638
|
+
|
|
639
|
+
exports.apply = apply
|
|
640
|
+
exports.inject = inject
|
|
641
|
+
return module.exports
|
|
642
|
+
},
|
|
643
|
+
})
|
|
644
|
+
} catch (error) {
|
|
645
|
+
// Never take the page down with us. The settings page simply will not appear.
|
|
646
|
+
try {
|
|
647
|
+
console.error("dsh-custom-mode: browser half failed to register", error)
|
|
648
|
+
} catch {
|
|
649
|
+
/* logging is best-effort */
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|