dsh-prompt-customizer 0.2.7 → 0.3.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/README.en.md CHANGED
@@ -4,110 +4,209 @@
4
4
 
5
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
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.
7
+ Plugin-injected prompt sections can pollute your system prompt. This plugin lets you **block**, **replace**, **inject** and **reorder** prompt sections by name, and **hide tools** from the model-facing catalog — live, without touching any other plugin.
8
+
9
+ Two ideas run through the whole design:
10
+
11
+ - **Per session phase.** Bootstrap (before promotion) / resident (after promotion) / compaction-controlled (after a compaction, before re-promotion) each get their **own block list, their own ordering space and their own tool catalog**. One preset can therefore show the model 1 tool and a handful of sections on the first turn, then open everything up after promotion.
12
+ - **Per agent preset.** The selector at the top chooses where edits land: `Target: global default` edits the defaults; picking an agent preset writes a field-level override for that preset only (set fields take over, unset fields keep falling back to global).
13
+
14
+ ## How the session phases flow
15
+
16
+ The phases are not a fixed three-beat rhythm — they are **derived from durable session
17
+ events**: the first durable `tool/call` or `assistant/message` promotes, and a
18
+ `compaction/end` resets that promotion. The three parts of the panel map to those states.
19
+
20
+ <table>
21
+ <tr align="center">
22
+ <td width="28%"><b>① Session starts</b><br><code>bootstrap</code><br><b>Guide period</b></td>
23
+ <td width="6%">→</td>
24
+ <td width="28%"><b>② First durable signal</b><br><code>tool/call</code> · <code>assistant/message</code><br><b>Resident period</b> <code>active</code></td>
25
+ <td width="6%">→</td>
26
+ <td width="32%"><b>③ A compaction</b><br><code>compaction/end</code><br><b>Compaction-controlled</b> <code>compaction</code></td>
27
+ </tr>
28
+ <tr>
29
+ <td colspan="5" align="center">
30
+ ④ another durable signal after the compaction → <b>back to the resident period</b>; compact again and it falls back to compaction-controlled
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
35
+ Every transition (note the guide period can reach compaction-controlled without passing through resident):
36
+
37
+ | Current phase | Event | Next |
38
+ | --- | --- | --- |
39
+ | Guide | first durable `tool/call` / `assistant/message` | **Resident** |
40
+ | Guide | `compaction/end` | **Compaction-controlled** |
41
+ | Resident | `compaction/end` (promotion reset) | **Compaction-controlled** |
42
+ | Compaction-controlled | a new durable signal after the compaction | **Resident** |
43
+ | Any | subagent session (`delegationDepth > 0`) | always **Resident** |
44
+ | Any | read with no session (inventory, preview) | always **Resident** |
45
+
46
+ What each phase resolves to — the three panel parts map one-to-one onto config fields:
47
+
48
+ | Panel part | Effective section blocks | Injected `phase` | Effective tool blacklist |
49
+ | --- | --- | --- | --- |
50
+ | Guide-period sections / tools | `sections` ∪ `sectionsBootstrap` | `bootstrap` (+ `always`) | `tools.bootstrap.exclude`, falling back to static when unset |
51
+ | Resident-period sections / tools | `sections` | `active` (+ `always`) | `tools.exclude` (static) |
52
+ | Compaction-controlled sections / tools | `sections` ∪ `sectionsCompaction` | `compaction` (+ `always`) | `tools.compaction.exclude`, falling back to bootstrap, then static |
53
+
54
+ Two things that bite:
55
+
56
+ - **`always` injections are present in all three phases**, and each phase has its own ordering space — which is why this plugin emits phase entries *after* the `always` group: the assembly applies inject records in array order and the last write wins, so the phase order is what survives.
57
+ - **A phase view is "what this state resolves to", not "what the session has been".** Native preset phase rules (zero-tool bootstrap, warmup, …) are in play at the same moment and may still change the result after other plugins — see [Known limitations](#known-limitations).
58
+
59
+ ## UI
60
+
61
+ ![Prompt tab: four tabs, edit-target selector, section rows](docs/images/prompt-sections.png)
62
+
63
+ ![Three phase parts, each with its own section list](docs/images/prompt-resident.png)
64
+
65
+ ![Tools tab: three phase catalogs with enabled/total counts](docs/images/tools-phases.png)
66
+
67
+ ![Config tab: save current config / save as agent preset / import](docs/images/config-tab.png)
68
+
69
+ ![Preview tab: model-view prompt switchable per phase, with the yellow warning for a complete-section takeover](docs/images/preview-tab.png)
8
70
 
9
71
  ## Features
10
72
 
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.
73
+ ### Prompt sections
74
+
75
+ - **Block** — remove a section by name. A blocked row does not disappear: it stays draggable and editable and can be restored at any time. Blocking only means "not injected into the model".
76
+ - **Replace** — edit a section's text; the editor is pre-filled with the text currently in effect (dynamically generated sections are resolved first so you see real content).
77
+ - **Inject** — every phase part has its own name + text box to add a brand-new section, badged `manual` and deletable.
78
+ - **Reorder** — drag a whole row (an insert line appears above/below the target row) or use ↑/↓. Order is stored as a contiguous 0-based virtual index, so there are never duplicate or fractional orders and no number to type by hand.
79
+ - **Independent per phase** — bootstrap blocks go to `sectionsBootstrap`, compaction blocks to `sectionsCompaction`, resident blocks to `sections`; each phase has its own ordering space, so "5 sections during bootstrap, everything when resident" is expressible.
80
+
81
+ ### Tool catalog
82
+
83
+ - **Blacklist only** (`exclude`) — unchecking a tool hides it **in that phase**; checking it back restores it. There is no whitelist mode: it silenced `exclude` entirely, and a single drag could quietly switch off every other tool in the phase while offering no expressive gain.
84
+ - **Dragging in = make that one tool visible in the phase** (remove it from the phase blacklist); dragging back to *All system tools* hides it there. Every action touches only the tool you dragged.
85
+ - **Phase catalogs** — bootstrap and compaction each have their own blacklist. Runtime precedence is **compaction > bootstrap > static**: after a compaction with a compaction catalog configured, it applies; otherwise an un-promoted session uses the bootstrap catalog; everything else uses the static filter.
86
+ - **Narrowing only, never widening** — the filter operates on the catalog the assembly already produced. A tool a phase never had cannot be made to appear here; that means editing the preset's composition.
87
+ - Only the model-facing catalog is affected; the tools themselves and routing keep working.
88
+
89
+ ### Model-view preview
90
+
91
+ The Preview tab shows the final system prompt and tool catalog after **all** plugins have filtered, switchable per phase. It reads the same assembly as the prompt and tools tabs, so what you see is what the model sees. The tools view reports "visible to the model / registry total" side by side — presets such as PTC or Code Mode wrap the whole catalog into a single `run_code`, so the registry alone misleads.
92
+
93
+ ### Config snapshots and agent presets
94
+
95
+ - **Save current config** — capture the complete customization of the current scope (per-phase block lists, per-phase ordering and phase tool catalogs) as a reusable snapshot.
96
+ - **Apply** — same-name sections are overridden, snapshot-only sections are added, and current sections outside the snapshot list are blocked. Phase fields the snapshot does not carry keep your current values instead of being wiped. Only one snapshot is active at a time.
97
+ - **Export / import** — snapshots round-trip as JSON (native save/open dialogs under Tauri, download / file picker on web); same-name presets are skipped. Order is stored relatively (each section records which section it follows), so snapshots stay portable across different section sets.
98
+ - **Save as agent preset** — **copy** the whole preset directory of the current edit target (composition file, companion scripts, skill directories) into the user preset root, then write the current customization into its override. The new preset immediately appears in the target selector: a real switchable agent preset, not merely a config copy.
99
+
100
+ ### Cross-preset registry pool
101
+
102
+ *All system sections* / *All system tools* do not follow the edit target: they are a **union across presets that only ever grows** (same name — last seen wins; new name — appended), filled in as you browse presets and stored in the derived cache `catalog.yaml`, which rebuilds itself if deleted.
103
+
104
+ ## How it works
105
+
106
+ The full path a change takes from the panel to the model:
107
+
108
+ <table>
109
+ <tr align="center">
110
+ <td width="19%"><b>① Settings panel</b><br>sections / tools / config / preview<br><i>edits an in-memory draft only</i></td>
111
+ <td width="3%">→</td>
112
+ <td width="19%"><b>② Plugin routes</b><br><code>/config</code> · <code>/config/apply</code><br><code>/preview</code> · <code>/inventory</code> · <code>/presets</code></td>
113
+ <td width="3%">→</td>
114
+ <td width="19%"><b>③ config.yaml</b><br>global fields + <code>overrides[preset]</code><br><i>mtime-lazy reload, hand edits apply at once</i></td>
115
+ <td width="3%">→</td>
116
+ <td width="34%"><b>④ Assembly waterfall</b><br>the <code>system-prompt/assemble</code> hook derives the phase from the session, resolves the effective config and filters</td>
117
+ </tr>
118
+ <tr>
119
+ <td colspan="9" align="center"><b>inside ④, in order</b>: <code>mergeConfig(global, overrides[session preset])</code> → sections: block → replace → inject → sort by <code>order</code>; tools: <code>pickToolsFilter</code> (compaction &gt; bootstrap &gt; static) → <code>applyToolFilter</code></td>
120
+ </tr>
121
+ <tr>
122
+ <td colspan="9" align="center">
123
+ <b>⑤ one last move before handing in</b>: after the waterfall the host restores the single <code>complete: true</code> section as the whole <code>sections</code> array when one exists — exactly why the panel shows a yellow warning for such presets (the tool catalog is unaffected)
124
+ </td>
125
+ </tr>
126
+ <tr>
127
+ <td colspan="9" align="center"><b>⑥ the model receives</b>: the final system prompt + visible tool catalog; the Preview tab reads this very result</td>
128
+ </tr>
129
+ </table>
130
+
131
+ Code map:
132
+
133
+ | Path | Role |
134
+ | --- | --- |
135
+ | `lib/index.js` | host half: assembly hook, five HTTP routes, agent-preset fork, one-off legacy cleanup |
136
+ | `lib/effective.js` | pure functions: field-level override merge, phase selection (injections / tool catalogs), blacklist filtering |
137
+ | `lib/promotion.js` | derives `{promoted, boundary}` from durable events — compaction resets, subagents always promoted |
138
+ | `lib/sectionOps.mjs` | phase-state pure functions **shared by the panel and the unit tests** (which list to write, injection identity, reorder, per-phase persistence) |
139
+ | `lib/store.js` / `lib/catalog.js` | atomic write + last-good fallback config store; cross-preset section/tool accumulation cache |
140
+ | `src/client/` | the panel (React written with `h()`, no JSX sugar), bundled into `client/client.js` |
141
+
142
+ The key convention: phase logic exists once. `lib/sectionOps.mjs` is both bundled into the
143
+ browser by tsdown and imported directly by `node --test`, so there is no second
144
+ implementation standing between "tests pass" and "the UI behaves".
26
145
 
27
146
  ## Installation
28
147
 
29
- Install from npm (replace `web` with your profile name):
30
-
31
148
  ```bash
32
149
  dsh plugin --profile web add dsh-prompt-customizer
33
150
  ```
34
151
 
35
- Or install from a local checkout:
152
+ Or from a local checkout:
36
153
 
37
154
  ```bash
38
155
  dsh plugin --profile web add /path/to/dsh-prompt-customizer
39
156
  ```
40
157
 
41
- After installation, open the dsh web UI → **Settings** → **提示词定制** (Prompt Customizer) in the sidebar.
158
+ Then open the dsh web UI → **Settings** → **提示词定制** (Prompt Customizer) in the sidebar.
42
159
 
43
160
  ## Usage
44
161
 
45
- ### Prompt sections tab
162
+ ### Prompt tab
46
163
 
47
- Each row shows a prompt section with:
164
+ The three phase parts (bootstrap / resident / compaction-controlled) are **always rendered** — if a preset has no such phase, that part is simply empty rather than hidden. Each row offers:
48
165
 
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).
166
+ - a **checkbox** to block / unblock (`未屏蔽` / `已屏蔽` badge)
167
+ - **Edit** to replace the section text; replaced rows carry an `已替换` badge and a restore button
168
+ - **↑/↓** and a **drag handle** to reorder within the phase; `#N` is the current virtual index
169
+ - a `system` / `manual` badge for provenance — only `manual` sections can be deleted
53
170
 
