dsh-prompt-customizer 0.2.7 → 0.3.0

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.
@@ -1,75 +1,50 @@
1
- /** Shared types for the prompt-customizer client panel. */
2
-
3
- /** The settings scope used to read/write the namespace config. */
4
- export interface SettingsScope {
5
- getSnapshot(): { status: string; value?: unknown; revision: number; writable: boolean }
6
- subscribe(callback: () => void): () => void
7
- set(field: string, value: unknown): void
8
- unset(field: string): void
9
- }
10
-
11
- /** Inventory payload from the host route. */
12
- export interface Inventory {
13
- sections: Array<{ name: string; order: number; text?: string; active: boolean; replaced: boolean }>
14
- tools: Array<{ name: string; description?: string; hidden: boolean }>
15
- }
16
-
17
- /** Final-assembly preview payload from the host route. */
18
- export interface Preview {
19
- ok: boolean
20
- scope?: unknown
21
- sections: Array<{ name: string; text: string }>
22
- text: string
23
- tools: Array<{ name: string; description: string } | string>
24
- }
25
-
26
- /** The resolved config object (namespace value). */
27
- export interface Config {
28
- sections?: string[]
29
- replace?: Record<string, string>
30
- inject?: Array<{ name: string; order: number; text: string; custom?: boolean }>
31
- tools?: { exclude?: string[]; include?: string[] }
32
- /** Per-agent overlays: byAgent.<agentId> overrides the default fields for that agent. */
33
- byAgent?: Record<string, AgentOverlay>
34
- presets?: Preset[]
35
- activePreset?: string
36
- }
37
-
38
- /** The per-agent overlay subset of Config (everything except byAgent/presets). */
39
- export interface AgentOverlay {
40
- sections?: string[]
41
- replace?: Record<string, string>
42
- inject?: Array<{ name: string; order: number; text: string; custom?: boolean }>
43
- tools?: { exclude?: string[]; include?: string[] }
44
- }
45
-
46
- /** Agent-preset info returned by /api/prompt-customizer/agents. */
47
- export interface AgentInfo {
48
- id: string
49
- name: string
50
- description: string
51
- order: number
52
- }
53
-
54
- export interface AgentsPayload {
55
- ok: boolean
56
- agents: AgentInfo[]
57
- defaultAgent?: string
58
- error?: string
59
- }
60
-
61
- /** A complete customization snapshot that can be saved / applied / exported. */
62
- export interface Preset {
63
- id: string
64
- name: string
65
- data: PresetData
66
- }
67
-
68
- /** The customization fields a preset captures (full snapshot). */
69
- export interface PresetData {
70
- sections?: string[]
71
- replace?: Record<string, string>
72
- /** Relative order: each section records the section it should follow. */
73
- order?: Array<{ name: string; after?: string; text: string; custom?: boolean }>
74
- tools?: { exclude?: string[]; include?: string[] }
75
- }
1
+ /** Shared types for the prompt-customizer client panel. */
2
+
3
+ /** The settings scope used to read/write the namespace config. */
4
+ export interface SettingsScope {
5
+ getSnapshot(): { status: string; value?: unknown; revision: number; writable: boolean }
6
+ subscribe(callback: () => void): () => void
7
+ set(field: string, value: unknown): void
8
+ unset(field: string): void
9
+ }
10
+
11
+ /** Inventory payload from the host route. */
12
+ export interface Inventory {
13
+ sections: Array<{ name: string; order: number; text?: string; active: boolean; replaced: boolean }>
14
+ tools: Array<{ name: string; description?: string; hidden: boolean }>
15
+ }
16
+
17
+ /** Final-assembly preview payload from the host route. */
18
+ export interface Preview {
19
+ ok: boolean
20
+ scope?: unknown
21
+ sections: Array<{ name: string; text: string }>
22
+ text: string
23
+ tools: Array<{ name: string; description: string } | string>
24
+ }
25
+
26
+ /** The resolved config object (namespace value). */
27
+ export interface Config {
28
+ sections?: string[]
29
+ replace?: Record<string, string>
30
+ inject?: Array<{ name: string; order: number; text: string; custom?: boolean }>
31
+ tools?: { exclude?: string[]; include?: string[] }
32
+ presets?: Preset[]
33
+ activePreset?: string
34
+ }
35
+
36
+ /** A complete customization snapshot that can be saved / applied / exported. */
37
+ export interface Preset {
38
+ id: string
39
+ name: string
40
+ data: PresetData
41
+ }
42
+
43
+ /** The customization fields a preset captures (full snapshot). */
44
+ export interface PresetData {
45
+ sections?: string[]
46
+ replace?: Record<string, string>
47
+ /** Relative order: each section records the section it should follow. */
48
+ order?: Array<{ name: string; after?: string; text: string; custom?: boolean }>
49
+ tools?: { exclude?: string[]; include?: string[] }
50
+ }
package/lib/client.js DELETED
@@ -1,344 +0,0 @@
1
- /**
2
- * dsh-prompt-customizer — browser half.
3
- *
4
- * Registers a "提示词定制" card under the Settings → Plugins list. The card
5
- * fetches the effective section / tool inventory from the host
6
- * `/api/prompt-customizer/inventory` and lets the user block, replace, inject
7
- * prompt sections and hide tools. All writes go through the bound settings
8
- * scope (`prompt-customizer` namespace) and apply live.
9
- *
10
- * Client bundles must register via `window.__ModuleLoader__.load({ id, factory })`
11
- * (see @deepseek-ai/dsh-client-modules): the factory receives `require`, and
12
- * returns `module.exports` carrying `name` / `inject` / `apply`.
13
- */
14
-
15
- window.__ModuleLoader__.load({
16
- id: "dsh-prompt-customizer",
17
- factory: (require) => {
18
- var module = { exports: {} };
19
- var exports = module.exports;
20
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
-
22
- const { createElement: h, useEffect, useState } = require("react");
23
-
24
- const name = "prompt-customizer";
25
- const inject = ["slots", "locale"];
26
-
27
- const NS = "prompt-customizer";
28
- const INVENTORY_URL = "/api/prompt-customizer/inventory";
29
-
30
- function apply(ctx) {
31
- ctx.effect(() => ctx.locale.register(NS, {
32
- zh: {
33
- nav: "提示词定制",
34
- loading: "加载中…",
35
- unavailable: "当前环境未启用本命名空间。",
36
- tabsSections: "提示词",
37
- tabsTools: "工具",
38
- refresh: "刷新",
39
- blockedOff: "未屏蔽",
40
- blockedOn: "已屏蔽",
41
- replaced: "已替换",
42
- replace: "替换",
43
- clearReplace: "清除替换",
44
- injectNew: "注入新段",
45
- name: "名称",
46
- order: "顺序",
47
- text: "文本",
48
- add: "添加",
49
- hiddenOff: "可见",
50
- hiddenOn: "已隐藏",
51
- includeMode: "白名单模式:仅保留勾选项",
52
- excludeMode: "黑名单模式:勾选 = 隐藏",
53
- empty: "(空)",
54
- dynamic: "<动态生成>",
55
- },
56
- en: {
57
- nav: "Prompt Customizer",
58
- loading: "Loading…",
59
- unavailable: "Namespace unavailable in this environment.",
60
- tabsSections: "Sections",
61
- tabsTools: "Tools",
62
- refresh: "Refresh",
63
- blockedOff: "Active",
64
- blockedOn: "Blocked",
65
- replaced: "Replaced",
66
- replace: "Replace",
67
- clearReplace: "Clear",
68
- injectNew: "Inject section",
69
- name: "Name",
70
- order: "Order",
71
- text: "Text",
72
- add: "Add",
73
- hiddenOff: "Visible",
74
- hiddenOn: "Hidden",
75
- includeMode: "Whitelist: keep only checked",
76
- excludeMode: "Blacklist: check to hide",
77
- empty: "(empty)",
78
- dynamic: "<dynamic>",
79
- },
80
- }), "prompt-customizer: locale");
81
- const t = ctx.locale.bind(NS);
82
-
83
- // Register a standalone settings section (sidebar menu entry) AND a
84
- // plugin settings card. Both are registered through a NESTED inject
85
- // of settingsScope so the panel can read/write the namespace config;
86
- // on hosts without the settings transport neither appears instead of
87
- // unmounting the whole plugin.
88
- //
89
- // `settings.section` is what puts "提示词定制" in the settings sidebar
90
- // as its own entry (dsh-market pattern: id + order + label). The
91
- // `settings.plugin.item` card additionally shows under the Plugins tab.
92
- const settingsCtx = ctx;
93
- settingsCtx.inject(["settingsScope"], (scoped) => {
94
- const scope = scoped.settingsScope.bind({ namespace: NS });
95
-
96
- scoped.slots.inject("settings.section", () => scoped.slots.register({
97
- name: "settings.section",
98
- id: "prompt-customizer",
99
- order: 50,
100
- label: () => t("nav"),
101
- locale: NS,
102
- inject: () => ({ t }),
103
- }, () => h(Panel, { scope, t })));
104
-
105
- scoped.slots.inject("settings.plugin.item", () => scoped.slots.register({
106
- name: "settings.plugin.item",
107
- key: NS,
108
- locale: NS,
109
- inject: () => ({ t }),
110
- }, () => h(Panel, { scope, t })));
111
- });
112
- }
113
-
114
- // ── React component ─────────────────────────────────────────────────────
115
-
116
- function Panel({ scope, t }) {
117
- const [snap, setSnap] = useState(() => scope.getSnapshot());
118
- const [inv, setInv] = useState(null);
119
- const [tab, setTab] = useState("sections");
120
- const [error, setError] = useState(null);
121
-
122
- useEffect(() => scope.subscribe(() => setSnap(scope.getSnapshot())), [scope]);
123
-
124
- const refresh = () => {
125
- fetch(INVENTORY_URL)
126
- .then((r) => r.json())
127
- .then((data) => { setInv(data); setError(null); })
128
- .catch((e) => setError(String(e && e.message ? e.message : e)));
129
- };
130
- useEffect(refresh, []);
131
-
132
- if (snap.status !== "ready" && snap.value === undefined) {
133
- return h("div", { style: s.muted }, t("loading"));
134
- }
135
-
136
- const cfg = snap.value && typeof snap.value === "object"
137
- ? snap.value
138
- : { sections: [], replace: {}, inject: [], tools: { exclude: [], include: [] } };
139
-
140
- return h("div", { style: s.root }, [
141
- h("div", { style: s.bar }, [
142
- h("button", { style: tab === "sections" ? s.tabActive : s.tab, onClick: () => setTab("sections") }, t("tabsSections")),
143
- h("button", { style: tab === "tools" ? s.tabActive : s.tab, onClick: () => setTab("tools") }, t("tabsTools")),
144
- h("button", { style: s.refresh, onClick: refresh }, t("refresh")),
145
- ]),
146
- error ? h("div", { style: s.error }, String(error)) : null,
147
- tab === "sections"
148
- ? SectionsTab({ cfg, inv, scope, t })
149
- : ToolsTab({ cfg, inv, scope, t }),
150
- ]);
151
- }
152
-
153
- function SectionsTab({ cfg, inv, scope, t }) {
154
- const sections = inv && inv.sections ? inv.sections : [];
155
- const replace = cfg.replace || {};
156
- const blockedNames = new Set(cfg.sections || []);
157
- // Which section is currently in inline "replace" edit mode.
158
- const [editing, setEditing] = useState(null);
159
- const [draft, setDraft] = useState("");
160
-
161
- const toggleBlock = (name, currentlyBlocked) => {
162
- const list = (cfg.sections || []).slice();
163
- if (currentlyBlocked) { const i = list.indexOf(name); if (i >= 0) list.splice(i, 1); }
164
- else list.push(name);
165
- scope.set("sections", list);
166
- };
167
- const startReplace = (name) => {
168
- setEditing(name);
169
- setDraft(replace[name] || "");
170
- };
171
- const commitReplace = (name) => {
172
- const next = { ...replace };
173
- if (draft.trim() === "") delete next[name];
174
- else next[name] = draft;
175
- scope.set("replace", next);
176
- setEditing(null);
177
- };
178
- const clearReplace = (name) => {
179
- const next = { ...replace };
180
- delete next[name];
181
- scope.set("replace", next);
182
- };
183
-
184
- return h("div", { style: s.list }, [
185
- sections.length === 0 ? h("div", { style: s.muted }, t("empty")) : null,
186
- sections.map((sec) => {
187
- // Blocked state comes from the CONFIG (user's block list), not
188
- // the inventory snapshot, so toggling updates the checkbox
189
- // immediately via the settings scope subscription.
190
- const blocked = blockedNames.has(sec.name);
191
- const isEditing = editing === sec.name;
192
- return h("div", { key: sec.name, style: s.row }, [
193
- h("label", { style: s.switchWrap }, [
194
- h("input", { type: "checkbox", checked: blocked, onChange: () => toggleBlock(sec.name, blocked) }),
195
- h("span", { style: blocked ? s.badgeBlocked : s.badgeOk }, blocked ? t("blockedOn") : t("blockedOff")),
196
- ]),
197
- h("div", { style: s.rowBody }, [
198
- h("div", { style: s.rowTitle }, [
199
- h("span", { style: s.code }, sec.name),
200
- h("span", { style: s.orderTag }, "#" + sec.order),
201
- sec.replaced ? h("span", { style: s.badgeReplaced }, t("replaced")) : null,
202
- ]),
203
- isEditing
204
- ? h("div", { style: s.editBox }, [
205
- h("textarea", { style: s.editInput, value: draft, onChange: (e) => setDraft(e.target.value), rows: 3 }),
206
- h("div", { style: s.injectRow }, [
207
- h("button", { style: s.mini, onClick: () => commitReplace(sec.name) }, t("add")),
208
- h("button", { style: s.mini, onClick: () => setEditing(null) }, t("clearReplace")),
209
- ]),
210
- ])
211
- : h("div", { style: s.preview }, String(sec.text || "").slice(0, 140) || t("dynamic")),
212
- ]),
213
- h("button", { style: s.mini, onClick: () => replace[sec.name] ? clearReplace(sec.name) : startReplace(sec.name) }, replace[sec.name] ? t("clearReplace") : t("replace")),
214
- ]);
215
- }),
216
- h("div", { style: s.injectBox }, [
217
- h("div", { style: s.rowTitle }, t("injectNew")),
218
- h(InjectForm, { inject: cfg.inject || [], scope, t }),
219
- ]),
220
- ]);
221
- }
222
-
223
- function InjectForm({ inject, scope, t }) {
224
- const [name, setName] = useState("");
225
- const [order, setOrder] = useState(120);
226
- const [text, setText] = useState("");
227
-
228
- const submit = () => {
229
- if (!name.trim()) return;
230
- const list = (inject || []).filter((x) => x.name !== name.trim());
231
- list.push({ name: name.trim(), order: Number(order) || 120, text });
232
- scope.set("inject", list);
233
- setName(""); setText(""); setOrder(120);
234
- };
235
-
236
- return h("div", {}, [
237
- h("div", { style: s.injectRow }, [
238
- h("input", { style: { ...s.input, width: "22%" }, placeholder: t("name"), value: name, onChange: (e) => setName(e.target.value) }),
239
- h("input", { style: { ...s.input, width: "12%" }, type: "number", placeholder: t("order"), value: order, onChange: (e) => setOrder(e.target.value) }),
240
- h("input", { style: { ...s.input, flex: 1 }, placeholder: t("text"), value: text, onChange: (e) => setText(e.target.value) }),
241
- h("button", { style: s.mini, onClick: submit }, t("add")),
242
- ]),
243
- inject.length === 0 ? null : h("div", { style: s.muted }, inject.map((x) => `${x.name} (#${x.order})`).join(",")),
244
- ]);
245
- }
246
-
247
- function ToolsTab({ cfg, inv, scope, t }) {
248
- const tools = inv && inv.tools ? inv.tools : [];
249
- const toolsCfg = cfg.tools || { exclude: [], include: [] };
250
- const exclude = toolsCfg.exclude || [];
251
- const include = toolsCfg.include || [];
252
- const includeMode = include.length > 0;
253
-
254
- const toggle = (name, currentlyHidden) => {
255
- if (includeMode) {
256
- const next = include.slice();
257
- if (currentlyHidden) next.push(name);
258
- else { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1); }
259
- scope.set("tools", { ...toolsCfg, include: next });
260
- } else {
261
- const next = exclude.slice();
262
- if (currentlyHidden) { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1); }
263
- else next.push(name);
264
- scope.set("tools", { ...toolsCfg, exclude: next });
265
- }
266
- };
267
- const setIncludeMode = (on) => {
268
- scope.set("tools", on
269
- ? { exclude: exclude.filter((n) => tools.some((t) => t.name === n)), include: tools.filter((t) => !t.hidden).map((t) => t.name) }
270
- : { exclude: ensureOnlyKnown(exclude, tools), include: [] });
271
- };
272
-
273
- return h("div", {}, [
274
- h("div", { style: s.bar }, [
275
- h("button", { style: includeMode ? s.tabActive : s.tab, onClick: () => setIncludeMode(true) }, t("includeMode")),
276
- h("button", { style: includeMode ? s.tab : s.tabActive, onClick: () => setIncludeMode(false) }, t("excludeMode")),
277
- ]),
278
- h("div", { style: s.list }, [
279
- tools.length === 0 ? h("div", { style: s.muted }, t("empty")) : null,
280
- tools.map((tool) => {
281
- // Hidden state comes from the CONFIG (exclude/include lists),
282
- // not the inventory snapshot, so toggling updates the
283
- // checkbox immediately via the settings scope subscription.
284
- const hidden = includeMode
285
- ? !include.includes(tool.name)
286
- : exclude.includes(tool.name);
287
- return h("div", { key: tool.name, style: s.row }, [
288
- h("label", { style: s.switchWrap }, [
289
- h("input", { type: "checkbox", checked: hidden, onChange: () => toggle(tool.name, hidden) }),
290
- h("span", { style: hidden ? s.badgeBlocked : s.badgeOk }, hidden ? t("hiddenOn") : t("hiddenOff")),
291
- ]),
292
- h("div", { style: s.rowBody }, [
293
- h("div", { style: s.rowTitle }, h("span", { style: s.code }, tool.name)),
294
- h("div", { style: s.preview }, String(tool.description || "").slice(0, 140)),
295
- ]),
296
- ]);
297
- }),
298
- ]),
299
- ]);
300
- }
301
-
302
- function ensureOnlyKnown(list, known) {
303
- const names = known.map((t) => t.name);
304
- return list.filter((n) => names.includes(n));
305
- }
306
-
307
- // ── inline styles ───────────────────────────────────────────────────────
308
-
309
- const s = (() => {
310
- const flex = { display: "flex", alignItems: "center" };
311
- return {
312
- root: { fontFamily: "inherit", fontSize: 13, lineHeight: 1.5 },
313
- bar: { ...flex, gap: 6, marginBottom: 8, flexWrap: "wrap" },
314
- tab: { padding: "4px 10px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, background: "transparent", cursor: "pointer", color: "inherit", opacity: .75 },
315
- tabActive: { padding: "4px 10px", border: "1px solid rgba(128,128,128,.7)", borderRadius: 6, background: "rgba(128,128,128,.18)", cursor: "pointer", color: "inherit" },
316
- refresh: { marginLeft: "auto", padding: "4px 10px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, background: "transparent", cursor: "pointer", color: "inherit" },
317
- list: { display: "flex", flexDirection: "column", gap: 6 },
318
- row: { ...flex, gap: 8, padding: "6px 8px", border: "1px solid rgba(128,128,128,.2)", borderRadius: 8, background: "rgba(128,128,128,.06)" },
319
- switchWrap: { ...flex, gap: 6, minWidth: 90 },
320
- rowBody: { flex: 1, minWidth: 0 },
321
- rowTitle: { ...flex, gap: 6, alignItems: "baseline", flexWrap: "wrap" },
322
- code: { fontFamily: "monospace", fontSize: 12, wordBreak: "break-all" },
323
- orderTag: { color: "rgba(128,128,128,.8)", fontSize: 11 },
324
- preview: { color: "rgba(128,128,128,.85)", fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
325
- badgeOk: { color: "#3fb950", fontSize: 11, border: "1px solid rgba(63,185,80,.5)", borderRadius: 999, padding: "0 6px" },
326
- badgeBlocked: { color: "#f85149", fontSize: 11, border: "1px solid rgba(248,81,73,.5)", borderRadius: 999, padding: "0 6px" },
327
- badgeReplaced: { color: "#d29922", fontSize: 11, border: "1px solid rgba(210,153,34,.5)", borderRadius: 999, padding: "0 6px" },
328
- mini: { padding: "3px 8px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, background: "transparent", cursor: "pointer", color: "inherit", whiteSpace: "nowrap" },
329
- injectBox: { marginTop: 10, padding: "8px", border: "1px dashed rgba(128,128,128,.4)", borderRadius: 8, display: "flex", flexDirection: "column", gap: 6 },
330
- injectRow: { ...flex, gap: 6 },
331
- editBox: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 },
332
- editInput: { padding: "6px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, background: "transparent", color: "inherit", fontFamily: "inherit", fontSize: 12, resize: "vertical", width: "100%", boxSizing: "border-box" },
333
- input: { padding: "4px 6px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, background: "transparent", color: "inherit" },
334
- muted: { color: "rgba(128,128,128,.75)", fontSize: 12 },
335
- error: { color: "#f85149", fontSize: 12, marginBottom: 6 },
336
- };
337
- })();
338
-
339
- exports.name = name;
340
- exports.inject = inject;
341
- exports.apply = apply;
342
- return module.exports;
343
- },
344
- });