@huanlin/dsh-plugin-preface-context 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.
Files changed (38) hide show
  1. package/README.md +113 -0
  2. package/cordis.patch.yml +11 -0
  3. package/lib/client.js +794 -0
  4. package/lib/client.js.map +1 -0
  5. package/lib/index.js +127 -0
  6. package/lib/types/client/bind-snapshot-selector.d.ts +7 -0
  7. package/lib/types/client/bind-snapshot-selector.js +19 -0
  8. package/lib/types/client/client/bind-snapshot-selector.d.ts +7 -0
  9. package/lib/types/client/client/bind-snapshot-selector.js +19 -0
  10. package/lib/types/client/client/index.d.ts +39 -0
  11. package/lib/types/client/client/index.js +50 -0
  12. package/lib/types/client/client/locales.d.ts +7 -0
  13. package/lib/types/client/client/locales.js +41 -0
  14. package/lib/types/client/client/preface-card-controller.d.ts +94 -0
  15. package/lib/types/client/client/preface-card-controller.js +181 -0
  16. package/lib/types/client/client/preface-card.css.d.ts +53 -0
  17. package/lib/types/client/client/preface-card.css.js +327 -0
  18. package/lib/types/client/client/preface-card.d.ts +27 -0
  19. package/lib/types/client/client/preface-card.js +41 -0
  20. package/lib/types/client/config.d.ts +41 -0
  21. package/lib/types/client/config.js +42 -0
  22. package/lib/types/client/index.d.ts +39 -0
  23. package/lib/types/client/index.js +50 -0
  24. package/lib/types/client/locales.d.ts +7 -0
  25. package/lib/types/client/locales.js +41 -0
  26. package/lib/types/client/preface-card-controller.d.ts +94 -0
  27. package/lib/types/client/preface-card-controller.js +181 -0
  28. package/lib/types/client/preface-card.css.d.ts +53 -0
  29. package/lib/types/client/preface-card.css.js +327 -0
  30. package/lib/types/client/preface-card.d.ts +27 -0
  31. package/lib/types/client/preface-card.js +41 -0
  32. package/lib/types/config.d.ts +40 -0
  33. package/lib/types/config.js +42 -0
  34. package/lib/types/index.d.ts +46 -0
  35. package/lib/types/index.js +64 -0
  36. package/lib/types/settings.d.ts +50 -0
  37. package/lib/types/settings.js +85 -0
  38. package/package.json +130 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Card styles + runtime style injection for the preface-context settings