54
- The **注入新段** (Inject section) box at the bottom adds a brand-new section by name, order, and text.
171
+ `阶段可见 N` next to each title is the number of rows in that part. The name + text box at the bottom injects into that part, with the phase already fixed.
55
172
 
56
173
  ### Tools tab
57
174
 
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
- ```
175
+ One part per phase, chip-listing the catalog entering that phase's filter, titled `阶段可见 enabled / total`. Check to keep a tool visible in that phase, uncheck to hide it; dragging a tool in from *All system tools* only makes **that one tool** visible in the phase, and dragging it back hides it there. For what the model actually receives, use the tools view in the Preview tab.
89
176
 
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`). |
177
+ ### Config tab
178
+
179
+ Three cards: **Save current config** (snapshot), **Save as agent preset** (fork the current target's preset directory), **Import config**. Each snapshot row is Apply / Export / Delete, and the active row carries an "in use" badge.
180
+
181
+ ### Edit target and saving
182
+
183
+ The selector at the top decides where changes land. The prompt and tools tabs share one unsaved draft, and **Save** writes it in a single request; switching to the config / preview tabs or changing the edit target discards the draft (after confirming). Nothing you mistype reaches the file until you press Save.
184
+
185
+ ## Where the config lives
186
+
187
+ - `~/.dsh/prompt-customizer/config.yaml` — the single source of truth. On first start, if this file is missing but the legacy `settings.yaml` has the section, it is migrated once (the master document is left intact and stays rollback-able). Hand edits take effect on the next read, no restart needed.
188
+ - `~/.dsh/prompt-customizer/catalog.yaml` — the cross-preset accumulation cache for the pools above. Purely derived; delete it and it rebuilds as you browse.
189
+
190
+ ## Known limitations
191
+
192
+ All of the below are measured, and the panel shows a yellow warning in exactly these situations rather than leaving you to guess:
193
+
194
+ - **Some presets have no section-composed prompt.** If a section is registered with `complete: true` (dsh-persona's wholesale takeover), the host restores that single section as the entire `sections` array **after** the assembly waterfall — so this plugin's blocking / replacement / injection / ordering never reaches the model (tool filtering still applies). The panel says so explicitly ("taken over wholesale by `deployment:persona`"). The only way to make section customization work is to turn off `complete` in that agent preset.
195
+ - **Dropping sections has more than one cause.** Besides `complete` takeover, a preset can also trim sections itself for a specific phase (measured: 20 → 1 sections during bootstrap, with no `complete` section in the registry). The panel therefore also compares names mechanically: it diffs the sections this plugin emitted against the final assembly and reports "emitted N, model sees M" when they disagree.
196
+ - **The synthetic session behind a preview can degrade.** Phase views are driven by a fabricated session; some preset plugins throw on it, so that phase falls back to an agent-less assembly and is marked degraded — i.e. it shows the result *without* native phase narrowing.
197
+ - **Replacing a dynamically generated section freezes its content.** Sections like `app:web-surface` are generated at assembly time (embedding the current web port and so on). After replacing, the text is fixed at the value you edited and no longer tracks runtime state. To keep following it, block the section instead.
198
+ - **Dynamic text resolution is best-effort.** The panel calls a dynamic section's generator with a minimal context to echo real content; sections that need richer context and throw fall back to `<动态生成>`, with no other effect. Also, sections containing `{{model}}` / `{{cwd}}` style variables show the **unsubstituted template** in the list — the Preview tab is where the substituted final text appears.
199
+ - **Export depends on the host allowing downloads.** The web fallback uses a browser download: if the host webview silently drops it, the panel still reports "exported" while no file lands. Import (file picker) is unaffected.
200
+ - **Tool catalogs cannot be widened** — see "narrowing only" above.
97
201
 
98
202
  ## Development
99
203
 
100
204
  ```bash
101
205
  npm install
102
- npm run check # typecheck + build
206
+ npm run check # typecheck + build + full test suite
103
207
  ```
104
208
 
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.
209
+ 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/`. Phase state logic sits in `lib/sectionOps.mjs`: the same pure functions are bundled into the UI and exercised directly by `node --test`, so the tests are the shipped code.
111
210
 
112
211
  ## License
113
212
 
package/README.md CHANGED
@@ -2,112 +2,210 @@
2
2
 
3
3
  > [English](README.en.md) | **简体中文**
4
4
 
5
- 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 插件,让你通过设置面板 UI 控制**系统提示词**和**工具目录**。
5
+ 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 插件,让你在设置面板里控制**系统提示词**和**工具目录**。
6
6
 
7
- 其他插件注入的提示词段可能会污染你的系统提示词。本插件让你可以按名称**屏蔽**、**替换**、**注入**和**排序**提示词段,并从模型目录中**隐藏工具**——全部实时生效,无需改动其他插件。
7
+ 其他插件注入的提示词段可能污染你的系统提示词。本插件让你按名称**屏蔽**、**替换**、**注入**、**排序**提示词段,并把工具从**模型可见目录**里隐藏 —— 全程实时生效,不需要改动任何其他插件。
8
+
9
+ 两件事贯穿整个设计:
10
+
11
+ - **按会话阶段分层**:引导期(未晋级)/ 常驻期(晋级后)/ 压缩受控期(压缩后仍未晋级)各有**独立的屏蔽名单、独立的排序空间、独立的工具目录**。同一个预设可以做到首轮只给模型 1 个工具和几段提示词,晋级后放开全套。
12
+ - **按 agent 预设分层**:顶部选择器选「目标:全局默认」改的是默认值;选中某个 agent 预设后,改动只对该预设生效(字段级覆盖,未设置的字段继续回落全局)。
13
+
14
+ ## 会话阶段怎么流转
15
+
16
+ 阶段不是固定三拍,而是由**持久会话事件**推导出来的:首个 durable 的 `tool/call` 或
17
+ `assistant/message` 触发晋级,一次 `compaction/end` 把晋级复位。面板上三个阶段部分
18
+ 就分别对应这三种状态。
19
+
20
+ <table>
21
+ <tr align="center">
22
+ <td width="28%"><b>① 会话开始</b><br><code>bootstrap</code><br><b>引导期</b></td>
23
+ <td width="6%">→</td>
24
+ <td width="28%"><b>② 首个 durable 信号</b><br><code>tool/call</code> · <code>assistant/message</code><br><b>常驻期</b> <code>active</code></td>
25
+ <td width="6%">→</td>
26
+ <td width="32%"><b>③ 一次压缩</b><br><code>compaction/end</code><br><b>压缩受控期</b> <code>compaction</code></td>
27
+ </tr>
28
+ <tr>
29
+ <td colspan="5" align="center">
30
+ ④ 压缩后再来一个 durable 信号 → <b>回到常驻期</b>;只要再来一次压缩,又会落回压缩受控期
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
35
+ 完整的转移关系(引导期也能不经常驻期直接进压缩受控期):
36
+
37
+ | 当前阶段 | 事件 | 去向 |
38
+ | --- | --- | --- |
39
+ | 引导期 | 首个 durable `tool/call` / `assistant/message` | **常驻期** |
40
+ | 引导期 | `compaction/end` | **压缩受控期** |
41
+ | 常驻期 | `compaction/end`(晋级复位) | **压缩受控期** |
42
+ | 压缩受控期 | 压缩之后新的 durable 信号 | **常驻期** |
43
+ | 任意 | 子代理会话(`delegationDepth > 0`) | 恒为**常驻期** |
44
+ | 任意 | 无会话的读取(清单、预览) | 恒为**常驻期** |
45
+
46
+ 每个阶段各解析出什么 —— 面板的三个部分与配置字段一一对应:
47
+
48
+ | 面板里的部分 | 生效的段屏蔽 | 注入段的 `phase` | 生效的工具黑名单 |
49
+ | --- | --- | --- | --- |
50
+ | 引导期提示词 / 引导期工具 | `sections` ∪ `sectionsBootstrap` | `bootstrap`(+ `always`) | `tools.bootstrap.exclude`,未配置则回落静态 |
51
+ | 常驻期提示词 / 常驻期工具 | `sections` | `active`(+ `always`) | `tools.exclude`(静态) |
52
+ | 压缩受控期提示词 / 工具 | `sections` ∪ `sectionsCompaction` | `compaction`(+ `always`) | `tools.compaction.exclude`,未配置则回落引导期,再回落静态 |
53
+
54
+ 两点容易踩:
55
+
56
+ - **`always` 注入段三个阶段都在**,且各阶段的排序空间独立 —— 所以本插件产出的阶段条目排在 `always` 组之后(运行时按数组序后写覆盖前写,阶段序才压得住全局序)。
57
+ - **阶段视图是"这个状态会解析成什么",不是"会话曾经是什么"**。预设原生的阶段规则(zero-tool bootstrap、warmup 等)在同一时刻也在生效,并且可能在其它插件之后改动结果 —— 见[已知限制](#已知限制)。
58
+
59
+ ## 界面
60
+
61
+ ![提示词 Tab:四个 Tab、编辑目标选择器、段行控件](docs/images/prompt-sections.png)
62
+
63
+ ![三个阶段部分,各自一份段列表](docs/images/prompt-resident.png)
64
+
65
+ ![工具 Tab 的三个阶段目录与启用计数](docs/images/tools-phases.png)
66
+
67
+ ![配置 Tab:保存当前配置 / 存为 agent 预设 / 导入配置](docs/images/config-tab.png)
68
+
69
+ ![预览 Tab:按阶段切换的模型视角提示词,以及对 complete 段接管的黄色警示](docs/images/preview-tab.png)
8
70
 
9
71
  ## 功能特性
10
72
 
11
- - **提示词段**
12
- - **屏蔽**指定名称的段(从组装后的系统提示词中移除)。
13
- - **替换**某段的文本(编辑时会回显原始文本,包括动态生成的段)。
14
- - **注入**全新的段。
15
- - 通过 **↑/↓ 箭头**或**拖拽**(HTML5)排序。顺序以虚拟的 0 起始下标存储,因此不会出现重复或小数 order。
16
- - **工具**
17
- - **黑名单**(`exclude`):隐藏列出的工具。
18
- - **白名单**(`include`):仅保留列出的工具(优先于 exclude)。
19
- - 只影响面向模型的目录,工具与路由仍正常工作。
20
- - **预设**
21
- - **保存**当前定制为预设(完整快照)。
22
- - **应用**预设:同名段被覆盖、预设独有段被添加、预设名单之外的当前段默认被屏蔽。
23
- - **导出 / 导入**预设为 JSON(完整快照)。
24
- - 预设存储**相对顺序**(每段记录它跟在哪个段之后),因此可跨不同段集合的提示词移植。
25
- - 本地可保存多个预设,同一时间只能激活一个。
73
+ ### 提示词段
74
+
75
+ - **屏蔽**:按名称从装配结果里移除该段。被屏蔽的行不会消失,仍可拖拽、编辑、恢复 —— 屏蔽只意味着不注入模型。
76
+ - **替换**:编辑该段文本,编辑框预填当前生效文本(动态段会尽力解析出真实内容再回显)。
77
+ - **注入**:每个阶段部分底部都能按名称 + 文本新建一段,带 `手动` 徽标,随时可删除。
78
+ - **排序**:整行拖拽(拖到某行上方/下方出现插入线)或 ↑/↓ 箭头。顺序以 0 起始的连续虚拟下标存储,不会出现重复或小数 order,也不需要手填数字。
79
+ - **每阶段独立**:引导期屏蔽写 `sectionsBootstrap`,压缩受控期写 `sectionsCompaction`,常驻期写 `sections`;三者的排序空间互不干扰,因此「引导期只留 5 段、常驻期全开」是可以分别表达的。
80
+
81
+ ### 工具目录
82
+
83
+ - **只有黑名单**(`exclude`):取消勾选即在**该阶段**隐藏这个工具,勾回来就恢复。没有白名单模式 —— 它会让 `exclude` 整体失效,一个拖拽就能悄悄关掉该阶段其它所有工具,而表达能力并不比反选更多。
84
+ - **拖入 = 让这一个工具在该阶段可见**(从该阶段黑名单里移除);拖回「本系统全部工具」= 在该阶段隐藏它。每个动作只影响你拖的那一个工具,绝不牵连其它工具。
85
+ - **阶段目录**:引导期与压缩受控期各有独立的黑名单,运行时优先级为 **压缩受控期 > 引导期 > 静态** —— 压缩后仍未晋级且配了压缩目录就用它,否则未晋级时用引导目录,其余情况用静态过滤。
86
+ - **只能收窄,不能扩充**:过滤的对象是「装配已经给出的目录」。某个阶段本来没有的工具,无论怎么设置都不会出现(要它出现得去改那个预设的组成,不是这里)。
87
+ - 只影响面向模型的目录;工具本身与路由照常工作。
88
+
89
+ ### 模型视角预览
90
+
91
+ 预览 Tab 展示经过**所有插件**过滤后的最终系统提示词与工具目录,并按阶段切换。它与提示词 / 工具 Tab 读同一份装配结果,所见即模型所见。工具视图给出「模型可见 / 注册表总数」的对照 —— PTC、Code Mode 这类预设会把整套目录包装成单个 `run_code`,只看注册表会误判。
92
+
93
+ ### 配置快照与 agent 预设
94
+
95
+ - **保存当前配置**:把当前作用域的完整定制(含每阶段屏蔽名单、每阶段排序、阶段工具目录)存成一份可复用的配置快照。
96
+ - **应用**:同名段被覆盖、快照独有段被添加、快照名单之外的当前段被默认屏蔽;快照里没有的阶段字段保留你当前的值,不会被抹掉。同一时间只有一份在用。
97
+ - **导出 / 导入**:配置快照以 JSON 往返(Tauri 桌面走原生对话框,Web 走下载 / 文件选择),同名预设会被跳过。顺序以相对形式存储(每段记住它跟随谁),快照因此可跨不同段集合移植。
98
+ - **存为 agent 预设**:整体**复制**当前编辑目标的预设目录(组成文件、伴生脚本、技能目录一并带走)到用户预设根,再把当前定制写进它的覆盖项。新预设随即出现在顶部选择器里 —— 它是一个真正可切换的 agent 预设,而不只是一份配置副本。
99
+
100
+ ### 跨预设的注册表池
101
+
102
+ 「本系统全部提示词 / 本系统全部工具」不随编辑目标切换:它是一个**跨预设、只增不减的并集**(同名后见到者赢,新名追加),随你浏览各预设逐步长齐,存放在 `catalog.yaml` 这个派生缓存里,删掉会自行重建。
103
+
104
+ ## 工作原理
105
+
106
+ 一次改动从面板到模型的完整链路:
107
+
108
+ <table>
109
+ <tr align="center">
110
+ <td width="19%"><b>① 设置面板</b><br>提示词 / 工具 / 配置 / 预览<br><i>只改内存草稿</i></td>
111
+ <td width="3%">→</td>
112
+ <td width="19%"><b>② 插件自有路由</b><br><code>/config</code> · <code>/config/apply</code><br><code>/preview</code> · <code>/inventory</code> · <code>/presets</code></td>
113
+ <td width="3%">→</td>
114
+ <td width="19%"><b>③ config.yaml</b><br>全局字段 + <code>overrides[预设]</code><br><i>mtime 懒加载,手改即时生效</i></td>
115
+ <td width="3%">→</td>
116
+ <td width="34%"><b>④ 装配瀑布流</b><br><code>system-prompt/assemble</code> 钩子按会话推导阶段,<br>解析出生效配置并过滤</td>
117
+ </tr>
118
+ <tr>
119
+ <td colspan="9" align="center"><b>④ 内部依次是</b>:<code>mergeConfig(全局, overrides[会话所属预设])</code> → 段:屏蔽 → 替换 → 注入 → 按 order 排序;工具:<code>pickToolsFilter</code>(压缩 &gt; 引导 &gt; 静态)→ <code>applyToolFilter</code></td>
120
+ </tr>
121
+ <tr>
122
+ <td colspan="9" align="center">
123
+ <b>⑤ 交卷前还有一手</b>:宿主在瀑布流<b>之后</b>,若存在 <code>complete: true</code> 的段就把整份 sections 还原成那一条 —— 这就是面板会对这类预设亮黄标的原因(工具目录不受影响)
124
+ </td>
125
+ </tr>
126
+ <tr>
127
+ <td colspan="9" align="center"><b>⑥ 模型收到</b>:最终系统提示词 + 可见工具目录;预览 Tab 读的就是这一份结果</td>
128
+ </tr>
129
+ </table>
130
+
131
+ 代码地图:
132
+
133
+ | 路径 | 作用 |
134
+ | --- | --- |
135
+ | `lib/index.js` | 宿主端:装配钩子、五条 HTTP 路由、agent 预设 fork、遗留字段一次性清理 |
136
+ | `lib/effective.js` | 纯函数:字段级 override 合并、阶段选取(注入段 / 工具目录)、黑名单过滤 |
137
+ | `lib/promotion.js` | 由 durable 事件推导 `{promoted, boundary}`,压缩复位、子代理恒已晋级 |
138
+ | `lib/sectionOps.mjs` | **面板与单测共用**的阶段状态纯函数(名单写回目标、注入身份、重排、逐阶段持久化) |
139
+ | `lib/store.js` / `lib/catalog.js` | 原子写 + last-good 回落的配置存储;跨预设段/工具累积登记表 |
140
+ | `src/client/` | 面板(React,无 JSX 语法糖依赖,`h()` 直写),构建进 `client/client.js` |
141
+
142
+ 关键约定:阶段逻辑只有一份实现 —— `lib/sectionOps.mjs` 既被 tsdown 打进浏览器包,也被
143
+ `node --test` 直接 import 跑单测,所以「测试通过」和「界面行为」之间不存在第二套代码。
26
144
 
27
145
  ## 安装
28
146
 
29
- 从 npm 安装(将 `web` 换成你的 profile 名):
30
-
31
147
  ```bash
32
148
  dsh plugin --profile web add dsh-prompt-customizer
33
149
  ```
34
150
 
35
- 或从本地目录安装:
151
+ 也可从本地检出安装:
36
152
 
37
153
  ```bash
38
154
  dsh plugin --profile web add /path/to/dsh-prompt-customizer
39
155
  ```
40
156
 
41
- 安装后,打开 dsh Web UI → **设置** → 侧边栏中的 **提示词定制**。
157
+ 安装后打开 dsh Web UI → **设置** → 侧边栏 **提示词定制**。
42
158
 
43
159
  ## 使用说明
44
160
 
45
161
  ### 提示词 Tab
46
162
 
47
- 每一行代表一个提示词段,包含:
163
+ 三个阶段部分(引导期 / 常驻期 / 压缩受控期)**恒定全部显示** —— 某个预设没有某个阶段时,那部分只是空的,不会被隐藏。每行包含:
48
164
 
49
- - **复选框**:屏蔽 / 取消屏蔽。
50
- - **替换**按钮:编辑该段文本(默认回显原始文本)。
51
- - **↑/↓ 箭头**和**拖拽手柄**:调整顺序。
52
- - `#N` 徽标显示该段的虚拟顺序(0 起始的位置)。
165
+ - **复选框**:屏蔽 / 取消屏蔽(`未屏蔽` / `已屏蔽` 徽标)
166
+ - **编辑**:替换该段文本;替换过的行带 `已替换` 徽标与「还原」按钮
167
+ - **↑/↓** 与**拖拽手柄**:调整该阶段内的顺序;`#N` 是它当前的虚拟下标
168
+ - `系统` / `手动` 徽标标明段的来源,只有 `手动` 段可以删除
53
169
 
54
- 底部的 **注入新段** 区域可按名称、顺序和文本添加全新段。
170
+ 标题旁的 `阶段可见 N` 是该部分当前的行数。底部的名称 + 文本输入框会按所在部分注入新段(阶段已锁定,不用再选)。
55
171
 
56
172
  ### 工具 Tab
57
173
 
58
- 勾选工具以隐藏(黑名单),或切换到白名单模式仅保留勾选的工具。
59
-
60
- ### 预设 Tab
61
-
62
- - **保存当前为预设**:捕获当前的提示词 / 工具定制。
63
- - **应用**:激活某个预设(覆盖同名段、添加预设独有段、屏蔽预设名单之外的当前段)。
64
- - **导出**:将预设下载为 JSON。
65
- - **导入**:加载预设 JSON 文件(同名预设会被跳过)。
66
-
67
- ## 配置
68
-
69
- 插件配置存储在 dsh 设置文档的 `prompt-customizer` 命名空间下(`~/.dsh/settings.yaml`)。你也可以直接编辑:
70
-
71
- ```yaml
72
- prompt-customizer:
73
- # 按名称屏蔽这些提示词段。
74
- sections:
75
- - plugin:some-plugin
76
- # 按名称替换某段文本(保留原始顺序)。
77
- replace:
78
- system: 'You are a helpful coding assistant.'
79
- # 注入 / 覆盖段(名称 + 顺序 + 文本)。
80
- inject:
81
- - name: tool:read
82
- order: 0
83
- text: ''
84
- # 工具目录过滤。
85
- tools:
86
- exclude: []
87
- include: []
88
- ```
174
+ 每阶段一个部分,芯片式列出该阶段进入过滤的目录,标题旁 `阶段可见 启用数 / 总数`。勾选 = 该阶段可见,取消 = 在该阶段隐藏;从「本系统全部工具」拖入某个阶段只是让**这一个**工具在该阶段可见,拖回「全部」则在该阶段隐藏它。想看模型真正拿到几个工具,请去预览 Tab 的工具视图。
89
175
 
90
- | 字段 | 类型 | 说明 |
91
- | --- | --- | --- |
92
- | `sections` | `string[]` | 要屏蔽的提示词段名称。 |
93
- | `replace` | `Record<string, string>` | 每个段名称对应的替换文本。 |
94
- | `inject` | `{name, order, text}[]` | 要注入或覆盖的段(order 控制拼接位置)。 |
95
- | `tools.exclude` | `string[]` | 要隐藏的工具名称。 |
96
- | `tools.include` | `string[]` | 非空时仅保留这些工具(优先于 `exclude`)。 |
176
+ ### 配置 Tab
177
+
178
+ 三张卡片:**保存当前配置**(配置快照)、**存为 agent 预设**(fork 当前目标的预设目录)、**导入配置**。快照列表每行是 应用 / 导出 / 删除,正在使用的那一行带「使用中」标记。
179
+
180
+ ### 编辑目标与保存
181
+
182
+ 顶部选择器决定改动落在哪。提示词与工具 Tab 共享同一份未保存草稿,**保存**按钮一次落盘;切到配置 / 预览 Tab 或切换编辑目标会丢弃草稿(会先弹确认)。也就是说:写坏的东西不点保存就不会进文件。
183
+
184
+ ## 配置存放位置
185
+
186
+ - `~/.dsh/prompt-customizer/config.yaml` —— 唯一权威配置。首次启动时若该文件不存在而旧版 `settings.yaml` 里有对应段落,会自动迁移一次(主文档原样保留,可回滚)。手工编辑即时生效,无需重启。
187
+ - `~/.dsh/prompt-customizer/catalog.yaml` —— 跨预设的段 / 工具累积登记表,纯派生缓存,删掉会随浏览重建。
188
+
189
+ ## 已知限制
190
+
191
+ 下面都是实测结论。面板在相关情况下会直接给出黄色警示,不需要你猜:
192
+
193
+ - **有些预设的提示词不由段组成**。若某个段以 `complete: true` 注册(dsh-persona 的「整段接管」),宿主会在装配瀑布流**之后**把整份 sections 还原成那一条段 —— 此时本插件的屏蔽 / 替换 / 注入 / 排序对模型完全无效(工具过滤不受影响)。面板会写明「最终系统提示词由 `deployment:persona` 段整段接管」。要让段级定制生效,只能在该 agent 预设里关掉 persona 的 `complete`。
194
+ - **丢弃段的原因不止一种**。除了 `complete` 接管,也存在预设用自己的规则在特定阶段裁段(实测某预设引导期 20 段 → 1 段,而注册表里并没有 complete 段)。所以面板还做了一次机制无关的比对:把本插件产出的段名与最终装配逐名核对,不符时提示「产出 N 段,模型只见 M 段」。
195
+ - **预览的伪会话可能降级**。阶段视图由一次合成会话驱动,个别预设插件会在这种会话上抛错;此时该阶段回退为无会话装配并标注「已降级」,即那个阶段看到的是不带原生阶段裁剪的结果。
196
+ - **替换动态生成的段会固定其内容**。像 `app:web-surface` 这类段在装配时实时生成(嵌入当前 Web 端口等)。替换后文本冻结为你编辑时的值,不再跟随运行时变化;想保持跟随请改用屏蔽。
197
+ - **动态段文本解析是尽力而为**。面板读取动态段时会尝试调用其生成函数(只传最小上下文)来回显真实内容;依赖更复杂上下文、一调就抛的段会显示 `<动态生成>`,其余功能不受影响。另外,带 `{{model}}` / `{{cwd}}` 之类变量的段在列表里显示的是**未替换的模板**,预览 Tab 里才是替换后的最终文本。
198
+ - **导出依赖宿主允许下载**。Web 回退走浏览器下载:若宿主 webview 静默丢弃下载,面板仍会提示「导出成功」而文件没有落地。导入(文件选择)不受影响。
199
+ - **工具目录不能被扩充**,见前文「只能收窄,不能扩充」。
97
200
 
98
201
  ## 开发
99
202
 
100
203
  ```bash
101
204
  npm install
102
- npm run check # typecheck + build
205
+ npm run check # typecheck + build + 全量单测
103
206
  ```
104
207
 
105
- 浏览器端使用 [tsdown](https://github.com/rolldown/tsdown) 打包为 `client/client.js`(`__ModuleLoader__` factory bundle)。宿主端代码位于 `lib/`。
106
-
107
- ## 已知限制
108
-
109
- - **替换动态生成的段会固定其生成内容**:像 `app:web-surface` 这类段在装配时实时生成(如嵌入当前 Web 服务器 URL)。替换后,文本会固定为您编辑时的值,不再随端口等运行时状态自动更新;如需跟随运行时变化,请勿替换该段,改用屏蔽。
110
- - **动态文本解析是尽力而为**:面板读取动态段时会尝试调用其生成函数并传入最小上下文,以便回显真实内容。个别依赖更复杂上下文、调用时会抛错的动态段会回退显示 `<动态生成>`,不影响其余功能。
208
+ 浏览器端用 [tsdown](https://github.com/rolldown/tsdown) 打包成 `client/client.js`(`__ModuleLoader__` factory bundle),宿主端代码在 `lib/`。阶段状态逻辑集中在 `lib/sectionOps.mjs`:同一份纯函数既被 UI 打包使用,也被 `node --test` 直接覆盖,测试即上线代码。
111
209
 
112
210
  ## License
113
211