dsh-prompt-customizer 0.2.5 → 0.2.7

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/lib/schema.js CHANGED
@@ -27,6 +27,24 @@ export const Config = Schema.object({
27
27
  exclude: Schema.array(Schema.string()).default([]),
28
28
  include: Schema.array(Schema.string()).default([]),
29
29
  }).default({}),
30
+ // Per-agent customizations: byAgent.<agentPresetId> OVERRIDES the default
31
+ // sections / replace / inject / tools for that specific agent preset. The
32
+ // top-level fields act as the shared default applied to every other agent.
33
+ // Each entry is a partial overlay; omitted fields inherit the default.
34
+ byAgent: Schema.dict(Schema.object({
35
+ sections: Schema.array(Schema.string()),
36
+ replace: Schema.dict(Schema.string()),
37
+ inject: Schema.array(Schema.object({
38
+ name: Schema.string().required(),
39
+ order: Schema.number().default(120),
40
+ text: Schema.string().required(),
41
+ custom: Schema.boolean().default(false),
42
+ })),
43
+ tools: Schema.object({
44
+ exclude: Schema.array(Schema.string()),
45
+ include: Schema.array(Schema.string()),
46
+ }),
47
+ })).default({}),
30
48
  // Presets: complete customization snapshots (full prompt + tool config).
