dsh-prompt-customizer 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DreamsTOF
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,114 @@
1
+ # dsh-prompt-customizer
2
+
3
+ > **English** | [简体中文](README.md)
4
+
5
+ A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) plugin that lets you control the **system prompt** and the **tool catalog** from a settings-panel UI.
6
+
7
+ Plugin-injected prompt sections (from other plugins) can pollute your system prompt. This plugin lets you **block**, **replace**, **inject**, and **reorder** prompt sections by name, and **hide tools** from the model catalog — all live, without touching the other plugins.
8
+
9
+ ## Features
10
+
11
+ - **Prompt sections**
12
+ - **Block** a section by name (removed from the assembled system prompt).
13
+ - **Replace** a section's text (the original text is echoed for editing, including dynamically generated sections).
14
+ - **Inject** brand-new sections.
15
+ - **Reorder** sections with ↑/↓ arrows or **drag & drop** (HTML5). Order is stored as a virtual 0-based index, so there are never duplicate or fractional orders.
16
+ - **Tools**
17
+ - **Blacklist** (`exclude`): hide the listed tools.
18
+ - **Whitelist** (`include`): keep only the listed tools (wins over exclude).
19
+ - Only the model-facing catalog is affected — tools and routes keep working.
20
+ - **Presets**
21
+ - **Save** the current customization as a preset (full snapshot).
22
+ - **Apply** a preset: same-name sections are overridden, preset-only sections are added, and current sections outside the preset list are disabled by default.
23
+ - **Export / Import** presets as JSON (full snapshot).
24
+ - Presets store **relative order** (each section records the section it follows), so they stay portable across prompts with different section sets.
25
+ - Multiple presets can be stored locally; only one is active at a time.
26
+
27
+ ## Installation
28
+
29
+ Install from npm:
30
+
31
+ ```bash
32
+ dsh plugin add dsh-prompt-customizer
33
+ ```
34
+
35
+ Or install from a local checkout:
36
+
37
+ ```bash
38
+ dsh plugin add /path/to/dsh-prompt-customizer
39
+ ```
40
+
41
+ After installation, open the dsh web UI → **Settings** → **提示词定制** (Prompt Customizer) in the sidebar.
42
+
43
+ ## Usage
44
+
45
+ ### Prompt sections tab
46
+
47
+ Each row shows a prompt section with:
48
+
49
+ - A **checkbox** to block/unblock it.
50
+ - A **replace** button to edit its text (the original text is pre-filled).
51
+ - **↑/↓ arrows** and a **drag handle** to reorder it.
52
+ - The `#N` badge shows the section's virtual order (0-based position).
53
+
54
+ The **注入新段** (Inject section) box at the bottom adds a brand-new section by name, order, and text.
55
+
56
+ ### Tools tab
57
+
58
+ Toggle tools to hide them (blacklist), or switch to whitelist mode to keep only the checked tools.
59
+
60
+ ### Presets tab
61
+
62
+ - **Save current as preset** — capture the current sections/tools customization.
63
+ - **Apply** — activate a preset (overrides same-name sections, adds preset-only sections, disables current sections outside the preset).
64
+ - **Export** — download the preset as JSON.
65
+ - **Import** — load a preset JSON file (same-name presets are skipped).
66
+
67
+ ## Configuration
68
+
69
+ The plugin stores its config in the dsh settings document under the `prompt-customizer` namespace (`~/.dsh/settings.yaml`). You can edit it directly:
70
+
71
+ ```yaml
72
+ prompt-customizer:
73
+ # Block these prompt sections by name.
74
+ sections:
75
+ - plugin:some-plugin
76
+ # Replace a section's text by name (original order kept).
77
+ replace:
78
+ system: 'You are a helpful coding assistant.'
79
+ # Inject / override sections (name + order + text).
80
+ inject:
81
+ - name: tool:read
82
+ order: 0
83
+ text: ''
84
+ # Tool catalog filtering.
85
+ tools:
86
+ exclude: []
87
+ include: []
88
+ ```
89
+
90
+ | Field | Type | Description |
91
+ | --- | --- | --- |
92
+ | `sections` | `string[]` | Prompt section names to block. |
93
+ | `replace` | `Record<string, string>` | Replacement text per section name. |
94
+ | `inject` | `{name, order, text}[]` | Sections to inject or override (order controls splice position). |
95
+ | `tools.exclude` | `string[]` | Tool names to hide. |
96
+ | `tools.include` | `string[]` | When non-empty, keep only these tools (wins over `exclude`). |
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ npm install
102
+ npm run check # typecheck + build
103
+ ```
104
+
105
+ The browser half is built with [tsdown](https://github.com/rolldown/tsdown) into `client/client.js` (a `__ModuleLoader__` factory bundle). The host half lives in `lib/`.
106
+
107
+ ## Known limitations
108
+
109
+ - **Replacing a dynamically generated section freezes its content**: sections like `app:web-surface` are generated live at assembly time (e.g. they embed the current Web server URL). After replacing, the text becomes fixed at the value you edited and no longer tracks runtime state such as the port. To keep following runtime changes, block the section instead of replacing it.
110
+ - **Dynamic text resolution is best-effort**: the panel tries to call a dynamic section's generator function (with a minimal context) to echo its real content. A few dynamic sections that depend on richer context and throw when called fall back to showing `<动态生成>`; the rest of the features are unaffected.
111
+
112
+ ## License
113
+
114
+ [MIT](LICENSE) © 2026 DreamsTOF
@@ -0,0 +1,2 @@
1
+ en: README.en.md
2
+ zh-CN: README.md
package/README.md CHANGED
@@ -1,107 +1,114 @@
1
1
  # dsh-prompt-customizer
2
2
 
3
- A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) plugin that lets you control the **system prompt** and the **tool catalog** from a settings-panel UI.
3
+ > [English](README.en.md) | **简体中文**
4
4
 
5
- Plugin-injected prompt sections (from other plugins) can pollute your system prompt. This plugin lets you **block**, **replace**, **inject**, and **reorder** prompt sections by name, and **hide tools** from the model catalog — all live, without touching the other plugins.
5
+ 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 插件,让你通过设置面板 UI 控制**系统提示词**和**工具目录**。
6
6
 
7
- ## Features
7
+ 其他插件注入的提示词段可能会污染你的系统提示词。本插件让你可以按名称**屏蔽**、**替换**、**注入**和**排序**提示词段,并从模型目录中**隐藏工具**——全部实时生效,无需改动其他插件。
8
8
 
9
- - **Prompt sections**
10
- - **Block** a section by name (removed from the assembled system prompt).
11
- - **Replace** a section's text (the original text is echoed for editing).
12
- - **Inject** brand-new sections.
13
- - **Reorder** sections with ↑/↓ arrows or **drag & drop** (HTML5). Order is stored as a virtual 0-based index, so there are never duplicate or fractional orders.
14
- - **Tools**
15
- - **Blacklist** (`exclude`): hide the listed tools.
16
- - **Whitelist** (`include`): keep only the listed tools (wins over exclude).
17
- - Only the model-facing catalog is affected — tools and routes keep working.
18
- - **Presets**
19
- - **Save** the current customization as a preset (full snapshot).
20
- - **Apply** a preset: same-name sections are overridden, preset-only sections are added, and current sections outside the preset list are disabled by default.
21
- - **Export / Import** presets as JSON (full snapshot).
22
- - Presets store **relative order** (each section records the section it follows), so they stay portable across prompts with different section sets.
23
- - Multiple presets can be stored locally; only one is active at a time.
9
+ ## 功能特性
24
10
 
25
- ## Installation
11
+ - **提示词段**
12
+ - **屏蔽**指定名称的段(从组装后的系统提示词中移除)。
13
+ - **替换**某段的文本(编辑时会回显原始文本,包括动态生成的段)。
14
+ - **注入**全新的段。
15
+ - 通过 **↑/↓ 箭头**或**拖拽**(HTML5)排序。顺序以虚拟的 0 起始下标存储,因此不会出现重复或小数 order。
16
+ - **工具**
17
+ - **黑名单**(`exclude`):隐藏列出的工具。
18
+ - **白名单**(`include`):仅保留列出的工具(优先于 exclude)。
19
+ - 只影响面向模型的目录,工具与路由仍正常工作。
20
+ - **预设**
21
+ - **保存**当前定制为预设(完整快照)。
22
+ - **应用**预设:同名段被覆盖、预设独有段被添加、预设名单之外的当前段默认被屏蔽。
23
+ - **导出 / 导入**预设为 JSON(完整快照)。
24
+ - 预设存储**相对顺序**(每段记录它跟在哪个段之后),因此可跨不同段集合的提示词移植。
25
+ - 本地可保存多个预设,同一时间只能激活一个。
26
26
 
27
- Install from npm:
27
+ ## 安装
28
+
29
+ 从 npm 安装:
28
30
 
29
31
  ```bash
