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/client/client.js +150 -78
- package/client/client.js.map +1 -1
- package/lib/index.js +197 -38
- package/lib/schema.js +18 -0
- package/package.json +1 -1
- package/src/client/PreviewTab.tsx +66 -0
- package/src/client/PreviewTools.tsx +31 -0
- package/src/client/ToolsTab.tsx +18 -14
- package/src/client/index.ts +6 -2
- package/src/client/locales.ts +16 -4
- package/src/client/styles.ts +3 -0
- package/src/client/types.ts +34 -0
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.
|
|
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
|
+
}
|
package/src/client/ToolsTab.tsx
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
/** Tools tab: hide tools from the model catalog
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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:
|
|
23
|
-
h('button', { style:
|
|
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
|
-
|
|
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:
|
|
34
|
-
h('span', { style:
|
|
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)),
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
:
|
|
111
|
+
: tab === 'presets'
|
|
112
|
+
? h(PresetsTab, { cfg, inv, scope, t })
|
|
113
|
+
: h(PreviewTab, { scope, t }),
|
|
110
114
|
])
|
|
111
115
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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',
|
package/src/client/styles.ts
CHANGED
|
@@ -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' },
|
package/src/client/types.ts
CHANGED
|
@@ -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
|