31
49
  presets: Schema.array(Schema.object({
32
50
  id: Schema.string().required(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-prompt-customizer",
3
3
  "description": "System-prompt & tool customizer for DeepSeek Harness with a settings-panel UI: block / replace / inject prompt sections by name, and hide tools from the model catalog by exclude/include. Config lives in the dsh settings document (namespace prompt-customizer).",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "main": "lib/index.js",
@@ -0,0 +1,66 @@
1
+ /** Preview tab: two read-only sub-tabs — the FINAL assembled system prompt
2
+ * after every plugin's filter, and the tools the current preset will inject
3
+ * (whitelist-style, after include/exclude). Neither view edits config. */
4
+ import { createElement as h, useEffect, useRef, useState, type ReactElement } from 'react'
5
+ import type { Preview, SettingsScope } from './types.ts'
6
+ import type { Translate } from './locales.ts'
7
+ import { s } from './styles.ts'
8
+ import { PreviewTools } from './PreviewTools.tsx'
9
+
10
+ const PREVIEW_URL = '/api/prompt-customizer/preview'
11
+
12
+ export function PreviewTab({ scope, t }: { scope: SettingsScope; t: Translate }): ReactElement {
13
+ const [sub, setSub] = useState<'prompt' | 'tools'>('prompt')
14
+ const [data, setData] = useState<Preview | null>(null)
15
+ const [error, setError] = useState<string | null>(null)
16
+
17
+ // Reload whenever the namespace config changes (block/replace/inject), so the
18
+ // section count and final text always reflect the current customization —
19
+ // not just on mount. A config change that lands while a fetch is in flight is
20
+ // never dropped: it schedules one trailing reload, so the preview converges
21
+ // to the latest config even when the in-flight response was served before the
22
+ // write committed on the host.
23
+ const inFlight = useRef(false)
24
+ const pending = useRef(false)
25
+ const load = (): void => {
26
+ if (inFlight.current) { pending.current = true; return }
27
+ inFlight.current = true
28
+ setData(null)
29
+ fetch(PREVIEW_URL, { cache: 'no-store' })
30
+ .then((r) => r.json())
31
+ .then((body: Preview) => {
32
+ if (body && body.ok === false) throw new Error((body as { error?: string }).error ?? 'preview failed')
33
+ setData(body)
34
+ setError(null)
35
+ })
36
+ .catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
37
+ .finally(() => {
38
+ inFlight.current = false
39
+ if (pending.current) { pending.current = false; load() }
40
+ })
41
+ }
42
+ useEffect(load, [])
43
+ useEffect(() => scope.subscribe(load), [scope])
44
+
45
+ return h('div', { style: s.list }, [
46
+ h('div', { style: s.bar }, [
47
+ h('button', { style: sub === 'prompt' ? s.tabActive : s.tab, onClick: () => setSub('prompt') }, t('previewPrompt')),
48
+ h('button', { style: sub === 'tools' ? s.tabActive : s.tab, onClick: () => setSub('tools') }, t('previewTools')),
49
+ ]),
50
+ error ? h('div', { style: s.error }, String(error)) : null,
51
+ data === null && !error ? h('div', { style: s.muted }, t('loading')) : null,
52
+ sub === 'prompt'
53
+ ? data
54
+ ? [
55
+ h('div', { style: s.rowTitle }, [
56
+ h('span', { style: s.muted }, t('previewHint')),
57
+ h('span', { style: s.orderTag }, `${data.sections.length} ${t('previewSections')}`),
58
+ ]),
59
+ h('pre', { style: s.previewText }, data.text || t('empty')),
60
+ ]
61
+ : null
62
+ : data
63
+ ? h(PreviewTools, { tools: data.tools, t })
64
+ : null,
65
+ ])
66
+ }
@@ -0,0 +1,31 @@
1
+ /** Tools preview: read-only whitelist-style list of the tools the current
2
+ * preset will inject (the FINAL model-visible catalog after include/exclude).
3
+ * A tool being listed means it is visible — there is no "blocked" state here.
4
+ * This view never edits the blacklist/whitelist config. */
5
+ import { createElement as h, type ReactElement } from 'react'
6
+ import type { Translate } from './locales.ts'
7
+ import { s } from './styles.ts'
8
+
9
+ export type PreviewTool = { name: string; description: string } | string
10
+
11
+ function norm(tool: PreviewTool): { name: string; description: string } {
12
+ return typeof tool === 'string' ? { name: tool, description: '' } : tool
13
+ }
14
+
15
+ export function PreviewTools({ tools, t }: { tools: PreviewTool[]; t: Translate }): ReactElement {
16
+ if (tools.length === 0) return h('div', { style: s.muted }, t('empty'))
17
+ return h('div', { style: s.list }, [
18
+ h('div', { style: s.rowTitle }, [
19
+ h('span', { style: s.orderTag }, `${tools.length} ${t('previewToolCount')}`),
20
+ ]),
21
+ tools.map((tool) => {
22
+ const { name, description } = norm(tool)
23
+ return h('div', { key: name, style: s.row }, [
24
+ h('div', { style: s.rowBody }, [
25
+ h('div', { style: s.rowTitle }, h('span', { style: s.code }, name)),
26
+ h('div', { style: s.preview }, String(description ?? '').slice(0, 140)),
27
+ ]),
28
+ ])
29
+ }),
30
+ ])
31
+ }
@@ -1,37 +1,41 @@
1
- /** Tools tab: hide tools from the model catalog by exclude/include. */
1
+ /** Tools tab: hide tools from the model catalog (blacklist only — check = hide). */
2
2
  import { createElement as h, type ReactElement } from 'react'
3
3
  import type { Config, Inventory, SettingsScope } from './types.ts'
4
4
  import type { Translate } from './locales.ts'
5
- import { isToolHidden, setToolMode, toggleTool } from './presets.ts'
6
5
  import { s } from './styles.ts'
7
6
 
8
7
  export function ToolsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
9
8
  const tools = inv?.tools ?? []
10
- const toolsCfg = cfg.tools ?? { exclude: [], include: [] }
11
- const includeMode = (toolsCfg.include ?? []).length > 0
9
+ const exclude = cfg.tools?.exclude ?? []
10
+
11
+ // Blacklist-only: a tool is hidden iff it is in the exclude list. Any stale
12
+ // whitelist (include) is discarded on write so mode switching is gone.
13
+ const hidden = (name: string): boolean => exclude.includes(name)
12
14
 
13
15
  const toggle = (name: string, currentlyHidden: boolean): void => {
14
- scope.set('tools', toggleTool(name, currentlyHidden, toolsCfg))
16
+ const next = exclude.slice()
17
+ if (currentlyHidden) { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1) }
18
+ else next.push(name)
19
+ scope.set('tools', { exclude: next, include: [] })
15
20
  }
16
- const setIncludeMode = (on: boolean): void => {
17
- scope.set('tools', setToolMode(on, toolsCfg, tools.map((tool) => tool.name)))
21
+
22
+ const setAll = (hideAll: boolean): void => {
23
+ scope.set('tools', { exclude: hideAll ? tools.map((tool) => tool.name) : [], include: [] })
18
24
  }
19
25
 
20
26
  return h('div', {}, [
21
27
  h('div', { style: s.bar }, [
22
- h('button', { style: includeMode ? s.tabActive : s.tab, onClick: () => setIncludeMode(true) }, t('includeMode')),
23
- h('button', { style: includeMode ? s.tab : s.tabActive, onClick: () => setIncludeMode(false) }, t('excludeMode')),
28
+ h('button', { style: s.mini, onClick: () => setAll(true) }, t('selectAll')),
29
+ h('button', { style: s.mini, onClick: () => setAll(false) }, t('selectNone')),
24
30
  ]),
25
31
  h('div', { style: s.list }, [
26
32
  tools.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
27
33
  tools.map((tool) => {
28
- // Hidden state comes from the CONFIG (exclude/include lists), not the
29
- // inventory snapshot, so toggling updates the checkbox immediately.
30
- const hidden = isToolHidden(tool.name, toolsCfg)
34
+ const isHidden = hidden(tool.name)
31
35
  return h('div', { key: tool.name, style: s.row }, [
32
36
  h('label', { style: s.switchWrap }, [
33
- h('input', { type: 'checkbox', checked: hidden, onChange: () => toggle(tool.name, hidden) }),
34
- h('span', { style: hidden ? s.badgeBlocked : s.badgeOk }, hidden ? t('hiddenOn') : t('hiddenOff')),
37
+ h('input', { type: 'checkbox', checked: isHidden, onChange: () => toggle(tool.name, isHidden) }),
38
+ h('span', { style: isHidden ? s.badgeBlocked : s.badgeOk }, isHidden ? t('hiddenOn') : t('hiddenOff')),
35
39
  ]),
36
40
  h('div', { style: s.rowBody }, [
37
41
  h('div', { style: s.rowTitle }, h('span', { style: s.code }, tool.name)),
@@ -17,6 +17,7 @@ import type { Config, Inventory, SettingsScope } from './types.ts'
17
17
  import { SectionsTab } from './SectionsTab.tsx'
18
18
  import { ToolsTab } from './ToolsTab.tsx'
19
19
  import { PresetsTab } from './PresetsTab.tsx'
20
+ import { PreviewTab } from './PreviewTab.tsx'
20
21
 
21
22
  const NS = 'prompt-customizer'
22
23
  const INVENTORY_URL = '/api/prompt-customizer/inventory'
@@ -73,7 +74,7 @@ export function apply(ctx: ClientContext): void {
73
74
  function Panel({ scope, t }: { scope: SettingsScope; t: Translate }): ReactElement {
74
75
  const [snap, setSnap] = useState(() => scope.getSnapshot())
75
76
  const [inv, setInv] = useState<Inventory | null>(null)
76
- const [tab, setTab] = useState<'sections' | 'tools' | 'presets'>('sections')
77
+ const [tab, setTab] = useState<'sections' | 'tools' | 'presets' | 'preview'>('sections')
77
78
  const [error, setError] = useState<string | null>(null)
78
79
 
79
80
  useEffect(() => scope.subscribe(() => setSnap(scope.getSnapshot())), [scope])
@@ -99,6 +100,7 @@ function Panel({ scope, t }: { scope: SettingsScope; t: Translate }): ReactEleme
99
100
  h('button', { style: tab === 'sections' ? s.tabActive : s.tab, onClick: () => setTab('sections') }, t('tabsSections')),
100
101
  h('button', { style: tab === 'tools' ? s.tabActive : s.tab, onClick: () => setTab('tools') }, t('tabsTools')),
101
102
  h('button', { style: tab === 'presets' ? s.tabActive : s.tab, onClick: () => setTab('presets') }, t('tabsPresets')),
103
+ h('button', { style: tab === 'preview' ? s.tabActive : s.tab, onClick: () => setTab('preview') }, t('tabsPreview')),
102
104
  h('button', { style: s.refresh, onClick: refresh }, t('refresh')),
103
105
  ]),
104
106
  error ? h('div', { style: s.error }, String(error)) : null,
@@ -106,6 +108,8 @@ function Panel({ scope, t }: { scope: SettingsScope; t: Translate }): ReactEleme
106
108
  ? h(SectionsTab, { cfg, inv, scope, t })
107
109
  : tab === 'tools'
108
110
  ? h(ToolsTab, { cfg, inv, scope, t })
109
- : h(PresetsTab, { cfg, inv, scope, t }),
111
+ : tab === 'presets'
112
+ ? h(PresetsTab, { cfg, inv, scope, t })
113
+ : h(PreviewTab, { scope, t }),
110
114
  ])
111
115
  }
@@ -7,6 +7,7 @@ export const DICT = {
7
7
  tabsSections: '提示词',
8
8
  tabsTools: '工具',
9
9
  tabsPresets: '预设',
10
+ tabsPreview: '预览',
10
11
  refresh: '刷新',
11
12
  blockedOff: '未屏蔽',
12
13
  blockedOn: '已屏蔽',
@@ -22,13 +23,18 @@ export const DICT = {
22
23
  add: '添加',
23
24
  hiddenOff: '可见',
24
25
  hiddenOn: '已隐藏',
25
- includeMode: '白名单模式:仅保留勾选项',
26
- excludeMode: '黑名单模式:勾选 = 隐藏',
26
+ selectAll: '全选',
27
+ selectNone: '全不选',
27
28
  empty: '(空)',
28
29
  dynamic: '<动态生成>',
29
30
  drag: '拖动排序',
30
31
  moveUp: '上移',
31
32
  moveDown: '下移',
33
+ previewHint: '以下为屏蔽/替换/注入后的最终系统提示词:',
34
+ previewPrompt: '提示词预览',
35
+ previewSections: '段',
36
+ previewTools: '工具预览',
37
+ previewToolCount: '个工具',
32
38
  savePreset: '保存当前为预设',
33
39
  presetName: '预设名称',
34
40
  save: '保存',
@@ -47,6 +53,7 @@ export const DICT = {
47
53
  tabsSections: 'Sections',
48
54
  tabsTools: 'Tools',
49
55
  tabsPresets: 'Presets',
56
+ tabsPreview: 'Preview',
50
57
  refresh: 'Refresh',
51
58
  blockedOff: 'Active',
52
59
  blockedOn: 'Blocked',
@@ -62,13 +69,18 @@ export const DICT = {
62
69
  add: 'Add',
63
70
  hiddenOff: 'Visible',
64
71
  hiddenOn: 'Hidden',
65
- includeMode: 'Whitelist: keep only checked',
66
- excludeMode: 'Blacklist: check to hide',
72
+ selectAll: 'Select all',
73
+ selectNone: 'Select none',
67
74
  empty: '(empty)',
68
75
  dynamic: '<dynamic>',
69
76
  drag: 'Drag to reorder',
70
77
  moveUp: 'Move up',
71
78
  moveDown: 'Move down',
79
+ previewHint: 'Final system prompt after block/replace/inject:',
80
+ previewPrompt: 'Prompt preview',
81
+ previewSections: 'sections',
82
+ previewTools: 'Tools preview',
83
+ previewToolCount: 'tools',
72
84
  savePreset: 'Save current as preset',
73
85
  presetName: 'Preset name',
74
86
  save: 'Save',
@@ -15,6 +15,9 @@ export const s = (() => {
15
15
  code: { fontFamily: 'monospace', fontSize: 12, wordBreak: 'break-all' },
16
16
  orderTag: { color: 'rgba(128,128,128,.8)', fontSize: 11 },
17
17
  preview: { color: 'rgba(128,128,128,.85)', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
18
+ previewText: { fontFamily: 'monospace', fontSize: 12, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 480, overflowY: 'auto', padding: 10, border: '1px solid rgba(128,128,128,.2)', borderRadius: 8, background: 'rgba(0,0,0,.18)', margin: 0 },
19
+ toolWrap: { display: 'flex', flexWrap: 'wrap', gap: 6 },
20
+ toolChip: { fontFamily: 'monospace', fontSize: 12, color: 'rgba(128,128,128,.95)', border: '1px solid rgba(128,128,128,.3)', borderRadius: 6, padding: '2px 6px', cursor: 'help' },
18
21
  badgeOk: { color: '#3fb950', fontSize: 11, border: '1px solid rgba(63,185,80,.5)', borderRadius: 999, padding: '0 6px' },
19
22
  badgeBlocked: { color: '#f85149', fontSize: 11, border: '1px solid rgba(248,81,73,.5)', borderRadius: 999, padding: '0 6px' },
20
23
  badgeReplaced: { color: '#d29922', fontSize: 11, border: '1px solid rgba(210,153,34,.5)', borderRadius: 999, padding: '0 6px' },
@@ -14,16 +14,50 @@ export interface Inventory {
14
14
  tools: Array<{ name: string; description?: string; hidden: boolean }>
15
15
  }
16
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
+
17
26
  /** The resolved config object (namespace value). */
18
27
  export interface Config {
19
28
  sections?: string[]
20
29
  replace?: Record<string, string>
21
30
  inject?: Array<{ name: string; order: number; text: string; custom?: boolean }>
22
31
  tools?: { exclude?: string[]; include?: string[] }
32
+ /** Per-agent overlays: byAgent.<agentId> overrides the default fields for that agent. */
33
+ byAgent?: Record<string, AgentOverlay>
23
34
  presets?: Preset[]
24
35
  activePreset?: string
25
36
  }
26
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
+
27
61
  /** A complete customization snapshot that can be saved / applied / exported. */
28
62
  export interface Preset {
29
63
  id: string