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 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
+