30
32
  dsh plugin add dsh-prompt-customizer
31
33
  ```
32
34
 
33
- Or install from a local checkout:
35
+ 或从本地目录安装:
34
36
 
35
37
  ```bash
36
38
  dsh plugin add /path/to/dsh-prompt-customizer
37
39
  ```
38
40
 
39
- After installation, open the dsh web UI → **Settings** → **提示词定制** (Prompt Customizer) in the sidebar.
41
+ 安装后,打开 dsh Web UI → **设置** → 侧边栏中的 **提示词定制**。
40
42
 
41
- ## Usage
43
+ ## 使用说明
42
44
 
43
- ### Prompt sections tab
45
+ ### 提示词 Tab
44
46
 
45
- Each row shows a prompt section with:
47
+ 每一行代表一个提示词段,包含:
46
48
 
47
- - A **checkbox** to block/unblock it.
48
- - A **replace** button to edit its text (the original text is pre-filled).
49
- - **↑/↓ arrows** and a **drag handle** to reorder it.
50
- - The `#N` badge shows the section's virtual order (0-based position).
49
+ - **复选框**:屏蔽 / 取消屏蔽。
50
+ - **替换**按钮:编辑该段文本(默认回显原始文本)。
51
+ - **↑/↓ 箭头**和**拖拽手柄**:调整顺序。
52
+ - `#N` 徽标显示该段的虚拟顺序(0 起始的位置)。
51
53
 
