dsh-prompt-customizer 0.2.6 → 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.
package/lib/schema.js CHANGED
@@ -1,52 +1,52 @@
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). `custom` marks a
16
- // section this plugin generated (a hidden marker so it can be identified as
17
- // deletable, even across presets, without relying on name collisions).
18
- inject: Schema.array(Schema.object({
19
- name: Schema.string().required(),
20
- order: Schema.number().default(120),
21
- text: Schema.string().required(),
22
- custom: Schema.boolean().default(false),
23
- })).default([]),
24
- // Tool catalog filtering: exclude hides listed tools; include (when non-empty)
25
- // keeps only listed tools and wins over exclude.
26
- tools: Schema.object({
27
- exclude: Schema.array(Schema.string()).default([]),
28
- include: Schema.array(Schema.string()).default([]),
29
- }).default({}),
30
- // Presets: complete customization snapshots (full prompt + tool config).
31
- presets: Schema.array(Schema.object({
32
- id: Schema.string().required(),
33
- name: Schema.string().required(),
34
- data: Schema.object({
35
- sections: Schema.array(Schema.string()),
36
- replace: Schema.dict(Schema.string()),
37
- // Relative order: each entry records the section it should follow.
38
- order: Schema.array(Schema.object({
39
- name: Schema.string().required(),
40
- after: Schema.string(),
41
- text: Schema.string(),
42
- custom: Schema.boolean().default(false),
43
- })),
44
- tools: Schema.object({
45
- exclude: Schema.array(Schema.string()),
46
- include: Schema.array(Schema.string()),
47
- }),
48
- }),
49
- })).default([]),
50
- // The id of the currently applied preset (only one active at a time).
51
- activePreset: Schema.string(),
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). `custom` marks a
16
+ // section this plugin generated (a hidden marker so it can be identified as
17
+ // deletable, even across presets, without relying on name collisions).
18
+ inject: Schema.array(Schema.object({
19
+ name: Schema.string().required(),
20
+ order: Schema.number().default(120),
21
+ text: Schema.string().required(),
22
+ custom: Schema.boolean().default(false),
23
+ })).default([]),
24
+ // Tool catalog filtering: exclude hides listed tools; include (when non-empty)
25
+ // keeps only listed tools and wins over exclude.
26
+ tools: Schema.object({
27
+ exclude: Schema.array(Schema.string()).default([]),
28
+ include: Schema.array(Schema.string()).default([]),
29
+ }).default({}),
30
+ // Presets: complete customization snapshots (full prompt + tool config).
31
+ presets: Schema.array(Schema.object({
32
+ id: Schema.string().required(),
33
+ name: Schema.string().required(),
34
+ data: Schema.object({
35
+ sections: Schema.array(Schema.string()),
36
+ replace: Schema.dict(Schema.string()),
37
+ // Relative order: each entry records the section it should follow.
38
+ order: Schema.array(Schema.object({
39
+ name: Schema.string().required(),
40
+ after: Schema.string(),
41
+ text: Schema.string(),
42
+ custom: Schema.boolean().default(false),
43
+ })),
44
+ tools: Schema.object({
45
+ exclude: Schema.array(Schema.string()),
46
+ include: Schema.array(Schema.string()),
47
+ }),
48
+ }),
49
+ })).default([]),
50
+ // The id of the currently applied preset (only one active at a time).
51
+ activePreset: Schema.string(),
52
52
  })
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "dsh-prompt-customizer",
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.6",
5
- "type": "module",
6
- "private": false,
7
- "main": "lib/index.js",
8
- "scripts": {
9
- "typecheck": "tsc -p tsconfig.client.json --noEmit",
10
- "build": "tsdown",
11
- "test": "node --test",
12
- "check": "npm run typecheck && npm run build && npm run test",
13
- "prepack": "npm run build"
14
- },
15
- "exports": {
16
- ".": "./lib/index.js",
17
- "./client": "./client/client.js",
18
- "./cordis.patch.yml": "./cordis.patch.yml",
19
- "./package.json": "./package.json"
20
- },
21
- "dsh": {
22
- "bundle": {
23
- "patch": "./cordis.patch.yml"
24
- },
25
- "client": {
26
- "inject": [
27
- "@deepseek-ai/dsh-client-connection",
28
- "@deepseek-ai/dsh-client-runtime",
29
- "@deepseek-ai/dsh-client-locale",
30
- "@deepseek-ai/dsh-client-ui-settings"
31
- ],
32
- "platform": "web"
33
- }
34
- },
35
- "peerDependencies": {
36
- "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
38
- "react": "^18.2.0",
39
- "react-dom": "^18.2.0"
40
- },
41
- "dependencies": {
42
- "@deepseek-ai/schemastery": "^3.18.0"
43
- },
44
- "devDependencies": {
45
- "@types/react": "~18.3.1",
46
- "@types/react-dom": "~18.3.1",
47
- "react": "^18.3.1",
48
- "react-dom": "~18.3.1",
49
- "tsdown": "^0.22.14",
50
- "typescript": "^5.6.0"
51
- },
52
- "files": [
53
- "lib",
54
- "client",
55
- "src",
56
- "cordis.patch.yml"
57
- ],
58
- "publishConfig": {
59
- "access": "public"
60
- },
61
- "license": "MIT"
1
+ {
2
+ "name": "dsh-prompt-customizer",
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.3.0",
5
+ "type": "module",
6
+ "private": false,
7
+ "main": "lib/index.js",
8
+ "scripts": {
9
+ "typecheck": "tsc -p tsconfig.client.json --noEmit",
10
+ "build": "tsdown",
11
+ "test": "node --test",
12
+ "check": "npm run typecheck && npm run build && npm run test",
13
+ "prepack": "npm run build"
14
+ },
15
+ "exports": {
16
+ ".": "./lib/index.js",
17
+ "./client": "./client/client.js",
18
+ "./cordis.patch.yml": "./cordis.patch.yml",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ },
25
+ "client": {
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-connection",
28
+ "@deepseek-ai/dsh-client-runtime",
29
+ "@deepseek-ai/dsh-client-locale",
30
+ "@deepseek-ai/dsh-client-ui-settings"
31
+ ],
32
+ "platform": "web"
33
+ }
34
+ },
35
+ "peerDependencies": {
36
+ "@deepseek-ai/cordis": "^4.0.1",
37
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
38
+ "react": "^18.2.0",
39
+ "react-dom": "^18.2.0"
40
+ },
41
+ "dependencies": {
42
+ "@deepseek-ai/schemastery": "^3.18.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/react": "~18.3.1",
46
+ "@types/react-dom": "~18.3.1",
47
+ "react": "^18.3.1",
48
+ "react-dom": "~18.3.1",
49
+ "tsdown": "^0.22.14",
50
+ "typescript": "^5.6.0"
51
+ },
52
+ "files": [
53
+ "lib",
54
+ "client",
55
+ "src",
56
+ "cordis.patch.yml"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "license": "MIT"
62
62
  }
