dsh-prompt-customizer 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["useState","h","h","useState","useRef","h","h","useState"],"sources":["../src/client/locales.ts","../src/client/styles.ts","../src/client/SectionsTab.tsx","../src/client/ToolsTab.tsx","../src/client/PresetsTab.tsx","../src/client/index.ts"],"sourcesContent":["/** Locale dictionaries for the prompt-customizer panel. */\nexport const DICT = {\n zh: {\n nav: '提示词定制',\n loading: '加载中…',\n unavailable: '当前环境未启用本命名空间。',\n tabsSections: '提示词',\n tabsTools: '工具',\n tabsPresets: '预设',\n refresh: '刷新',\n blockedOff: '未屏蔽',\n blockedOn: '已屏蔽',\n replaced: '已替换',\n replace: '替换',\n clearReplace: '清除替换',\n injectNew: '注入新段',\n name: '名称',\n order: '顺序',\n text: '文本',\n add: '添加',\n hiddenOff: '可见',\n hiddenOn: '已隐藏',\n includeMode: '白名单模式:仅保留勾选项',\n excludeMode: '黑名单模式:勾选 = 隐藏',\n empty: '(空)',\n dynamic: '<动态生成>',\n drag: '拖动排序',\n moveUp: '上移',\n moveDown: '下移',\n savePreset: '保存当前为预设',\n presetName: '预设名称',\n save: '保存',\n importPreset: '导入预设',\n import: '导入',\n apply: '应用',\n export: '导出',\n delete: '删除',\n active: '使用中',\n preset: '预设',\n },\n en: {\n nav: 'Prompt Customizer',\n loading: 'Loading…',\n unavailable: 'Namespace unavailable in this environment.',\n tabsSections: 'Sections',\n tabsTools: 'Tools',\n tabsPresets: 'Presets',\n refresh: 'Refresh',\n blockedOff: 'Active',\n blockedOn: 'Blocked',\n replaced: 'Replaced',\n replace: 'Replace',\n clearReplace: 'Clear',\n injectNew: 'Inject section',\n name: 'Name',\n order: 'Order',\n text: 'Text',\n add: 'Add',\n hiddenOff: 'Visible',\n hiddenOn: 'Hidden',\n includeMode: 'Whitelist: keep only checked',\n excludeMode: 'Blacklist: check to hide',\n empty: '(empty)',\n dynamic: '<dynamic>',\n drag: 'Drag to reorder',\n moveUp: 'Move up',\n moveDown: 'Move down',\n savePreset: 'Save current as preset',\n presetName: 'Preset name',\n save: 'Save',\n importPreset: 'Import preset',\n import: 'Import',\n apply: 'Apply',\n export: 'Export',\n delete: 'Delete',\n active: 'Active',\n preset: 'Preset',\n },\n} as const\n\nexport type Translate = (key: string) => string\n","/** Inline styles shared by the prompt-customizer panel components. */\nexport const s = (() => {\n const flex = { display: 'flex', alignItems: 'center' }\n return {\n root: { fontFamily: 'inherit', fontSize: 13, lineHeight: 1.5 },\n bar: { ...flex, gap: 6, marginBottom: 8, flexWrap: 'wrap' },\n tab: { padding: '4px 10px', border: '1px solid rgba(128,128,128,.35)', borderRadius: 6, background: 'transparent', cursor: 'pointer', color: 'inherit', opacity: 0.75 },\n tabActive: { padding: '4px 10px', border: '1px solid rgba(128,128,128,.7)', borderRadius: 6, background: 'rgba(128,128,128,.18)', cursor: 'pointer', color: 'inherit' },\n refresh: { marginLeft: 'auto', padding: '4px 10px', border: '1px solid rgba(128,128,128,.35)', borderRadius: 6, background: 'transparent', cursor: 'pointer', color: 'inherit' },\n list: { display: 'flex', flexDirection: 'column', gap: 6 },\n row: { ...flex, gap: 8, padding: '6px 8px', border: '1px solid rgba(128,128,128,.2)', borderRadius: 8, background: 'rgba(128,128,128,.06)' },\n switchWrap: { ...flex, gap: 6, minWidth: 90 },\n rowBody: { flex: 1, minWidth: 0 },\n rowTitle: { ...flex, gap: 6, alignItems: 'baseline', flexWrap: 'wrap' },\n code: { fontFamily: 'monospace', fontSize: 12, wordBreak: 'break-all' },\n orderTag: { color: 'rgba(128,128,128,.8)', fontSize: 11 },\n preview: { color: 'rgba(128,128,128,.85)', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },\n badgeOk: { color: '#3fb950', fontSize: 11, border: '1px solid rgba(63,185,80,.5)', borderRadius: 999, padding: '0 6px' },\n badgeBlocked: { color: '#f85149', fontSize: 11, border: '1px solid rgba(248,81,73,.5)', borderRadius: 999, padding: '0 6px' },\n badgeReplaced: { color: '#d29922', fontSize: 11, border: '1px solid rgba(210,153,34,.5)', borderRadius: 999, padding: '0 6px' },\n mini: { padding: '3px 8px', border: '1px solid rgba(128,128,128,.35)', borderRadius: 6, background: 'transparent', cursor: 'pointer', color: 'inherit', whiteSpace: 'nowrap' },\n arrowCol: { display: 'flex', flexDirection: 'column', gap: 2 },\n arrow: { padding: '0 6px', border: '1px solid rgba(128,128,128,.3)', borderRadius: 4, background: 'transparent', cursor: 'pointer', color: 'inherit', fontSize: 11, lineHeight: 1.4, opacity: 0.8 },\n rowDragging: { opacity: 0.4 },\n rowDropAbove: { boxShadow: '0 -2px 0 0 #58a6ff' },\n rowDropBelow: { boxShadow: '0 2px 0 0 #58a6ff' },\n dragHandle: { cursor: 'grab', color: 'rgba(128,128,128,.7)', fontSize: 13, padding: '0 2px', userSelect: 'none' },\n injectBox: { marginTop: 10, padding: '8px', border: '1px dashed rgba(128,128,128,.4)', borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 6 },\n injectRow: { ...flex, gap: 6 },\n editBox: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 },\n 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' },\n input: { padding: '4px 6px', border: '1px solid rgba(128,128,128,.35)', borderRadius: 6, background: 'transparent', color: 'inherit' },\n muted: { color: 'rgba(128,128,128,.75)', fontSize: 12 },\n error: { color: '#f85149', fontSize: 12, marginBottom: 6 },\n }\n})()\n","/** Prompt-sections tab: block / replace / inject / reorder prompt sections. */\nimport { createElement as h, useState, type ReactElement, type DragEvent, type ChangeEvent } from 'react'\nimport type { Config, Inventory, SettingsScope } from './types.ts'\nimport type { Translate } from './locales.ts'\nimport { s } from './styles.ts'\n\ninterface Section {\n name: string\n order: number\n text?: string\n active: boolean\n replaced: boolean\n}\n\nexport function SectionsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {\n const replace = cfg.replace ?? {}\n const blockedNames = new Set(cfg.sections ?? [])\n const [editing, setEditing] = useState<string | null>(null)\n const [draft, setDraft] = useState('')\n const [dragName, setDragName] = useState<string | null>(null)\n const [dropTarget, setDropTarget] = useState<{ name: string; pos: 'above' | 'below' } | null>(null)\n\n // Merge inventory sections with user-injected sections, sorted by order.\n // Injected sections that don't exist in the original prompt appear here too,\n // positioned by their order value.\n const merged: Section[] = (() => {\n const map = new Map<string, Section>()\n for (const sec of inv?.sections ?? []) map.set(sec.name, sec)\n for (const item of cfg.inject ?? []) {\n const existing = map.get(item.name)\n if (existing) map.set(item.name, { ...existing, order: item.order })\n else map.set(item.name, {\n name: item.name,\n order: item.order,\n text: item.text || '<动态生成>',\n active: !blockedNames.has(item.name),\n replaced: false,\n })\n }\n return [...map.values()].sort((a, b) => a.order - b.order)\n })()\n\n const toggleBlock = (name: string, currentlyBlocked: boolean): void => {\n const list = (cfg.sections ?? []).slice()\n if (currentlyBlocked) { const i = list.indexOf(name); if (i >= 0) list.splice(i, 1) }\n else list.push(name)\n scope.set('sections', list)\n }\n const startReplace = (name: string, original: string): void => {\n // Echo the ORIGINAL prompt text so the user edits from the current value.\n setEditing(name)\n setDraft(replace[name] ?? original)\n }\n const commitReplace = (name: string): void => {\n const next = { ...replace }\n if (draft.trim() === '') delete next[name]\n else next[name] = draft\n scope.set('replace', next)\n setEditing(null)\n }\n const clearReplace = (name: string): void => {\n const next = { ...replace }\n delete next[name]\n scope.set('replace', next)\n }\n\n // Persist the full ordered list. We treat the sorted list as an array and\n // re-index every section with a sequential integer order (0, 1, 2, …), so\n // there are never duplicate or fractional orders. text is left empty for\n // original sections (keeps their text); injected sections keep their own.\n const persistOrder = (ordered: Section[]): void => {\n const list = ordered.map((sec, i) => {\n const existing = (cfg.inject ?? []).find((x) => x.name === sec.name)\n return { name: sec.name, order: i, text: existing ? existing.text : '' }\n })\n scope.set('inject', list)\n }\n\n // Move a section up/down one slot (array-index swap), then re-index.\n const moveOrder = (index: number, dir: -1 | 1): void => {\n const target = index + dir\n if (target < 0 || target >= merged.length) return\n const next = merged.slice()\n const [item] = next.splice(index, 1)\n next.splice(target, 0, item)\n persistOrder(next)\n }\n\n const onDragStart = (e: DragEvent, name: string): void => {\n setDragName(name)\n e.dataTransfer.effectAllowed = 'move'\n try { e.dataTransfer.setData('text/plain', name) } catch { /* ignore */ }\n }\n const onDragOver = (e: DragEvent, name: string): void => {\n if (!dragName || dragName === name) return\n e.preventDefault()\n e.dataTransfer.dropEffect = 'move'\n const rect = e.currentTarget.getBoundingClientRect()\n const pos = e.clientY < rect.top + rect.height / 2 ? 'above' : 'below'\n setDropTarget({ name, pos })\n }\n const onDrop = (e: DragEvent, target: Section): void => {\n e.preventDefault()\n if (!dragName || dragName === target.name) { setDragName(null); setDropTarget(null); return }\n const dragged = merged.find((x) => x.name === dragName)\n if (!dragged) { setDragName(null); setDropTarget(null); return }\n const next = merged.filter((x) => x.name !== dragName)\n const idx = next.findIndex((x) => x.name === target.name)\n const pos = dropTarget?.name === target.name ? dropTarget.pos : 'below'\n next.splice(pos === 'above' ? idx : idx + 1, 0, dragged)\n persistOrder(next)\n setDragName(null)\n setDropTarget(null)\n }\n const onDragEnd = (): void => { setDragName(null); setDropTarget(null) }\n\n // Add a new injected section, placed by its order hint, then re-index.\n const addSection = (name: string, order: number, text: string): void => {\n const next = merged.slice()\n const entry: Section = { name, order, text: text || '<动态生成>', active: true, replaced: false }\n const existing = next.findIndex((x) => x.name === name)\n if (existing >= 0) next[existing] = entry\n else {\n const idx = next.findIndex((x) => x.order > order)\n if (idx < 0) next.push(entry)\n else next.splice(idx, 0, entry)\n }\n persistOrder(next)\n }\n\n return h('div', { style: s.list }, [\n merged.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,\n merged.map((sec, index) => {\n const blocked = blockedNames.has(sec.name)\n const isEditing = editing === sec.name\n const isDragging = dragName === sec.name\n const isDropTarget = dropTarget?.name === sec.name\n const rowStyle = {\n ...s.row,\n ...(isDragging ? s.rowDragging : {}),\n ...(isDropTarget && dropTarget.pos === 'above' ? s.rowDropAbove : {}),\n ...(isDropTarget && dropTarget.pos === 'below' ? s.rowDropBelow : {}),\n }\n return h('div', {\n key: sec.name,\n style: rowStyle,\n draggable: true,\n onDragStart: (e: DragEvent) => onDragStart(e, sec.name),\n onDragOver: (e: DragEvent) => onDragOver(e, sec.name),\n onDrop: (e: DragEvent) => onDrop(e, sec),\n onDragEnd,\n }, [\n h('span', { style: s.dragHandle, title: t('drag') }, '⠿'),\n h('label', { style: s.switchWrap }, [\n h('input', { type: 'checkbox', checked: blocked, onChange: () => toggleBlock(sec.name, blocked) }),\n h('span', { style: blocked ? s.badgeBlocked : s.badgeOk }, blocked ? t('blockedOn') : t('blockedOff')),\n ]),\n h('div', { style: s.rowBody }, [\n h('div', { style: s.rowTitle }, [\n h('span', { style: s.code }, sec.name),\n h('span', { style: s.orderTag }, '#' + index),\n sec.replaced ? h('span', { style: s.badgeReplaced }, t('replaced')) : null,\n ]),\n isEditing\n ? h('div', { style: s.editBox }, [\n h('textarea', { style: s.editInput, value: draft, onChange: (e: ChangeEvent<HTMLTextAreaElement>) => setDraft(e.target.value), rows: 3 }),\n h('div', { style: s.injectRow }, [\n h('button', { style: s.mini, onClick: () => commitReplace(sec.name) }, t('add')),\n h('button', { style: s.mini, onClick: () => setEditing(null) }, t('clearReplace')),\n ]),\n ])\n : h('div', { style: s.preview }, String(sec.text ?? '').slice(0, 140) || t('dynamic')),\n ]),\n h('div', { style: s.arrowCol }, [\n h('button', { style: s.arrow, disabled: index === 0, onClick: () => moveOrder(index, -1), title: t('moveUp') }, '↑'),\n h('button', { style: s.arrow, disabled: index === merged.length - 1, onClick: () => moveOrder(index, 1), title: t('moveDown') }, '↓'),\n ]),\n h('button', { style: s.mini, onClick: () => replace[sec.name] ? clearReplace(sec.name) : startReplace(sec.name, String(sec.text ?? '')) }, replace[sec.name] ? t('clearReplace') : t('replace')),\n ])\n }),\n h('div', { style: s.injectBox }, [\n h('div', { style: s.rowTitle }, t('injectNew')),\n h(InjectForm, { onAdd: addSection, t }),\n ]),\n ])\n}\n\nfunction InjectForm({ onAdd, t }: { onAdd: (name: string, order: number, text: string) => void; t: Translate }): ReactElement {\n const [name, setName] = useState('')\n const [order, setOrder] = useState('120')\n const [text, setText] = useState('')\n\n const submit = (): void => {\n if (!name.trim()) return\n onAdd(name.trim(), Number(order) || 120, text)\n setName(''); setText(''); setOrder('120')\n }\n\n return h('div', {}, [\n h('div', { style: s.injectRow }, [\n h('input', { style: { ...s.input, width: '22%' }, placeholder: t('name'), value: name, onChange: (e) => setName(e.target.value) }),\n h('input', { style: { ...s.input, width: '12%' }, type: 'number', placeholder: t('order'), value: order, onChange: (e) => setOrder(e.target.value) }),\n h('input', { style: { ...s.input, flex: 1 }, placeholder: t('text'), value: text, onChange: (e) => setText(e.target.value) }),\n h('button', { style: s.mini, onClick: submit }, t('add')),\n ]),\n ])\n}\n","/** Tools tab: hide tools from the model catalog by exclude/include. */\nimport { createElement as h, type ReactElement } from 'react'\nimport type { Config, Inventory, SettingsScope } from './types.ts'\nimport type { Translate } from './locales.ts'\nimport { s } from './styles.ts'\n\nexport function ToolsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {\n const tools = inv?.tools ?? []\n const toolsCfg = cfg.tools ?? { exclude: [], include: [] }\n const exclude = toolsCfg.exclude ?? []\n const include = toolsCfg.include ?? []\n const includeMode = include.length > 0\n\n const toggle = (name: string, currentlyHidden: boolean): void => {\n if (includeMode) {\n const next = include.slice()\n if (currentlyHidden) next.push(name)\n else { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1) }\n scope.set('tools', { ...toolsCfg, include: next })\n } else {\n const next = exclude.slice()\n if (currentlyHidden) { const i = next.indexOf(name); if (i >= 0) next.splice(i, 1) }\n else next.push(name)\n scope.set('tools', { ...toolsCfg, exclude: next })\n }\n }\n const setIncludeMode = (on: boolean): void => {\n scope.set('tools', on\n ? { exclude: exclude.filter((n) => tools.some((tool) => tool.name === n)), include: tools.filter((tool) => !tool.hidden).map((tool) => tool.name) }\n : { exclude: ensureOnlyKnown(exclude, tools), include: [] })\n }\n\n return h('div', {}, [\n h('div', { style: s.bar }, [\n h('button', { style: includeMode ? s.tabActive : s.tab, onClick: () => setIncludeMode(true) }, t('includeMode')),\n h('button', { style: includeMode ? s.tab : s.tabActive, onClick: () => setIncludeMode(false) }, t('excludeMode')),\n ]),\n h('div', { style: s.list }, [\n tools.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,\n tools.map((tool) => {\n // Hidden state comes from the CONFIG (exclude/include lists), not the\n // inventory snapshot, so toggling updates the checkbox immediately.\n const hidden = includeMode ? !include.includes(tool.name) : exclude.includes(tool.name)\n return h('div', { key: tool.name, style: s.row }, [\n h('label', { style: s.switchWrap }, [\n h('input', { type: 'checkbox', checked: hidden, onChange: () => toggle(tool.name, hidden) }),\n h('span', { style: hidden ? s.badgeBlocked : s.badgeOk }, hidden ? t('hiddenOn') : t('hiddenOff')),\n ]),\n h('div', { style: s.rowBody }, [\n h('div', { style: s.rowTitle }, h('span', { style: s.code }, tool.name)),\n h('div', { style: s.preview }, String(tool.description ?? '').slice(0, 140)),\n ]),\n ])\n }),\n ]),\n ])\n}\n\nfunction ensureOnlyKnown(list: string[], known: Array<{ name: string }>): string[] {\n const names = known.map((tool) => tool.name)\n return list.filter((n) => names.includes(n))\n}\n","/** Presets tab: save / apply / export / import complete customization snapshots. */\nimport { createElement as h, useRef, useState, type ReactElement, type ChangeEvent } from 'react'\nimport type { Config, Inventory, Preset, PresetData, SettingsScope } from './types.ts'\nimport type { Translate } from './locales.ts'\nimport { s } from './styles.ts'\n\nexport function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {\n const presets = cfg.presets ?? []\n const [name, setName] = useState('')\n const fileRef = useRef<HTMLInputElement | null>(null)\n\n // Current section names (inventory + injected), used to decide what to block.\n const currentNames = (() => {\n const set = new Set<string>()\n for (const sec of inv?.sections ?? []) set.add(sec.name)\n for (const item of cfg.inject ?? []) set.add(item.name)\n return set\n })()\n\n // Capture the current customization as a new preset (full snapshot). The\n // absolute order is converted to a relative form (each section records the\n // section it should follow) so the preset stays portable across prompts.\n const saveCurrent = (): void => {\n const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`\n const inject = cfg.inject ?? []\n const order = inject.map((sec, i) => ({\n name: sec.name,\n after: i > 0 ? inject[i - 1].name : undefined,\n text: sec.text,\n }))\n const data: PresetData = {\n sections: cfg.sections,\n replace: cfg.replace,\n order,\n tools: cfg.tools,\n }\n scope.set('presets', [...presets, { id: genId(), name: presetName, data }])\n setName('')\n }\n\n // Apply a preset. The preset's section list defines the active set:\n // - same-name sections are overridden (content + order)\n // - preset sections missing from the current prompt are added\n // - current sections NOT in the preset list are disabled (blocked)\n // - the active sections are arranged by the preset's relative order\n const applyPreset = (preset: Preset): void => {\n const presetOrder = preset.data.order ?? []\n const presetNames = new Set(presetOrder.map((x) => x.name))\n\n // Blocked list: preset's own blocked names, plus any current section that\n // is not part of the preset's active set; preset's active names unblocked.\n const blocked = new Set(preset.data.sections ?? [])\n for (const name of currentNames) if (!presetNames.has(name)) blocked.add(name)\n for (const name of presetNames) blocked.delete(name)\n\n scope.set('inject', resolveOrder(presetOrder))\n scope.set('replace', { ...(cfg.replace ?? {}), ...(preset.data.replace ?? {}) })\n scope.set('sections', [...blocked])\n scope.set('tools', preset.data.tools ?? { exclude: [], include: [] })\n scope.set('activePreset', preset.id)\n }\n\n const deletePreset = (id: string): void => {\n scope.set('presets', presets.filter((p) => p.id !== id))\n if (cfg.activePreset === id) scope.unset('activePreset')\n }\n\n // Serialize a preset to a downloadable JSON file.\n const exportPreset = (preset: Preset): void => {\n const blob = new Blob([JSON.stringify({ name: preset.name, data: preset.data }, null, 2)], { type: 'application/json' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = `${preset.name}.json`\n a.click()\n URL.revokeObjectURL(url)\n }\n\n // Import preset(s) from a JSON file. Same-name presets are skipped; only\n // presets with new names are added to the local list.\n const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {\n const file = e.target.files?.[0]\n if (!file) return\n const reader = new FileReader()\n reader.onload = () => {\n try {\n const parsed = JSON.parse(String(reader.result))\n const incoming = Array.isArray(parsed) ? parsed : [parsed]\n const existing = new Set(presets.map((p) => p.name))\n const added = incoming\n .filter((p) => p && typeof p.name === 'string' && !existing.has(p.name))\n .map((p) => ({ id: genId(), name: p.name, data: (p.data ?? {}) as PresetData }))\n if (added.length > 0) scope.set('presets', [...presets, ...added])\n } catch { /* ignore invalid json */ }\n }\n reader.readAsText(file)\n e.target.value = ''\n }\n\n return h('div', { style: s.list }, [\n h('div', { style: s.injectBox }, [\n h('div', { style: s.rowTitle }, t('savePreset')),\n h('div', { style: s.injectRow }, [\n h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),\n h('button', { style: s.mini, onClick: saveCurrent }, t('save')),\n ]),\n ]),\n h('div', { style: s.injectBox }, [\n h('div', { style: s.rowTitle }, t('importPreset')),\n h('div', { style: s.injectRow }, [\n h('button', { style: s.mini, onClick: () => fileRef.current?.click() }, t('import')),\n h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),\n ]),\n ]),\n presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,\n presets.map((preset) => {\n const active = cfg.activePreset === preset.id\n return h('div', { key: preset.id, style: s.row }, [\n h('div', { style: s.rowBody }, [\n h('div', { style: s.rowTitle }, [\n h('span', { style: s.code }, preset.name),\n active ? h('span', { style: s.badgeOk }, t('active')) : null,\n ]),\n ]),\n h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),\n h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),\n h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),\n ])\n }),\n ])\n}\n\nfunction genId(): string {\n return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)\n}\n\n/**\n * Resolve a preset's relative order into an absolute ordered list (0..n-1).\n * Sections with no anchor (or whose anchor is absent) come first in preset\n * order; each anchored section is inserted right after its anchor; any\n * remaining sections (cycles) are appended.\n */\nfunction resolveOrder(presetOrder: Array<{ name: string; after?: string; text: string }>): Array<{ name: string; order: number; text: string }> {\n const afterMap = new Map<string, string | undefined>()\n const textMap = new Map<string, string>()\n for (const sec of presetOrder) {\n afterMap.set(sec.name, sec.after)\n textMap.set(sec.name, sec.text)\n }\n\n const result: string[] = []\n const placed = new Set<string>()\n\n // Roots: no anchor, or anchor not part of the preset's own set.\n for (const sec of presetOrder) {\n const anchor = afterMap.get(sec.name)\n if (!anchor || !afterMap.has(anchor)) {\n result.push(sec.name)\n placed.add(sec.name)\n }\n }\n\n // Chain resolution: place each section right after its (already placed) anchor.\n let changed = true\n while (changed) {\n changed = false\n for (const sec of presetOrder) {\n if (placed.has(sec.name)) continue\n const anchor = afterMap.get(sec.name)\n if (anchor && placed.has(anchor)) {\n result.splice(result.indexOf(anchor) + 1, 0, sec.name)\n placed.add(sec.name)\n changed = true\n }\n }\n }\n\n // Any remaining (cycle / unresolved) → append in preset order.\n for (const sec of presetOrder) {\n if (!placed.has(sec.name)) { result.push(sec.name); placed.add(sec.name) }\n }\n\n return result.map((name, i) => ({ name, order: i, text: textMap.get(name) ?? '' }))\n}\n","/**\n * dsh-prompt-customizer — browser half (TypeScript source).\n *\n * Registers a \"提示词定制\" settings section (sidebar entry) and a plugin\n * settings card. The panel fetches the effective section / tool inventory\n * from the host `/api/prompt-customizer/inventory` and lets the user block,\n * replace, inject prompt sections and hide tools. All writes go through the\n * bound settings scope (`prompt-customizer` namespace) and apply live.\n *\n * Built by tsdown into the __ModuleLoader__ factory bundle at client/client.js;\n * the only external is the loader module table's react entry.\n */\nimport { createElement as h, useEffect, useState, type ReactElement } from 'react'\nimport { DICT, type Translate } from './locales.ts'\nimport { s } from './styles.ts'\nimport type { Config, Inventory, SettingsScope } from './types.ts'\nimport { SectionsTab } from './SectionsTab.tsx'\nimport { ToolsTab } from './ToolsTab.tsx'\nimport { PresetsTab } from './PresetsTab.tsx'\n\nconst NS = 'prompt-customizer'\nconst INVENTORY_URL = '/api/prompt-customizer/inventory'\n\nexport const name = 'prompt-customizer'\nexport const inject = ['slots', 'locale']\n\n/** The subset of the client cordis context this plugin touches. */\ninterface ClientContext {\n effect(callback: () => unknown, label?: string): void\n locale: {\n register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown\n bind(namespace: string): Translate\n }\n inject(services: string[], callback: (scoped: SettingsScopeHost) => void): void\n}\n\n/** The host surface the settings card needs (settingsScope transport). */\ninterface SettingsScopeHost {\n settingsScope: {\n bind(options: { namespace: string }): SettingsScope\n }\n slots: {\n inject(name: string, register: () => unknown): void\n register(meta: Record<string, unknown>, render: () => unknown): unknown\n }\n}\n\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, DICT), 'prompt-customizer: locale')\n const t = ctx.locale.bind(NS)\n\n // Register a standalone settings section (sidebar menu entry) AND a plugin\n // settings card. Both go through a NESTED inject of settingsScope so the\n // panel can read/write the namespace config; on hosts without the settings\n // transport neither appears instead of unmounting the whole plugin.\n ctx.inject(['settingsScope'], (scoped) => {\n const scope = scoped.settingsScope.bind({ namespace: NS })\n\n scoped.slots.inject('settings.section', () => scoped.slots.register({\n name: 'settings.section',\n id: 'prompt-customizer',\n order: 50,\n label: () => t('nav'),\n locale: NS,\n inject: () => ({ t }),\n }, () => h(Panel, { scope, t })))\n\n scoped.slots.inject('settings.plugin.item', () => scoped.slots.register({\n name: 'settings.plugin.item',\n key: NS,\n locale: NS,\n inject: () => ({ t }),\n }, () => h(Panel, { scope, t })))\n })\n}\n\n// ── Root panel ──────────────────────────────────────────────────────────────\n\nfunction Panel({ scope, t }: { scope: SettingsScope; t: Translate }): ReactElement {\n const [snap, setSnap] = useState(() => scope.getSnapshot())\n const [inv, setInv] = useState<Inventory | null>(null)\n const [tab, setTab] = useState<'sections' | 'tools' | 'presets'>('sections')\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => scope.subscribe(() => setSnap(scope.getSnapshot())), [scope])\n\n const refresh = (): void => {\n fetch(INVENTORY_URL)\n .then((r) => r.json())\n .then((data: Inventory) => { setInv(data); setError(null) })\n .catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))\n }\n useEffect(refresh, [])\n\n if (snap.status !== 'ready' && snap.value === undefined) {\n return h('div', { style: s.muted }, t('loading'))\n }\n\n const cfg: Config = snap.value && typeof snap.value === 'object'\n ? snap.value as Config\n : { sections: [], replace: {}, inject: [], tools: { exclude: [], include: [] } }\n\n return h('div', { style: s.root }, [\n h('div', { style: s.bar }, [\n h('button', { style: tab === 'sections' ? s.tabActive : s.tab, onClick: () => setTab('sections') }, t('tabsSections')),\n h('button', { style: tab === 'tools' ? s.tabActive : s.tab, onClick: () => setTab('tools') }, t('tabsTools')),\n h('button', { style: tab === 'presets' ? s.tabActive : s.tab, onClick: () => setTab('presets') }, t('tabsPresets')),\n h('button', { style: s.refresh, onClick: refresh }, t('refresh')),\n ]),\n error ? h('div', { style: s.error }, String(error)) : null,\n tab === 'sections'\n ? h(SectionsTab, { cfg, inv, scope, t })\n : tab === 'tools'\n ? h(ToolsTab, { cfg, inv, scope, t })\n : h(PresetsTab, { cfg, inv, scope, t }),\n ])\n}\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ # dsh-prompt-customizer bundle patch: inserts the dual-face plugin row into
2
+ # the web profile roster. Applied as a profile bundle layer (the
3
+ # `dsh.bundle.patch` manifest field). The node half (exports ".") runs in the
4
+ # host process (settings namespace, prompt/tool filtering, inventory route);
5
+ # the `dsh.client` declaration in package.json loads the browser half
6
+ # (exports "./client", served at /plugins/prompt-customizer/client.js).
7
+ - insert:
8
+ - id: prompt-customizer
9
+ name: 'dsh-prompt-customizer'
package/lib/client.js ADDED
@@ -0,0 +1,344 @@
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
+ });
package/lib/index.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * dsh-prompt-customizer — host half.
3
+ *
4
+ * - Registers the `prompt-customizer` settings namespace (live-applied).
5
+ * - Hooks the `system-prompt/assemble` waterfall to block / replace / inject
6
+ * prompt sections and to hide tools from the model catalog by name. Only the
7
+ * model-facing list is affected; tools and routes keep working.
8
+ * - Serves `GET /api/prompt-customizer/inventory` returning the effective
9
+ * section and tool lists (for the `standard` agent when a roster exists),
10
+ * each marked with whether it is currently blocked / hidden.
11
+ */
12
+
13
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
14
+ import { Config, NS } from './schema.js'
15
+
16
+ export const name = 'prompt-customizer'
17
+ export const inject = ['settings', 'systemPrompt', 'tools']
18
+
19
+ export function apply(ctx, entry = {}) {
20
+ // 1) settings namespace: base = composition entry, live-applied.
21
+ const scope = ctx.settings.register(settingsNamespace(NS), Config, {
22
+ base: entry,
23
+ applies: 'live',
24
+ })
25
+ const read = () => scope.get() ?? {}
26
+
27
+ // 2) assemble waterfall: apply filter/replace/inject live.
28
+ ctx.on('system-prompt/assemble', async (assembly, context, next) => {
29
+ const assembled = await next()
30
+ const cfg = read()
31
+ const denied = new Set(cfg.sections ?? [])
32
+ const replace = cfg.replace ?? {}
33
+ const injectList = cfg.inject ?? []
34
+ const te = cfg.tools ?? {}
35
+ const exclude = new Set(te.exclude ?? [])
36
+ const include = Array.isArray(te.include) && te.include.length > 0 ? new Set(te.include) : null
37
+
38
+ let sections = assembled.sections
39
+ .filter((section) => !denied.has(section.name))
40
+ .map((section) =>
41
+ Object.hasOwn(replace, section.name) ? { ...section, text: replace[section.name] } : section,
42
+ )
43
+ for (const item of injectList) {
44
+ const index = sections.findIndex((section) => section.name === item.name)
45
+ if (index >= 0) {
46
+ // Existing section: apply the order override to control splice order.
47
+ // Only override text when the user actually provided replacement text.
48
+ sections[index] = item.text
49
+ ? { ...sections[index], text: item.text, order: item.order }
50
+ : { ...sections[index], order: item.order }
51
+ } else {
52
+ sections.push({ name: item.name, order: item.order, text: item.text })
53
+ }
54
+ }
55
+ sections.sort((a, b) => a.order - b.order)
56
+
57
+ let tools = assembled.tools
58
+ if (include) tools = tools.filter((tool) => include.has(tool.name))
59
+ else if (exclude.size > 0) tools = tools.filter((tool) => !exclude.has(tool.name))
60
+
61
+ return { ...assembled, sections, tools }
62
+ })
63
+
64
+ // 3) inventory route on the shared webserver (present in every web profile).
65
+ // Use ctx.get() so this stays optional — accessing ctx.webServer without
66
+ // declaring it in `inject` would throw "cannot get property without inject".
67
+ const webserver = ctx.get('webServer')
68
+ if (webserver) {
69
+ const dispose = webserver.register({
70
+ kind: 'exact',
71
+ path: '/api/prompt-customizer/inventory',
72
+ handler: async (_req, res) => {
73
+ try {
74
+ writeJson(res, 200, await buildInventory(ctx, read()))
75
+ } catch (error) {
76
+ writeJson(res, 500, {
77
+ ok: false,
78
+ error: String(error && error.message ? error.message : error),
79
+ })
80
+ }
81
+ },
82
+ })
83
+ ctx.effect(() => dispose, 'prompt-customizer: inventory route')
84
+ }
85
+ }
86
+
87
+ /** Resolve the `standard` agent's standing scope key when a roster is present. */
88
+ async function resolveStandardScope(ctx) {
89
+ try {
90
+ const presets = ctx.get('agentPresets')
91
+ if (presets && typeof presets.standingKeyFor === 'function') {
92
+ return await presets.standingKeyFor('standard')
93
+ }
94
+ } catch { /* no roster — fall back to the global layer */ }
95
+ return undefined
96
+ }
97
+
98
+ /** Enumerate the effective sections and tools with their blocked/hidden flags. */
99
+ async function buildInventory(ctx, cfg) {
100
+ const denied = new Set(cfg.sections ?? [])
101
+ const replace = cfg.replace ?? {}
102
+ const injectList = cfg.inject ?? []
103
+ const injectOrder = new Map(injectList.map((item) => [item.name, item.order]))
104
+ const te = cfg.tools ?? {}
105
+ const exclude = new Set(te.exclude ?? [])
106
+ const include = Array.isArray(te.include) && te.include.length > 0 ? new Set(te.include) : null
107
+
108
+ const scopeKey = await resolveStandardScope(ctx)
109
+
110
+ const sectionsMap = ctx.systemPrompt.layers.merge(scopeKey, (layer) => layer.sections)
111
+ const sections = [...sectionsMap.entries()]
112
+ .map(([name, section]) => ({
113
+ name,
114
+ order: injectOrder.has(name) ? injectOrder.get(name) : (typeof section.order === 'number' ? section.order : 0),
115
+ text: typeof section.text === 'function' ? '<动态生成>' : String(section.text ?? ''),
116
+ active: !denied.has(name),
117
+ replaced: Object.hasOwn(replace, name),
118
+ }))
119
+ .sort((a, b) => a.order - b.order)
120
+
121
+ let tools = []
122
+ try {
123
+ const schemas = ctx.tools.schemas(scopeKey) ?? []
124
+ tools = schemas
125
+ .map((tool) => ({
126
+ name: tool.name,
127
+ description: typeof tool.description === 'string' ? tool.description : '',
128
+ hidden: include ? !include.has(tool.name) : exclude.has(tool.name),
129
+ }))
130
+ .filter((tool) => tool.name && tool.name !== undefined)
131
+ .sort((a, b) => a.name.localeCompare(b.name))
132
+ } catch { /* tools registry unresolved — return empty list */ }
133
+
134
+ return { sections, tools, config: cfg }
135
+ }
136
+
137
+ function writeJson(res, status, body) {
138
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
139
+ res.end(JSON.stringify(body))
140
+ }
package/lib/schema.js ADDED
@@ -0,0 +1,27 @@
1
+ import Schema from '@deepseek-ai/schemastery'
2
+
3
+ /** Settings namespace for this plugin (lowercase kebab-case). */
4
+ export const NS = 'prompt-customizer'
5
+
6
+ /**
7
+ * The plugin's configuration schema, also used as the settings-namespace
8
+ * schema. GUI edits write into this shape under `~/.dsh/settings.yaml`.
9
+ */
10
+ export const Config = Schema.object({
11
+ // Block these prompt sections by name (filtered out of the assembled system prompt).
12
+ sections: Schema.array(Schema.string()).default([]),
13
+ // Replace a section's text by name (original order kept).
14
+ replace: Schema.dict(Schema.string()).default({}),
15
+ // Inject / overwrite sections (by name + order + text).
16
+ inject: Schema.array(Schema.object({
17
+ name: Schema.string().required(),
18
+ order: Schema.number().default(120),
19
+ text: Schema.string().required(),
20
+ })).default([]),
21
+ // Tool catalog filtering: exclude hides listed tools; include (when non-empty)
22
+ // keeps only listed tools and wins over exclude.
23
+ tools: Schema.object({
24
+ exclude: Schema.array(Schema.string()).default([]),
25
+ include: Schema.array(Schema.string()).default([]),
26
+ }).default({}),
27
+ })