52
- The **注入新段** (Inject section) box at the bottom adds a brand-new section by name, order, and text.
54
+ 底部的 **注入新段** 区域可按名称、顺序和文本添加全新段。
53
55
 
54
- ### Tools tab
56
+ ### 工具 Tab
55
57
 
56
- Toggle tools to hide them (blacklist), or switch to whitelist mode to keep only the checked tools.
58
+ 勾选工具以隐藏(黑名单),或切换到白名单模式仅保留勾选的工具。
57
59
 
58
- ### Presets tab
60
+ ### 预设 Tab
59
61
 
60
- - **Save current as preset** — capture the current sections/tools customization.
61
- - **Apply** — activate a preset (overrides same-name sections, adds preset-only sections, disables current sections outside the preset).
62
- - **Export** — download the preset as JSON.
63
- - **Import** — load a preset JSON file (same-name presets are skipped).
62
+ - **保存当前为预设**:捕获当前的提示词 / 工具定制。
63
+ - **应用**:激活某个预设(覆盖同名段、添加预设独有段、屏蔽预设名单之外的当前段)。
64
+ - **导出**:将预设下载为 JSON。
65
+ - **导入**:加载预设 JSON 文件(同名预设会被跳过)。
64
66
 
65
- ## Configuration
67
+ ## 配置
66
68
 
67
- The plugin stores its config in the dsh settings document under the `prompt-customizer` namespace (`~/.dsh/settings.yaml`). You can edit it directly:
69
+ 插件配置存储在 dsh 设置文档的 `prompt-customizer` 命名空间下(`~/.dsh/settings.yaml`)。你也可以直接编辑:
68
70
 
69
71
  ```yaml
70
72
  prompt-customizer:
71
- # Block these prompt sections by name.
73
+ # 按名称屏蔽这些提示词段。
72
74
  sections:
73
75
  - plugin:some-plugin
74
- # Replace a section's text by name (original order kept).
76
+ # 按名称替换某段文本(保留原始顺序)。
75
77
  replace:
76
78
  system: 'You are a helpful coding assistant.'
77
- # Inject / override sections (name + order + text).
79
+ # 注入 / 覆盖段(名称 + 顺序 + 文本)。
78
80
  inject:
79
81
  - name: tool:read
80
82
  order: 0
81
83
  text: ''
82
- # Tool catalog filtering.
84
+ # 工具目录过滤。
83
85
  tools:
84
86
  exclude: []
85
87
  include: []
86
88
  ```
87
89
 
88
- | Field | Type | Description |
90
+ | 字段 | 类型 | 说明 |
89
91
  | --- | --- | --- |