@@ -1,110 +1,110 @@
1
- /** Presets tab: save / apply / export / import complete customization snapshots. */
2
- import { createElement as h, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
3
- import type { Config, Inventory, Preset, PresetData, SettingsScope } from './types.ts'
4
- import type { Translate } from './locales.ts'
5
- import { addImportedPresets, applyPresetData, buildPresetData, mergeSections, removePreset, serializePreset } from './presets.ts'
6
- import { s } from './styles.ts'
7
-
8
- export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
9
- const presets = cfg.presets ?? []
10
- const [name, setName] = useState('')
11
- const fileRef = useRef<HTMLInputElement | null>(null)
12
-
13
- const blockedNames = new Set(cfg.sections ?? [])
14
- const merged = mergeSections(inv, cfg, blockedNames)
15
- const currentNames = new Set(merged.map((sec) => sec.name))
16
-
17
- // Capture the current customization as a new preset (full snapshot). The
18
- // absolute order is converted to a relative form (each section records the
19
- // section it should follow) so the preset stays portable across prompts.
20
- const saveCurrent = (): void => {
21
- const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
22
- const data = buildPresetData(cfg, merged)
23
- scope.set('presets', [...presets, { id: genId(), name: presetName, data }])
24
- setName('')
25
- }
26
-
27
- // Apply a preset. The patch is computed by the pure helper:
28
- // - same-name sections are overridden (content + order)
29
- // - preset sections missing from the current prompt are added
30
- // - current sections NOT in the preset list are disabled (blocked)
31
- // - only the preset's ACTIVE sections are unblocked (its own blocked list
32
- // is preserved)
33
- const applyPreset = (preset: Preset): void => {
34
- const patch = applyPresetData(preset.data, cfg, currentNames)
35
- scope.set('inject', patch.inject)
36
- scope.set('replace', patch.replace)
37
- scope.set('sections', patch.sections)
38
- scope.set('tools', patch.tools)
39
- scope.set('activePreset', preset.id)
40
- }
41
-
42
- const deletePreset = (id: string): void => {
43
- const next = removePreset(presets, id, cfg.activePreset)
44
- scope.set('presets', next.presets)
45
- if (next.activeId === undefined && cfg.activePreset === id) scope.unset('activePreset')
46
- }
47
-
48
- // Serialize a preset to a downloadable JSON file.
49
- const exportPreset = (preset: Preset): void => {
50
- const blob = new Blob([JSON.stringify(serializePreset(preset), null, 2)], { type: 'application/json' })
51
- const url = URL.createObjectURL(blob)
52
- const a = document.createElement('a')
53
- a.href = url
54
- a.download = `${preset.name}.json`
55
- a.click()
56
- URL.revokeObjectURL(url)
57
- }
58
-
59
- // Import preset(s) from a JSON file. Same-name presets are skipped; only
60
- // presets with new names are added to the local list.
61
- const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {
62
- const file = e.target.files?.[0]
63
- if (!file) return
64
- const reader = new FileReader()
65
- reader.onload = () => {
66
- try {
67
- const parsed = JSON.parse(String(reader.result))
68
- scope.set('presets', addImportedPresets(presets, parsed, genId))
69
- } catch { /* ignore invalid json */ }
70
- }
71
- reader.readAsText(file)
72
- e.target.value = ''
73
- }
74
-
75
- return h('div', { style: s.list }, [
76
- h('div', { style: s.injectBox }, [
77
- h('div', { style: s.rowTitle }, t('savePreset')),
78
- h('div', { style: s.injectRow }, [
79
- h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
80
- h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
81
- ]),
82
- ]),
83
- h('div', { style: s.injectBox }, [
84
- h('div', { style: s.rowTitle }, t('importPreset')),
85
- h('div', { style: s.injectRow }, [
86
- h('button', { style: s.mini, onClick: () => fileRef.current?.click() }, t('import')),
87
- h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
88
- ]),
89
- ]),
90
- presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
91
- presets.map((preset) => {
92
- const active = cfg.activePreset === preset.id
93
- return h('div', { key: preset.id, style: s.row }, [
94
- h('div', { style: s.rowBody }, [
95
- h('div', { style: s.rowTitle }, [
96
- h('span', { style: s.code }, preset.name),
97
- active ? h('span', { style: s.badgeOk }, t('active')) : null,
98
- ]),
99
- ]),
100
- h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
101
- h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),
102
- h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
103
- ])
104
- }),
105
- ])
106
- }
107
-
108
- function genId(): string {
109
- return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
110
- }
1
+ /** Presets tab: save / apply / export / import complete customization snapshots. */
2
+ import { createElement as h, useRef, useState, type ReactElement, type ChangeEvent } from 'react'
3
+ import type { Config, Inventory, Preset, PresetData, SettingsScope } from './types.ts'
4
+ import type { Translate } from './locales.ts'
5
+ import { addImportedPresets, applyPresetData, buildPresetData, mergeSections, removePreset, serializePreset } from './presets.ts'
6
+ import { s } from './styles.ts'
7
+
8
+ export function PresetsTab({ cfg, inv, scope, t }: { cfg: Config; inv: Inventory | null; scope: SettingsScope; t: Translate }): ReactElement {
9
+ const presets = cfg.presets ?? []
10
+ const [name, setName] = useState('')
11
+ const fileRef = useRef<HTMLInputElement | null>(null)
12
+
13
+ const blockedNames = new Set(cfg.sections ?? [])
14
+ const merged = mergeSections(inv, cfg, blockedNames)
15
+ const currentNames = new Set(merged.map((sec) => sec.name))
16
+
17
+ // Capture the current customization as a new preset (full snapshot). The
18
+ // absolute order is converted to a relative form (each section records the
19
+ // section it should follow) so the preset stays portable across prompts.
20
+ const saveCurrent = (): void => {
21
+ const presetName = name.trim() || `${t('preset')} ${presets.length + 1}`
22
+ const data = buildPresetData(cfg, merged)
23
+ scope.set('presets', [...presets, { id: genId(), name: presetName, data }])
24
+ setName('')
25
+ }
26
+
27
+ // Apply a preset. The patch is computed by the pure helper:
28
+ // - same-name sections are overridden (content + order)
29
+ // - preset sections missing from the current prompt are added
30
+ // - current sections NOT in the preset list are disabled (blocked)
31
+ // - only the preset's ACTIVE sections are unblocked (its own blocked list
32
+ // is preserved)
33
+ const applyPreset = (preset: Preset): void => {
34
+ const patch = applyPresetData(preset.data, cfg, currentNames)
35
+ scope.set('inject', patch.inject)
36
+ scope.set('replace', patch.replace)
37
+ scope.set('sections', patch.sections)
38
+ scope.set('tools', patch.tools)
39
+ scope.set('activePreset', preset.id)
40
+ }
41
+
42
+ const deletePreset = (id: string): void => {
43
+ const next = removePreset(presets, id, cfg.activePreset)
44
+ scope.set('presets', next.presets)
45
+ if (next.activeId === undefined && cfg.activePreset === id) scope.unset('activePreset')
46
+ }
47
+
48
+ // Serialize a preset to a downloadable JSON file.
49
+ const exportPreset = (preset: Preset): void => {
50
+ const blob = new Blob([JSON.stringify(serializePreset(preset), null, 2)], { type: 'application/json' })
51
+ const url = URL.createObjectURL(blob)
52
+ const a = document.createElement('a')
53
+ a.href = url
54
+ a.download = `${preset.name}.json`
55
+ a.click()
56
+ URL.revokeObjectURL(url)
57
+ }
58
+
59
+ // Import preset(s) from a JSON file. Same-name presets are skipped; only
60
+ // presets with new names are added to the local list.
61
+ const onImportFile = (e: ChangeEvent<HTMLInputElement>): void => {
62
+ const file = e.target.files?.[0]
63
+ if (!file) return
64
+ const reader = new FileReader()
65
+ reader.onload = () => {
66
+ try {
67
+ const parsed = JSON.parse(String(reader.result))
68
+ scope.set('presets', addImportedPresets(presets, parsed, genId))
69
+ } catch { /* ignore invalid json */ }
70
+ }
71
+ reader.readAsText(file)
72
+ e.target.value = ''
73
+ }
74
+
75
+ return h('div', { style: s.list }, [
76
+ h('div', { style: s.injectBox }, [
77
+ h('div', { style: s.rowTitle }, t('savePreset')),
78
+ h('div', { style: s.injectRow }, [
79
+ h('input', { style: { ...s.input, flex: 1 }, placeholder: t('presetName'), value: name, onChange: (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value) }),
80
+ h('button', { style: s.mini, onClick: saveCurrent }, t('save')),
81
+ ]),
82
+ ]),
83
+ h('div', { style: s.injectBox }, [
84
+ h('div', { style: s.rowTitle }, t('importPreset')),
85
+ h('div', { style: s.injectRow }, [
86
+ h('button', { style: s.mini, onClick: () => fileRef.current?.click() }, t('import')),
87
+ h('input', { ref: fileRef, type: 'file', accept: '.json,application/json', style: { display: 'none' }, onChange: onImportFile }),
88
+ ]),
89
+ ]),
90
+ presets.length === 0 ? h('div', { style: s.muted }, t('empty')) : null,
91
+ presets.map((preset) => {
92
+ const active = cfg.activePreset === preset.id
93
+ return h('div', { key: preset.id, style: s.row }, [
94
+ h('div', { style: s.rowBody }, [
95
+ h('div', { style: s.rowTitle }, [
96
+ h('span', { style: s.code }, preset.name),
97
+ active ? h('span', { style: s.badgeOk }, t('active')) : null,
98
+ ]),
99
+ ]),
100
+ h('button', { style: s.mini, onClick: () => applyPreset(preset) }, t('apply')),
101
+ h('button', { style: s.mini, onClick: () => exportPreset(preset) }, t('export')),
102
+ h('button', { style: s.mini, onClick: () => deletePreset(preset.id) }, t('delete')),
103
+ ])
104
+ }),
105
+ ])
106
+ }
107
+
108
+ function genId(): string {
109
+ return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
110
+ }
@@ -1,59 +1,61 @@
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.
20
- const inFlight = useRef(false)
21
- const load = (): void => {
22
- if (inFlight.current) return
23
- inFlight.current = true
24
- setData(null)
25
- fetch(PREVIEW_URL)
26
- .then((r) => r.json())
27
- .then((body: Preview) => {
28
- if (body && body.ok === false) throw new Error((body as { error?: string }).error ?? 'preview failed')
29
- setData(body)
30
- setError(null)
31
- })
32
- .catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
33
- .finally(() => { inFlight.current = false })
34
- }
35
- useEffect(load, [])
36
- useEffect(() => scope.subscribe(load), [scope])
37
-
38
- return h('div', { style: s.list }, [
39
- h('div', { style: s.bar }, [
40
- h('button', { style: sub === 'prompt' ? s.tabActive : s.tab, onClick: () => setSub('prompt') }, t('previewPrompt')),
41
- h('button', { style: sub === 'tools' ? s.tabActive : s.tab, onClick: () => setSub('tools') }, t('previewTools')),
42
- ]),
43
- error ? h('div', { style: s.error }, String(error)) : null,
44
- data === null && !error ? h('div', { style: s.muted }, t('loading')) : null,
45
- sub === 'prompt'
46
- ? data
47
- ? [
48
- h('div', { style: s.rowTitle }, [
49
- h('span', { style: s.muted }, t('previewHint')),
50
- h('span', { style: s.orderTag }, `${data.sections.length} ${t('previewSections')}`),
51
- ]),
52
- h('pre', { style: s.previewText }, data.text || t('empty')),
53
- ]
54
- : null
55
- : data
56
- ? h(PreviewTools, { tools: data.tools, t })
57
- : null,
58
- ])
59
- }
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, active, refreshId }: { scope: SettingsScope; t: Translate; active: boolean; refreshId: number }): 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.
20
+ const inFlight = useRef(false)
21
+ const load = (): void => {
22
+ if (inFlight.current) return
23
+ inFlight.current = true
24
+ setData(null)
25
+ fetch(`${PREVIEW_URL}?t=${Date.now()}`)
26
+ .then((r) => r.json())
27
+ .then((body: Preview) => {
28
+ if (body && body.ok === false) throw new Error((body as { error?: string }).error ?? 'preview failed')
29
+ setData(body)
30
+ setError(null)
31
+ })
32
+ .catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
33
+ .finally(() => { inFlight.current = false })
34
+ }
35
+ useEffect(load, [])
36
+ useEffect(() => scope.subscribe(load), [scope])
37
+ // Refresh when this tab becomes active or when refresh button is clicked
38
+ useEffect(() => { if (active) load() }, [active, refreshId])
39
+
40
+ return h('div', { style: s.list }, [
41
+ h('div', { style: s.bar }, [
42
+ h('button', { style: sub === 'prompt' ? s.tabActive : s.tab, onClick: () => setSub('prompt') }, t('previewPrompt')),
43
+ h('button', { style: sub === 'tools' ? s.tabActive : s.tab, onClick: () => setSub('tools') }, t('previewTools')),
44
+ ]),
45
+ error ? h('div', { style: s.error }, String(error)) : null,
46
+ data === null && !error ? h('div', { style: s.muted }, t('loading')) : null,
47
+ sub === 'prompt'
48
+ ? data
49
+ ? [
50
+ h('div', { style: s.rowTitle }, [
51
+ h('span', { style: s.muted }, t('previewHint')),
52
+ h('span', { style: s.orderTag }, `${data.sections.length} ${t('previewSections')}`),
53
+ ]),
54
+ h('pre', { style: s.previewText }, data.text || t('empty')),
55
+ ]
56
+ : null
57
+ : data
58
+ ? h(PreviewTools, { tools: data.tools, t })
59
+ : null,
60
+ ])
61
+ }
@@ -1,31 +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
+ /** 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
+ }