3
+ * card.
4
+ *
5
+ * The DSH client loader never serves `.css` artifacts — every style must be
6
+ * injected inside the bundle's factory closure (the same contract the
7
+ * upstream tsdown.client preset implements with lightningcss). A plain
8
+ * template-string stylesheet installed once at plugin activation is the slim
9
+ * equivalent for a hand-rolled bundle: static, prefixed class names
10
+ * (`dsh-pfc-*`, unique in the document) replace the hashed CSS-modules map,
11
+ * and the `<style data-plugin-css>` tag is removed when the plugin unloads.
12
+ *
13
+ * Colors resolve through `--dsw-alias-*` tokens so the card adapts to the
14
+ * light/dark theme.
15
+ *
16
+ * @module @huanlin/dsh-plugin-preface-context/client/preface-card.css
17
+ */
18
+ /** Class-name map consumed by the card component (CSS-modules replacement). */
19
+ export const css = {
20
+ card: 'dsh-pfc-card',
21
+ cardOpen: 'dsh-pfc-card--open',
22
+ header: 'dsh-pfc-header',
23
+ headText: 'dsh-pfc-head-text',
24
+ name: 'dsh-pfc-name',
25
+ description: 'dsh-pfc-description',
26
+ pending: 'dsh-pfc-pending',
27
+ chevron: 'dsh-pfc-chevron',
28
+ chevronOpen: 'dsh-pfc-chevron--open',
29
+ body: 'dsh-pfc-body',
30
+ readOnly: 'dsh-pfc-readonly',
31
+ field: 'dsh-pfc-field',
32
+ head: 'dsh-pfc-field-head',
33
+ label: 'dsh-pfc-label',
34
+ badges: 'dsh-pfc-badges',
35
+ badge: 'dsh-pfc-badge',
36
+ reset: 'dsh-pfc-reset',
37
+ textarea: 'dsh-pfc-textarea',
38
+ toggleRow: 'dsh-pfc-toggle-row',
39
+ toggle: 'dsh-pfc-toggle',
40
+ toggleOn: 'dsh-pfc-toggle--on',
41
+ knob: 'dsh-pfc-knob',
42
+ knobOn: 'dsh-pfc-knob--on',
43
+ hint: 'dsh-pfc-hint',
44
+ footer: 'dsh-pfc-footer',
45
+ failed: 'dsh-pfc-failed',
46
+ discard: 'dsh-pfc-discard',
47
+ save: 'dsh-pfc-save',
48
+ };
49
+ /** `data-plugin-css` tag id of the injected stylesheet. */
50
+ const STYLE_TAG_ID = '@huanlin/dsh-plugin-preface-context/preface-card.css';
51
+ /** The card stylesheet (injected as one tagged `<style>` at activation). */
52
+ const CSS = `
53
+ .dsh-pfc-card {
54
+ list-style: none;
55
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
56
+ background: var(--dsw-alias-bg-layer-3, transparent);
57
+ border-radius: 12px;
58
+ transition: border-color 0.16s, background 0.16s;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .dsh-pfc-card--open {
63
+ border-color: var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.35));
64
+ }
65
+
66
+ .dsh-pfc-header {
67
+ display: flex;
68
+ width: 100%;
69
+ align-items: center;
70
+ gap: 12px;
71
+ padding: 14px 16px;
72
+ font: inherit;
73
+ color: inherit;
74
+ text-align: left;
75
+ background: transparent;
76
+ border: 0;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .dsh-pfc-head-text {
81
+ display: flex;
82
+ flex: 1 1 auto;
83
+ flex-direction: column;
84
+ gap: 4px;
85
+ min-width: 0;
86
+ }
87
+
88
+ .dsh-pfc-name {
89
+ font-size: 15px;
90
+ font-weight: 600;
91
+ line-height: 1.4;
92
+ color: var(--dsw-alias-label-primary, inherit);
93
+ }
94
+
95
+ .dsh-pfc-description {
96
+ font-size: 13px;
97
+ line-height: 1.5;
98
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.7));
99
+ }
100
+
101
+ .dsh-pfc-pending {
102
+ flex: none;
103
+ white-space: nowrap;
104
+ font-size: 11px;
105
+ font-weight: 500;
106
+ line-height: 17px;
107
+ padding: 1px 8px;
108
+ border-radius: 999px;
109
+ color: var(--dsw-alias-label-secondary, inherit);
110
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
111
+ }
112
+
113
+ .dsh-pfc-chevron {
114
+ flex: none;
115
+ display: inline-flex;
116
+ align-items: center;
117
+ color: var(--dsw-alias-label-tertiary, inherit);
118
+ transition: transform 0.16s;
119
+ }
120
+
121
+ .dsh-pfc-chevron--open {
122
+ transform: rotate(180deg);
123
+ }
124
+
125
+ .dsh-pfc-body {
126
+ margin: 0 16px;
127
+ padding: 12px 0 4px;
128
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
129
+ }
130
+
131
+ .dsh-pfc-readonly {
132
+ margin: 0 0 8px;
133
+ font-size: 12px;
134
+ line-height: 1.5;
135
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.7));
136
+ }
137
+
138
+ .dsh-pfc-field {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 4px;
142
+ margin: 0 0 12px;
143
+ }
144
+
145
+ .dsh-pfc-field-head {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+
151
+ .dsh-pfc-label {
152
+ font-size: 13px;
153
+ font-weight: 500;
154
+ color: var(--dsw-alias-label-primary, inherit);
155
+ }
156
+
157
+ .dsh-pfc-badges {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 6px;
161
+ margin-left: auto;
162
+ }
163
+
164
+ .dsh-pfc-badge {
165
+ font-size: 11px;
166
+ font-weight: 500;
167
+ line-height: 17px;
168
+ padding: 1px 8px;
169
+ border-radius: 999px;
170
+ color: var(--dsw-alias-label-secondary, inherit);
171
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
172
+ }
173
+
174
+ .dsh-pfc-reset {
175
+ appearance: none;
176
+ font: inherit;
177
+ font-size: 12px;
178
+ padding: 0;
179
+ border: 0;
180
+ background: transparent;
181
+ cursor: pointer;
182
+ color: var(--dsw-alias-brand-primary, #0a84ff);
183
+ }
184
+
185
+ .dsh-pfc-reset:disabled {
186
+ cursor: not-allowed;
187
+ opacity: 0.5;
188
+ }
189
+
190
+ .dsh-pfc-textarea {
191
+ width: 100%;
192
+ box-sizing: border-box;
193
+ min-height: 96px;
194
+ resize: vertical;
195
+ padding: 6px 10px;
196
+ font-family: var(--dsw-font-mono, ui-monospace, monospace);
197
+ font-size: 13px;
198
+ line-height: 1.5;
199
+ color: var(--dsw-alias-label-primary, inherit);
200
+ background: var(--dsw-alias-bg-layer-3, transparent);
201
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
202
+ border-radius: 8px;
203
+ }
204
+
205
+ .dsh-pfc-textarea:focus {
206
+ outline: none;
207
+ border-color: var(--dsw-alias-brand-primary, #0a84ff);
208
+ }
209
+
210
+ .dsh-pfc-textarea:disabled {
211
+ opacity: 0.6;
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ .dsh-pfc-toggle-row {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ }
220
+
221
+ .dsh-pfc-toggle {
222
+ position: relative;
223
+ width: 40px;
224
+ height: 22px;
225
+ flex: none;
226
+ padding: 0;
227
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
228
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
229
+ border-radius: 999px;
230
+ cursor: pointer;
231
+ transition: background 0.16s, border-color 0.16s;
232
+ }
233
+
234
+ .dsh-pfc-toggle--on {
235
+ background: var(--dsw-alias-brand-primary, #0a84ff);
236
+ border-color: var(--dsw-alias-brand-primary, #0a84ff);
237
+ }
238
+
239
+ .dsh-pfc-knob {
240
+ position: absolute;
241
+ top: 2px;
242
+ left: 2px;
243
+ width: 16px;
244
+ height: 16px;
245
+ background: var(--dsw-alias-bg-layer-1, #fff);
246
+ border-radius: 50%;
247
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
248
+ transition: transform 0.16s;
249
+ }
250
+
251
+ .dsh-pfc-knob--on {
252
+ transform: translateX(18px);
253
+ }
254
+
255
+ .dsh-pfc-toggle:disabled {
256
+ cursor: not-allowed;
257
+ opacity: 0.5;
258
+ }
259
+
260
+ .dsh-pfc-hint {
261
+ margin: 0;
262
+ font-size: 12px;
263
+ line-height: 1.5;
264
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.6));
265
+ }
266
+
267
+ .dsh-pfc-footer {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: flex-end;
271
+ gap: 8px;
272
+ padding: 12px 0 4px;
273
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
274
+ }
275
+
276
+ .dsh-pfc-failed {
277
+ flex: 1 1 auto;
278
+ min-width: 0;
279
+ margin: 0;
280
+ font-size: 12px;
281
+ line-height: 1.5;
282
+ color: var(--dsw-alias-label-error, #ff453a);
283
+ }
284
+
285
+ .dsh-pfc-discard,
286
+ .dsh-pfc-save {
287
+ appearance: none;
288
+ font: inherit;
289
+ font-size: 13px;
290
+ font-weight: 500;
291
+ line-height: 20px;
292
+ padding: 5px 14px;
293
+ border: 1px solid transparent;
294
+ border-radius: 8px;
295
+ cursor: pointer;
296
+ color: var(--dsw-alias-label-primary, inherit);
297
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
298
+ transition: background 0.16s, opacity 0.16s;
299
+ }
300
+
301
+ .dsh-pfc-save {
302
+ background: var(--dsw-alias-brand-primary, #0a84ff);
303
+ color: var(--dsw-alias-bg-layer-1, #fff);
304
+ }
305
+
306
+ .dsh-pfc-discard:disabled,
307
+ .dsh-pfc-save:disabled {
308
+ cursor: not-allowed;
309
+ opacity: 0.5;
310
+ }
311
+ `;
312
+ /**
313
+ * Install the card stylesheet as one tagged `<style>` element.
314
+ * @returns the disposer removing the tag (idempotent outside a document).
315
+ */
316
+ export function installPrefaceCardStyles() {
317
+ if (typeof document === 'undefined')
318
+ return () => { };
319
+ if (document.querySelector(`style[data-plugin-css='${STYLE_TAG_ID}']`) !== null)
320
+ return () => { };
321
+ const style = document.createElement('style');
322
+ style.dataset.plugin = '@huanlin/dsh-plugin-preface-context';
323
+ style.dataset.pluginCss = STYLE_TAG_ID;
324
+ style.textContent = CSS;
325
+ document.head.appendChild(style);
326
+ return () => { style.remove(); };
327
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The preface-context settings card: a collapsible card on the "插件配置"
3
+ * settings page. Renders an enable toggle and a multiline textarea for the
4
+ * context text. Stages edits locally and writes them on save through the
5
+ * bound {@link PrefaceCardController}.
6
+ *
7
+ * The chrome mirrors the upstream `PluginCard` shape (a header button
8
+ * stacking name over description, a dirty pill, a rotating chevron; a body
9
+ * with the form controls and a footer with Discard/Save) because the
10
+ * upstream client value face exports no reusable card. Styles come from the
11
+ * statically injected `preface-card.css` stylesheet (the loader never serves
12
+ * `.css` artifacts); every color resolves through a `--dsw-alias-*` token so
13
+ * the card adapts to the light/dark theme.
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
17
+ import type { PrefaceCardFace, PrefaceCardActions } from './preface-card-controller.ts';
18
+ /** Injected dependencies of the card (slot `inject`). */
19
+ export type PrefaceCardInjected = PrefaceCardFace & PrefaceCardActions;
20
+ /** Props the renderer binds for the card. */
21
+ export type PrefaceCardProps = PropsRuntime<'settings.plugin.item'> & PropsLocale<'settings.preface-context'> & InjectFace<PrefaceCardInjected>;
22
+ /**
23
+ * Render the preface-context card.
24
+ * @param props - locale copy, the card snapshot, and its form actions.
25
+ * @returns the card, or nothing when the namespace is unavailable.
26
+ */
27
+ export declare function PrefaceCard(props: PrefaceCardProps): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The preface-context settings card: a collapsible card on the "插件配置"
4
+ * settings page. Renders an enable toggle and a multiline textarea for the
5
+ * context text. Stages edits locally and writes them on save through the
6
+ * bound {@link PrefaceCardController}.
7
+ *
8
+ * The chrome mirrors the upstream `PluginCard` shape (a header button
9
+ * stacking name over description, a dirty pill, a rotating chevron; a body
10
+ * with the form controls and a footer with Discard/Save) because the
11
+ * upstream client value face exports no reusable card. Styles come from the
12
+ * statically injected `preface-card.css` stylesheet (the loader never serves
13
+ * `.css` artifacts); every color resolves through a `--dsw-alias-*` token so
14
+ * the card adapts to the light/dark theme.
15
+ */
16
+ import { useState } from 'react';
17
+ import clsx from 'clsx';
18
+ import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
19
+ import { css } from "./preface-card.css.js";
20
+ /**
21
+ * Render the preface-context card.
22
+ * @param props - locale copy, the card snapshot, and its form actions.
23
+ * @returns the card, or nothing when the namespace is unavailable.
24
+ */
25
+ export function PrefaceCard(props) {
26
+ const [open, setOpen] = useState(false);
27
+ const { t } = props;
28
+ const state = props.usePrefaceCard(snapshot => snapshot);
29
+ if (!state.available)
30
+ return null;
31
+ const title = t('title');
32
+ const blocked = !state.dirty || state.saving;
33
+ const enabledOn = state.enabled.text === 'true';
34
+ return (_jsxs("li", { className: clsx(css.card, open && css.cardOpen), children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'collapse' : 'expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: title }), _jsx("span", { className: css.description, children: t('description') })] }), state.dirty ? _jsx("span", { className: css.pending, children: t('unsaved') }) : null, _jsx(IconChevronDownOutline14, { className: clsx(css.chevron, open && css.chevronOpen) })] }), open
35
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: t('readOnly') }) : null, _jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, children: t('enabled') }), state.enabled.overridden
36
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: t('overridden') }), _jsx("button", { type: "button", className: css.reset, disabled: !state.writable, onClick: () => { props.resetField('enabled'); }, children: t('reset') })] }))
37
+ : null] }), _jsx("div", { className: css.toggleRow, children: _jsx("button", { type: "button", role: "switch", "aria-checked": enabledOn, className: clsx(css.toggle, enabledOn && css.toggleOn), disabled: !state.writable, onClick: () => { props.edit('enabled', enabledOn ? 'false' : 'true'); }, children: _jsx("span", { className: clsx(css.knob, enabledOn && css.knobOn) }) }) }), _jsx("p", { className: css.hint, children: t('enabledHint') })] }), _jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, children: t('contextText') }), state.contextText.overridden
38
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: t('overridden') }), _jsx("button", { type: "button", className: css.reset, disabled: !state.writable, onClick: () => { props.resetField('contextText'); }, children: t('reset') })] }))
39
+ : null] }), _jsx("textarea", { id: "plugin-config-preface-context-text", className: css.textarea, value: state.contextText.text, placeholder: t('contextTextPlaceholder'), disabled: !state.writable, onChange: (event) => { props.edit('contextText', event.target.value); } }), _jsx("p", { className: css.hint, children: t('contextTextHint') })] }), _jsxs("div", { className: css.footer, children: [state.failed ? _jsx("p", { className: css.failed, role: "status", children: t('saveFailed') }) : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.discard, children: t('discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.save, children: t(state.saving ? 'saving' : 'save') })] })] }))
40
+ : null] }));
41
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Config schema for the preface-context plugin (Schemastery, strict).
3
+ *
4
+ * The plugin-row config (the `entry` passed to `apply`) is the composition
5
+ * BASE of the `preface-context` settings namespace. The settings user layer
6
+ * is layered on top (schema defaults → base → user layer) when a settings
7
+ * service is mounted; without one, the entry is the sole source.
8
+ *
9
+ * @module @huanlin/dsh-plugin-preface-context/config
10
+ */
11
+ /** User-editable configuration for the preface-context plugin. */
12
+ export interface PrefaceConfig {
13
+ /** Master switch: when false, no context is injected at session start. */
14
+ enabled: boolean;
15
+ /** The text block injected as model-visible instructions context. */
16
+ contextText: string;
17
+ }
18
+ /**
19
+ * Schemastery schema for the `preface-context` settings namespace.
20
+ *
21
+ * Strict by construction: unknown keys fail validation here, even though the
22
+ * settings service itself is non-strict and would otherwise accept them.
23
+ */
24
+ export declare const Config: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types").default<Schemastery.ObjectS<{
25
+ enabled: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types").default<boolean, boolean>;
26
+ contextText: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types").default<string, string>;
27
+ }>, Schemastery.ObjectT<{
28
+ enabled: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types").default<boolean, boolean>;
29
+ contextText: import("C:/Users/Administrator/.dsh/source/current/vendor/schemastery/lib/types").default<string, string>;
30
+ }>>;
31
+ /**
32
+ * Resolve a raw config patch through the schema, returning a full
33
+ * {@link PrefaceConfig} with defaults applied. Unknown keys are rejected
34
+ * here (the settings service itself is non-strict and would otherwise accept
35
+ * them), matching the Loader's strict validation.
36
+ * @param input - a partial or complete config object.
37
+ * @returns the schema-resolved config.
38
+ * @throws when the input carries an unknown key or a value the schema rejects.
39
+ */
40
+ export declare function resolvePrefaceConfig(input: Partial<PrefaceConfig>): PrefaceConfig;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Config schema for the preface-context plugin (Schemastery, strict).
3
+ *
4
+ * The plugin-row config (the `entry` passed to `apply`) is the composition
5
+ * BASE of the `preface-context` settings namespace. The settings user layer
6
+ * is layered on top (schema defaults → base → user layer) when a settings
7
+ * service is mounted; without one, the entry is the sole source.
8
+ *
9
+ * @module @huanlin/dsh-plugin-preface-context/config
10
+ */
11
+ import z from 'schemastery';
12
+ /**
13
+ * Schemastery schema for the `preface-context` settings namespace.
14
+ *
15
+ * Strict by construction: unknown keys fail validation here, even though the
16
+ * settings service itself is non-strict and would otherwise accept them.
17
+ */
18
+ export const Config = z.object({
19
+ enabled: z.boolean().default(true),
20
+ contextText: z.string().default(''),
21
+ });
22
+ /** Known config keys (for strict unknown-key rejection). */
23
+ const CONFIG_KEYS = new Set(['enabled', 'contextText']);
24
+ /**
25
+ * Resolve a raw config patch through the schema, returning a full
26
+ * {@link PrefaceConfig} with defaults applied. Unknown keys are rejected
27
+ * here (the settings service itself is non-strict and would otherwise accept
28
+ * them), matching the Loader's strict validation.
29
+ * @param input - a partial or complete config object.
30
+ * @returns the schema-resolved config.
31
+ * @throws when the input carries an unknown key or a value the schema rejects.
32
+ */
33
+ export function resolvePrefaceConfig(input) {
34
+ if (input !== null && typeof input === 'object' && !Array.isArray(input)) {
35
+ for (const key of Object.keys(input)) {
36
+ if (!CONFIG_KEYS.has(key)) {
37
+ throw new Error(`preface-context: unknown config key "${key}"`);
38
+ }
39
+ }
40
+ }
41
+ return Config(input);
42
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-plugin-preface-context — injects a user-configured text block as
3
+ * model-visible instructions context at the start of every session.
4
+ *
5
+ * The plugin listens to `agent/session-start` and, when enabled and the
6
+ * context text is non-empty, calls `agent.inject()` with a plugin-sourced
7
+ * `user/message` carrying the text as a single text block. The message
8
+ * enters the session inbox and is claimed by the first `agent/pre-step`,
9
+ * reaching the model as instructions context closest to its first answer.
10
+ *
11
+ * The text and the enable switch are editable on the "插件配置" settings
12
+ * page: the host registers the `preface-context` settings namespace (the
13
+ * plugin-row config is the composition base), and the client half binds a
14
+ * card to that namespace through `ctx.settingsScope.bind()`. The runtime
15
+ * reads the LIVE composed value through the bridge source, so a settings
16
+ * edit takes effect on the next session start without a restart.
17
+ *
18
+ * @module @huanlin/dsh-plugin-preface-context
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis';
21
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
22
+ import type { PrefaceConfig } from './config.ts';
23
+ export declare const name = "dsh-plugin-preface-context";
24
+ /** Services the plugin consumes; the row loads once all are available. */
25
+ export declare const inject: string[];
26
+ /** Loader schema (schemastery, strict) — validated by the cordis Loader. */
27
+ export { Config } from './config.ts';
28
+ export type { PrefaceConfig } from './config.ts';
29
+ /**
30
+ * Build the user-message to inject for one session start.
31
+ *
32
+ * The message carries the context text as a single text block and is tagged
33
+ * `source.kind: 'plugin'` with `form: 'instructions'` so the model and UI
34
+ * can present it as workspace instructions rather than a user prompt.
35
+ * @param config - the live composed config.
36
+ * @returns the message to inject, or `undefined` when injection is disabled
37
+ * or the text is empty/whitespace.
38
+ */
39
+ export declare function buildPrefaceMessage(config: PrefaceConfig): ReturnType<typeof createUserMessage> | undefined;
40
+ /**
41
+ * Plugin entry: install the settings bridge, then inject the preface text on
42
+ * every session start.
43
+ * @param ctx - the plugin's cordis context.
44
+ * @param config - the composition entry config (base layer of the settings namespace).
45
+ */
46
+ export declare function apply(ctx: Context, config: PrefaceConfig): void;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * dsh-plugin-preface-context — injects a user-configured text block as
3
+ * model-visible instructions context at the start of every session.
4
+ *
5
+ * The plugin listens to `agent/session-start` and, when enabled and the
6
+ * context text is non-empty, calls `agent.inject()` with a plugin-sourced
7
+ * `user/message` carrying the text as a single text block. The message
8
+ * enters the session inbox and is claimed by the first `agent/pre-step`,
9
+ * reaching the model as instructions context closest to its first answer.
10
+ *
11
+ * The text and the enable switch are editable on the "插件配置" settings
12
+ * page: the host registers the `preface-context` settings namespace (the
13
+ * plugin-row config is the composition base), and the client half binds a
14
+ * card to that namespace through `ctx.settingsScope.bind()`. The runtime
15
+ * reads the LIVE composed value through the bridge source, so a settings
16
+ * edit takes effect on the next session start without a restart.
17
+ *
18
+ * @module @huanlin/dsh-plugin-preface-context
19
+ */
20
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
21
+ import { installPrefaceSettings } from "./settings.js";
22
+ export const name = 'dsh-plugin-preface-context';
23
+ /** Services the plugin consumes; the row loads once all are available. */
24
+ export const inject = ['agents', 'llm'];
25
+ /** Loader schema (schemastery, strict) — validated by the cordis Loader. */
26
+ export { Config } from "./config.js";
27
+ /** Plugin source tag carried by every injected message. */
28
+ const PLUGIN_SOURCE = { kind: 'plugin', plugin: 'dsh-plugin-preface-context', form: 'instructions' };
29
+ /**
30
+ * Build the user-message to inject for one session start.
31
+ *
32
+ * The message carries the context text as a single text block and is tagged
33
+ * `source.kind: 'plugin'` with `form: 'instructions'` so the model and UI
34
+ * can present it as workspace instructions rather than a user prompt.
35
+ * @param config - the live composed config.
36
+ * @returns the message to inject, or `undefined` when injection is disabled
37
+ * or the text is empty/whitespace.
38
+ */
39
+ export function buildPrefaceMessage(config) {
40
+ if (!config.enabled)
41
+ return undefined;
42
+ const text = config.contextText.trim();
43
+ if (text === '')
44
+ return undefined;
45
+ return createUserMessage({
46
+ content: [{ type: 'text', text: config.contextText }],
47
+ source: PLUGIN_SOURCE,
48
+ });
49
+ }
50
+ /**
51
+ * Plugin entry: install the settings bridge, then inject the preface text on
52
+ * every session start.
53
+ * @param ctx - the plugin's cordis context.
54
+ * @param config - the composition entry config (base layer of the settings namespace).
55
+ */
56
+ export function apply(ctx, config) {
57
+ const bridge = installPrefaceSettings(ctx, config);
58
+ ctx.on('agent/session-start', ({ agent }) => {
59
+ const message = buildPrefaceMessage(bridge.source());
60
+ if (message === undefined)
61
+ return;
62
+ agent.inject(message);
63
+ });
64
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Host-side `preface-context` settings namespace + live source wiring.
3
+ *
4
+ * The plugin-row config (the `entry` passed to the plugin's `apply`) is the
5
+ * composition BASE of the `preface-context` settings namespace: when a dsh
6
+ * settings service is mounted, its user layer is layered on top (schema
7
+ * defaults → base → user layer) and the runtime reads the live resolved
8
+ * value through the bridge's `source` thunk. Without a settings service the
9
+ * conditional `ctx.inject(['settings'], ...)` child never activates and the
10
+ * source is exactly the entry: behavior identical to a no-settings build.
11
+ *
12
+ * The namespace reaches the web configuration boundary directly: the
13
+ * apiproxy `settings.mutate` / `settings.describe` RPCs accept any
14
+ * registered namespace (no allowlist gate in current upstream dsh), so the
15
+ * client card reads/writes through `ctx.settingsScope.bind({ namespace })`
16
+ * — no gateway channel is needed.
17
+ *
18
+ * @module @huanlin/dsh-plugin-preface-context/settings
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis';
21
+ import type { PrefaceConfig } from './config.ts';
22
+ /** The `preface-context` settings namespace (registered when a settings service exists). */
23
+ export declare const PREFACE_SETTINGS_NAMESPACE = "preface-context";
24
+ /**
25
+ * The live configuration source for the runtime.
26
+ *
27
+ * `source()` returns the composed config (schema defaults → plugin-row base
28
+ * → settings user layer). `onChange` registers a callback that fires at
29
+ * attach, on committed changes, and at detach back to the entry.
30
+ */
31
+ export interface PrefaceSettingsBridge {
32
+ /** Read the currently authoritative config. */
33
+ source(): PrefaceConfig;
34
+ /** Register a change callback (fires at attach, on change, at detach). */
35
+ onChange(callback: () => void): void;
36
+ }
37
+ /**
38
+ * Install the `preface-context` settings namespace and wire the live source.
39
+ *
40
+ * Mirrors the dsh `installSettingsSection` pattern. The registration rides a
41
+ * conditional `ctx.inject(['settings'], ...)` child, so with no settings
42
+ * service the source stays the entry config. Multi-fiber dedupe: the host
43
+ * may compose several preface-context fibers, and `settings.register` fails
44
+ * loud on a duplicate namespace — the catch lets the first registration own
45
+ * the live namespace while later instances fall back to the entry source.
46
+ * @param ctx - consumer plugin context owning the wiring.
47
+ * @param entry - the composition entry config, used as `base`.
48
+ * @returns the bridge the runtime and gateway read through.
49
+ */
50
+ export declare function installPrefaceSettings(ctx: Context, entry: PrefaceConfig): PrefaceSettingsBridge;