90
- | `sections` | `string[]` | Prompt section names to block. |
91
- | `replace` | `Record<string, string>` | Replacement text per section name. |
92
- | `inject` | `{name, order, text}[]` | Sections to inject or override (order controls splice position). |
93
- | `tools.exclude` | `string[]` | Tool names to hide. |
94
- | `tools.include` | `string[]` | When non-empty, keep only these tools (wins over `exclude`). |
92
+ | `sections` | `string[]` | 要屏蔽的提示词段名称。 |
93
+ | `replace` | `Record<string, string>` | 每个段名称对应的替换文本。 |
94
+ | `inject` | `{name, order, text}[]` | 要注入或覆盖的段(order 控制拼接位置)。 |
95
+ | `tools.exclude` | `string[]` | 要隐藏的工具名称。 |
96
+ | `tools.include` | `string[]` | 非空时仅保留这些工具(优先于 `exclude`)。 |
95
97
 
96
- ## Development
98
+ ## 开发
97
99
 
98
100
  ```bash
99
101
  npm install
100
102
  npm run check # typecheck + build
101
103
  ```
102
104
 
103
- The browser half is built with [tsdown](https://github.com/rolldown/tsdown) into `client/client.js` (a `__ModuleLoader__` factory bundle). The host half lives in `lib/`.
105
+ 浏览器端使用 [tsdown](https://github.com/rolldown/tsdown) 打包为 `client/client.js`(`__ModuleLoader__` factory bundle)。宿主端代码位于 `lib/`。
106
+
107
+ ## 已知限制
108
+
109
+ - **替换动态生成的段会固定其生成内容**:像 `app:web-surface` 这类段在装配时实时生成(如嵌入当前 Web 服务器 URL)。替换后,文本会固定为您编辑时的值,不再随端口等运行时状态自动更新;如需跟随运行时变化,请勿替换该段,改用屏蔽。
110
+ - **动态文本解析是尽力而为**:面板读取动态段时会尝试调用其生成函数并传入最小上下文,以便回显真实内容。个别依赖更复杂上下文、调用时会抛错的动态段会回退显示 `<动态生成>`,不影响其余功能。
104
111
 
105
112
  ## License
106
113
 
107
- MIT
114
+ [MIT](LICENSE) © 2026 DreamsTOF
package/client/client.js CHANGED
@@ -281,6 +281,121 @@ window.__ModuleLoader__.load({
281
281
  };
282
282
  })();
283
283
  //#endregion
284
+ //#region src/client/presets.ts
285
+ /** Merge the inventory sections with the user's inject list, sorted by order. */
286
+ function mergeSections(inv, cfg, blockedNames) {
287
+ const map = /* @__PURE__ */ new Map();
288
+ for (const sec of inv?.sections ?? []) map.set(sec.name, sec);
289
+ for (const item of cfg.inject ?? []) {
290
+ const existing = map.get(item.name);
291
+ if (existing) map.set(item.name, {
292
+ ...existing,
293
+ order: item.order
294
+ });
295
+ else map.set(item.name, {
296
+ name: item.name,
297
+ order: item.order,
298
+ text: item.text || "<动态生成>",
299
+ active: !blockedNames.has(item.name),
300
+ replaced: false
301
+ });
302
+ }
303
+ return [...map.values()].sort((a, b) => a.order - b.order);
304
+ }
305
+ /**
306
+ * Resolve a preset's relative order into an absolute ordered list (0..n-1).
307
+ * Sections with no anchor (or whose anchor is absent) come first in preset
308
+ * order; each anchored section is inserted right after its anchor; any
309
+ * remaining sections (cycles) are appended.
310
+ */
311
+ function resolveOrder(presetOrder) {
312
+ const list = presetOrder ?? [];
313
+ const afterMap = /* @__PURE__ */ new Map();
314
+ const textMap = /* @__PURE__ */ new Map();
315
+ for (const sec of list) {
316
+ afterMap.set(sec.name, sec.after);
317
+ textMap.set(sec.name, sec.text);
318
+ }
319
+ const result = [];
320
+ const placed = /* @__PURE__ */ new Set();
321
+ for (const sec of list) {
322
+ const anchor = afterMap.get(sec.name);
323
+ if (!anchor || !afterMap.has(anchor)) {
324
+ result.push(sec.name);
325
+ placed.add(sec.name);
326
+ }
327
+ }
328
+ let changed = true;
329
+ while (changed) {
330
+ changed = false;
331
+ for (const sec of list) {
332
+ if (placed.has(sec.name)) continue;
333
+ const anchor = afterMap.get(sec.name);
334
+ if (anchor && placed.has(anchor)) {
335
+ result.splice(result.indexOf(anchor) + 1, 0, sec.name);
336
+ placed.add(sec.name);
337
+ changed = true;
338
+ }
339
+ }
340
+ }
341
+ for (const sec of list) if (!placed.has(sec.name)) {
342
+ result.push(sec.name);
343
+ placed.add(sec.name);
344
+ }
345
+ return result.map((name, i) => ({
346
+ name,
347
+ order: i,
348
+ text: textMap.get(name) ?? ""
349
+ }));
350
+ }
351
+ /**
352
+ * Build a preset snapshot from the current config. The order list is derived
353
+ * from the MERGED section list (not just `cfg.inject`) so that a preset saved
354
+ * after only blocking sections (without reordering) still carries the full
355
+ * ordered set, and thus a meaningful "active list" when applied.
356
+ */
357
+ function buildPresetData(cfg, merged) {
358
+ const order = merged.map((sec, i) => ({
359
+ name: sec.name,
360
+ after: i > 0 ? merged[i - 1].name : void 0,
361
+ text: sec.text ?? ""
362
+ }));
363
+ return {
364
+ sections: cfg.sections,
365
+ replace: cfg.replace,
366
+ order,
367
+ tools: cfg.tools
368
+ };
369
+ }
370
+ /**
371
+ * Compute the config patch when applying a preset:
372
+ * - same-name sections are overridden (the order list drives this at runtime)
373
+ * - preset sections missing from the current prompt are added
374
+ * - current sections NOT in the preset's ordered list are disabled by default
375
+ * - only the preset's ACTIVE sections (in the order list but not in its block
376
+ * list) are unblocked; the preset's own blocked sections stay blocked.
377
+ */
378
+ function applyPresetData(data, cfg, currentNames) {
379
+ const presetOrder = data.order ?? [];
380
+ const presetNames = new Set(presetOrder.map((x) => x.name));
381
+ const blocked = new Set(data.sections ?? []);
382
+ const activeNames = new Set([...presetNames].filter((n) => !blocked.has(n)));
383
+ for (const name of currentNames) if (!presetNames.has(name)) blocked.add(name);
384
+ for (const name of activeNames) blocked.delete(name);
385
+ return {
386
+ sections: [...blocked],
387
+ replace: {
388
+ ...cfg.replace ?? {},
389
+ ...data.replace ?? {}
390
+ },
391
+ inject: resolveOrder(presetOrder),
392
+ tools: {
393
+ exclude: data.tools?.exclude ?? [],
394
+ include: data.tools?.include ?? []
395
+ }
396
+ };
397
+ }
398
+ //#endregion
284
399
  //#region src/client/SectionsTab.tsx
285
400
  /** Prompt-sections tab: block / replace / inject / reorder prompt sections. */
286
401
  function SectionsTab({ cfg, inv, scope, t }) {
@@ -290,25 +405,7 @@ window.__ModuleLoader__.load({
290
405
  const [draft, setDraft] = (0, react.useState)("");
291
406
  const [dragName, setDragName] = (0, react.useState)(null);
292
407
  const [dropTarget, setDropTarget] = (0, react.useState)(null);
293
- const merged = (() => {
294
- const map = /* @__PURE__ */ new Map();
295
- for (const sec of inv?.sections ?? []) map.set(sec.name, sec);
296
- for (const item of cfg.inject ?? []) {
297
- const existing = map.get(item.name);
298
- if (existing) map.set(item.name, {
299
- ...existing,
300
- order: item.order
301
- });
302
- else map.set(item.name, {
303
- name: item.name,
304
- order: item.order,
305
- text: item.text || "<动态生成>",
306
- active: !blockedNames.has(item.name),
307
- replaced: false
308
- });
309
- }
310
- return [...map.values()].sort((a, b) => a.order - b.order);
311
- })();
408
+ const merged = mergeSections(inv, cfg, blockedNames);
312
409
  const toggleBlock = (name, currentlyBlocked) => {
313
410
  const list = (cfg.sections ?? []).slice();
314
411
  if (currentlyBlocked) {
@@ -603,26 +700,11 @@ window.__ModuleLoader__.load({
603
700
  const presets = cfg.presets ?? [];
604
701
  const [name, setName] = (0, react.useState)("");
605
702
  const fileRef = (0, react.useRef)(null);
606
- const currentNames = (() => {
607
- const set = /* @__PURE__ */ new Set();
608
- for (const sec of inv?.sections ?? []) set.add(sec.name);
609
- for (const item of cfg.inject ?? []) set.add(item.name);
610
- return set;
611
- })();
703
+ const merged = mergeSections(inv, cfg, new Set(cfg.sections ?? []));
704
+ const currentNames = new Set(merged.map((sec) => sec.name));
612
705
  const saveCurrent = () => {
613
706
  const presetName = name.trim() || `${t("preset")} ${presets.length + 1}`;
614
- const inject = cfg.inject ?? [];
615
- const order = inject.map((sec, i) => ({
616
- name: sec.name,
617
- after: i > 0 ? inject[i - 1].name : void 0,
618
- text: sec.text
619
- }));
620
- const data = {
621
- sections: cfg.sections,
622
- replace: cfg.replace,
623
- order,
624
- tools: cfg.tools
625
- };
707
+ const data = buildPresetData(cfg, merged);
626
708
  scope.set("presets", [...presets, {
627
709
  id: genId(),
628
710
  name: presetName,
@@ -631,21 +713,11 @@ window.__ModuleLoader__.load({
631
713
  setName("");
632
714
  };
633
715
  const applyPreset = (preset) => {
634
- const presetOrder = preset.data.order ?? [];
635
- const presetNames = new Set(presetOrder.map((x) => x.name));
636
- const blocked = new Set(preset.data.sections ?? []);
637
- for (const name of currentNames) if (!presetNames.has(name)) blocked.add(name);
638
- for (const name of presetNames) blocked.delete(name);
639
- scope.set("inject", resolveOrder(presetOrder));
640
- scope.set("replace", {
641
- ...cfg.replace ?? {},
642
- ...preset.data.replace ?? {}
643
- });
644
- scope.set("sections", [...blocked]);
645
- scope.set("tools", preset.data.tools ?? {
646
- exclude: [],
647
- include: []
648
- });
716
+ const patch = applyPresetData(preset.data, cfg, currentNames);
717
+ scope.set("inject", patch.inject);
718
+ scope.set("replace", patch.replace);
719
+ scope.set("sections", patch.sections);
720
+ scope.set("tools", patch.tools);
649
721
  scope.set("activePreset", preset.id);
650
722
  };
651
723
  const deletePreset = (id) => {
@@ -734,51 +806,6 @@ window.__ModuleLoader__.load({
734
806
  function genId() {
735
807
  return "p_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
736
808
  }
737
- /**
738
- * Resolve a preset's relative order into an absolute ordered list (0..n-1).
739
- * Sections with no anchor (or whose anchor is absent) come first in preset
740
- * order; each anchored section is inserted right after its anchor; any
741
- * remaining sections (cycles) are appended.
742
- */
743
- function resolveOrder(presetOrder) {
744
- const afterMap = /* @__PURE__ */ new Map();
745
- const textMap = /* @__PURE__ */ new Map();
746
- for (const sec of presetOrder) {
747
- afterMap.set(sec.name, sec.after);
748
- textMap.set(sec.name, sec.text);
749
- }
750
- const result = [];
751
- const placed = /* @__PURE__ */ new Set();
752
- for (const sec of presetOrder) {
753
- const anchor = afterMap.get(sec.name);
754
- if (!anchor || !afterMap.has(anchor)) {
755
- result.push(sec.name);
756
- placed.add(sec.name);
757
- }
758
- }
759
- let changed = true;
760
- while (changed) {
761
- changed = false;
762
- for (const sec of presetOrder) {
763
- if (placed.has(sec.name)) continue;
764
- const anchor = afterMap.get(sec.name);
765
- if (anchor && placed.has(anchor)) {
766
- result.splice(result.indexOf(anchor) + 1, 0, sec.name);
767
- placed.add(sec.name);
768
- changed = true;
769
- }
770
- }
771
- }
772
- for (const sec of presetOrder) if (!placed.has(sec.name)) {
773
- result.push(sec.name);
774
- placed.add(sec.name);
775
- }
776
- return result.map((name, i) => ({
777
- name,
778
- order: i,
779
- text: textMap.get(name) ?? ""
780
- }));
781
- }
782
809
  //#endregion
783
810
  //#region src/client/index.ts
784
811
  /**
@@ -813,15 +840,6 @@ window.__ModuleLoader__.load({
813
840
  scope,
814
841
  t
815
842
  })));
816
- scoped.slots.inject("settings.plugin.item", () => scoped.slots.register({
817
- name: "settings.plugin.item",
818
- key: NS,
819
- locale: NS,
820
- inject: () => ({ t })
821
- }, () => (0, react.createElement)(Panel, {
822
- scope,
823
- t
824
- })));
825
843
  });
826
844
  }
827
845
  function Panel({ scope, t }) {