@khorsheed/dsh-context-guard 0.2.1 → 0.2.2
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/CHANGELOG.md +9 -0
- package/README.en.md +2 -1
- package/README.i18n.yaml +2 -2
- package/README.md +2 -1
- package/lib/client.js +320 -135
- package/lib/index.js +47 -18
- package/lib/types/client/CompactGuardButton.js +2 -2
- package/lib/types/client/SettingsCard.d.ts +11 -6
- package/lib/types/client/SettingsCard.js +50 -22
- package/lib/types/client/config.js +14 -1
- package/lib/types/client/icons.d.ts +19 -0
- package/lib/types/client/icons.js +3 -0
- package/lib/types/client/index.d.ts +22 -7
- package/lib/types/client/index.js +52 -10
- package/lib/types/client/scope.d.ts +62 -0
- package/lib/types/client/scope.js +59 -0
- package/lib/types/client/slots.d.ts +22 -11
- package/lib/types/index.d.ts +22 -8
- package/lib/types/index.js +32 -9
- package/lib/types/namespace.d.ts +2 -2
- package/lib/types/namespace.js +2 -2
- package/lib/types/settings.d.ts +16 -10
- package/lib/types/settings.js +19 -6
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +7 -21
package/lib/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import z from "@deepseek-ai/schemastery";
|
|
|
3
3
|
/**
|
|
4
4
|
* The settings namespace owned by the context-guard plugin. Plain string so
|
|
5
5
|
* both the host half (settings registration) and the browser half
|
|
6
|
-
* (settingsScope binding and the settings.plugin.item card key) import
|
|
7
|
-
* without dragging host-only dependencies into the client bundle.
|
|
6
|
+
* (settingsScope binding and the 0.1.5 settings.plugin.item card key) import
|
|
7
|
+
* it without dragging host-only dependencies into the client bundle.
|
|
8
8
|
*/
|
|
9
9
|
/** Settings namespace owned by the context-guard plugin. */
|
|
10
10
|
const CONTEXT_GUARD_NS = "context-guard";
|
|
@@ -12,38 +12,67 @@ const CONTEXT_GUARD_NS = "context-guard";
|
|
|
12
12
|
//#region lib/types/settings.js
|
|
13
13
|
/**
|
|
14
14
|
* Host-side settings section of the context-guard plugin: the schemastery
|
|
15
|
-
* schema
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* schema of the `context-guard` configuration entry. rc.1's SettingsForms
|
|
16
|
+
* serves the entry's own Config and edits only its volatile fields; 0.1.5
|
|
17
|
+
* validates the legacy `settings.register` namespace with the same schema.
|
|
18
|
+
* The browser half reads the resolved section through whichever settings
|
|
19
|
+
* scope the line serves (see client/scope.ts); the composition entry (the
|
|
20
|
+
* cordis.yml row) becomes the section's base layer, so YAML values keep
|
|
19
21
|
* working as the defaults the card can override.
|
|
20
22
|
*
|
|
21
23
|
* These two fields tune ONLY when the compact button appears — the official
|
|
22
24
|
* compaction engine (compaction-basic) reads its own `thresholdRatio` /
|
|
23
25
|
* `maxTokens` config and never touches this section.
|
|
24
26
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const thresholdField = z.number().min(.01).max(1).default(.8);
|
|
28
|
+
/**
|
|
29
|
+
* Section shape: the context-occupancy fraction at which the button appears.
|
|
30
|
+
* The field is marked `.volatile()` when the running schemastery supports it
|
|
31
|
+
* (3.18.4, host rc.1 — SettingsForms serves and edits ONLY volatile fields);
|
|
32
|
+
* 0.1.5's schemastery (3.18.2) has no such method, the probe leaves the field
|
|
33
|
+
* plain, and the legacy register path carries the section (the marker is
|
|
34
|
+
* inert there). The inline probe (not a helper) keeps the emitted type
|
|
35
|
+
* nameable under pnpm's layout.
|
|
36
|
+
*/
|
|
37
|
+
const ContextGuardSettingsSchema = z.object({ thresholdRatio: typeof thresholdField.volatile === "function" ? thresholdField.volatile() : thresholdField });
|
|
27
38
|
//#endregion
|
|
28
39
|
//#region lib/types/index.js
|
|
29
40
|
/**
|
|
30
|
-
* Context-guard plugin, node half.
|
|
31
|
-
*
|
|
32
|
-
* the browser half can bind its
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* Context-guard plugin, node half. Serves the `context-guard` settings
|
|
42
|
+
* section so the web settings page can render the compact-timing card and
|
|
43
|
+
* the browser half can bind its scope to the same section — on rc.1 through
|
|
44
|
+
* the entry's own Config (below; SettingsForms serves and edits its volatile
|
|
45
|
+
* field), on 0.1.5 through an explicit `settings.register` namespace whose
|
|
46
|
+
* base layer is the composition entry. The plugin's own runtime behavior is
|
|
47
|
+
* purely client-side (the composer compact button); this registration only
|
|
48
|
+
* exists to make the two tunables (`thresholdRatio`, `maxTokens`) editable
|
|
49
|
+
* from the GUI instead of YAML.
|
|
36
50
|
* @module @khorsheed/dsh-context-guard
|
|
37
51
|
*/
|
|
38
52
|
/**
|
|
39
|
-
*
|
|
53
|
+
* The entry Config: rc.1's SettingsForms serves exactly this schema under the
|
|
54
|
+
* row id (`context-guard` — the settings namespace by construction) and
|
|
55
|
+
* persists edits to its volatile field through the profile patch. On 0.1.5
|
|
56
|
+
* the same schema validates the legacy namespace instead; the volatile
|
|
57
|
+
* marker is probed at schema build time and simply absent there. The bare
|
|
58
|
+
* `z` annotation keeps the emitted type nameable under pnpm's layout.
|
|
59
|
+
*/
|
|
60
|
+
const Config = ContextGuardSettingsSchema;
|
|
61
|
+
/**
|
|
62
|
+
* Serve the compact-timing section on whichever settings face the host line
|
|
63
|
+
* carries.
|
|
40
64
|
* @param ctx - Host context whose optional settings service owns the section.
|
|
41
|
-
* @param config - the plugin's composition entry,
|
|
65
|
+
* @param config - the plugin's composition entry, the legacy path's base layer.
|
|
42
66
|
*/
|
|
43
67
|
function apply(ctx, config = {}) {
|
|
44
68
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
45
|
-
settingsCtx.settings
|
|
69
|
+
const settings = settingsCtx.settings;
|
|
70
|
+
if (typeof settings.register === "function") {
|
|
71
|
+
settings.register(CONTEXT_GUARD_NS, ContextGuardSettingsSchema, { base: config });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (typeof settings.configure === "function") settings.configure({ auto: false }, ctx.fiber);
|
|
46
75
|
});
|
|
47
76
|
}
|
|
48
77
|
//#endregion
|
|
49
|
-
export { CONTEXT_GUARD_NS, ContextGuardSettingsSchema, apply };
|
|
78
|
+
export { CONTEXT_GUARD_NS, Config, ContextGuardSettingsSchema, apply };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { IconWarningOutlineMedium } from './icons.js';
|
|
4
4
|
import { guardReading } from './guard.js';
|
|
5
5
|
import css from './CompactGuardButton.module.css';
|
|
6
6
|
/**
|
|
@@ -49,5 +49,5 @@ export function CompactGuardButton({ useProjection, useConfig, t, thresholdRatio
|
|
|
49
49
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
50
50
|
});
|
|
51
51
|
};
|
|
52
|
-
return (_jsxs("span", { className: css.wrap, children: [_jsxs("button", { type: "button", className: `${css.button} ${css.warning}`, "aria-label": t('button.aria', { percent: String(reading.percent) }), disabled: busy, onClick: run, children: [_jsx("span", { className: css.icon, "aria-hidden": true, children: _jsx(
|
|
52
|
+
return (_jsxs("span", { className: css.wrap, children: [_jsxs("button", { type: "button", className: `${css.button} ${css.warning}`, "aria-label": t('button.aria', { percent: String(reading.percent) }), disabled: busy, onClick: run, children: [_jsx("span", { className: css.icon, "aria-hidden": true, children: _jsx(IconWarningOutlineMedium, { size: 14 }) }), t('button.label')] }), error !== null && (_jsx("span", { className: css.error, role: "status", title: error, children: t('button.error') }))] }));
|
|
53
53
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { ContextGuardSettingsCardProps } from './slots.ts';
|
|
1
|
+
import type { ContextGuardBundleConfigProps, ContextGuardSettingsCardProps } from './slots.ts';
|
|
2
2
|
/**
|
|
3
|
-
* The context-guard settings card in the plugin configuration tab: the
|
|
4
|
-
* collapsible chrome the official plugin cards use (a header naming the
|
|
3
|
+
* The context-guard settings card in the 0.1.5 plugin configuration tab: the
|
|
4
|
+
* same collapsible chrome the official plugin cards use (a header naming the
|
|
5
5
|
* plugin and what its settings govern, disclosing the controls in place),
|
|
6
6
|
* re-implemented here because the client bundle-purity gate forbids
|
|
7
|
-
* value-importing the official card chrome.
|
|
8
|
-
* through the shared settingsScope on save; per-field reset reverts a field
|
|
9
|
-
* to the composition layer.
|
|
7
|
+
* value-importing the official card chrome.
|
|
10
8
|
*/
|
|
11
9
|
export declare function ContextGuardSettingsCard({ useConfig, scope, t }: ContextGuardSettingsCardProps): import("react").JSX.Element | null;
|
|
10
|
+
/**
|
|
11
|
+
* The bundle's configuration on the alpha.2 Plugins page
|
|
12
|
+
* (`plugins.bundle.config`, keyed by package name): the page draws the title,
|
|
13
|
+
* the icon, and the crumb itself, so the entry renders the one-liner for the
|
|
14
|
+
* `summary` view and the bare form — with its own save control — for `page`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ContextGuardBundleConfig({ view, useConfig, scope, t }: ContextGuardBundleConfigProps): import("react").JSX.Element | null;
|
|
12
17
|
//# sourceMappingURL=SettingsCard.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { IconChevronDownOutlineMedium } from './icons.js';
|
|
4
4
|
import css from './SettingsCard.module.css';
|
|
5
5
|
/** Parse a draft into a number, or NaN when it is not one. */
|
|
6
6
|
function parseNumber(text) {
|
|
@@ -15,18 +15,11 @@ function overridden(snapshot, field) {
|
|
|
15
15
|
&& field in snapshot.user;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* re-implemented here because the client bundle-purity gate forbids
|
|
22
|
-
* value-importing the official card chrome. Two staged number fields write
|
|
23
|
-
* through the shared settingsScope on save; per-field reset reverts a field
|
|
24
|
-
* to the composition layer.
|
|
18
|
+
* The staged threshold edit both settings surfaces share. Two staged number
|
|
19
|
+
* fields write through the shared settings scope on save; per-field reset
|
|
20
|
+
* reverts a field to the composition layer.
|
|
25
21
|
*/
|
|
26
|
-
|
|
27
|
-
const snapshot = useConfig(value => value);
|
|
28
|
-
const value = snapshot.status === 'ready' ? snapshot.value : undefined;
|
|
29
|
-
const [open, setOpen] = useState(false);
|
|
22
|
+
function useThresholdDraft(scope, snapshot, value) {
|
|
30
23
|
const [threshold, setThreshold] = useState({ text: '', invalid: false });
|
|
31
24
|
const [saved, setSaved] = useState(false);
|
|
32
25
|
const [error, setError] = useState(false);
|
|
@@ -53,8 +46,6 @@ export function ContextGuardSettingsCard({ useConfig, scope, t }) {
|
|
|
53
46
|
aliveRef.current = false;
|
|
54
47
|
};
|
|
55
48
|
}, [value, snapshot.revision]);
|
|
56
|
-
if (snapshot.status === 'unavailable')
|
|
57
|
-
return null;
|
|
58
49
|
const write = (draft, min, max) => {
|
|
59
50
|
const parsed = parseNumber(draft.text);
|
|
60
51
|
setThreshold({ text: draft.text, invalid: !Number.isFinite(parsed) || parsed < min || parsed > max });
|
|
@@ -85,13 +76,50 @@ export function ContextGuardSettingsCard({ useConfig, scope, t }) {
|
|
|
85
76
|
setError(true);
|
|
86
77
|
});
|
|
87
78
|
};
|
|
88
|
-
const
|
|
79
|
+
const discard = () => {
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return;
|
|
82
|
+
setThreshold({ text: String(value.thresholdRatio), invalid: false });
|
|
83
|
+
setSaved(false);
|
|
84
|
+
setError(false);
|
|
85
|
+
};
|
|
89
86
|
const dirty = threshold.text !== String(value?.thresholdRatio ?? '');
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
return { threshold, saved, error, dirty, write, save, reset, discard };
|
|
88
|
+
}
|
|
89
|
+
/** The threshold field row and the save/discard footer both settings surfaces render. */
|
|
90
|
+
function SettingsBody({ snapshot, draft, t }) {
|
|
91
|
+
return (_jsxs("div", { className: css.body, children: [_jsxs("label", { className: css.field, children: [_jsxs("span", { className: css.fieldHead, children: [_jsx("span", { className: css.fieldName, children: t('settings.field.threshold') }), overridden(snapshot, 'thresholdRatio') && _jsx("span", { className: css.badge, children: t('settings.overridden') })] }), _jsxs("span", { className: css.inputRow, children: [_jsx("input", { className: draft.threshold.invalid ? `${css.input} ${css.inputInvalid}` : css.input, type: "number", step: "0.01", min: "0.01", max: "1", value: draft.threshold.text, "aria-label": t('settings.field.threshold'), onChange: (event) => { draft.write({ text: event.target.value, invalid: false }, 0.01, 1); } }), _jsx("button", { type: "button", className: css.reset, onClick: draft.reset, children: t('settings.reset') })] }), _jsx("span", { className: css.hint, children: t('settings.field.threshold.hint') })] }), _jsxs("span", { className: css.footer, children: [_jsxs("span", { className: css.status, children: [draft.saved ? _jsx("span", { className: css.saved, children: t('settings.saved') }) : null, draft.error ? _jsx("span", { className: css.errorText, children: t('settings.error') }) : null] }), _jsx("button", { type: "button", className: css.secondary, disabled: !draft.dirty, onClick: draft.discard, children: t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: !draft.dirty || draft.threshold.invalid || snapshot.writable !== true, onClick: draft.save, children: t('settings.save') })] })] }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The context-guard settings card in the 0.1.5 plugin configuration tab: the
|
|
95
|
+
* same collapsible chrome the official plugin cards use (a header naming the
|
|
96
|
+
* plugin and what its settings govern, disclosing the controls in place),
|
|
97
|
+
* re-implemented here because the client bundle-purity gate forbids
|
|
98
|
+
* value-importing the official card chrome.
|
|
99
|
+
*/
|
|
100
|
+
export function ContextGuardSettingsCard({ useConfig, scope, t }) {
|
|
101
|
+
const snapshot = useConfig(value => value);
|
|
102
|
+
const value = snapshot.status === 'ready' ? snapshot.value : undefined;
|
|
103
|
+
const [open, setOpen] = useState(false);
|
|
104
|
+
const draft = useThresholdDraft(scope, snapshot, value);
|
|
105
|
+
if (snapshot.status === 'unavailable')
|
|
106
|
+
return null;
|
|
107
|
+
const title = t('settings.title');
|
|
108
|
+
return (_jsxs("li", { className: open ? `${css.card} ${css.cardOpen}` : css.card, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'settings.collapse' : 'settings.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('settings.description') })] }), _jsx(IconChevronDownOutlineMedium, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && value !== undefined && _jsx(SettingsBody, { snapshot: snapshot, draft: draft, t: t })] }));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The bundle's configuration on the alpha.2 Plugins page
|
|
112
|
+
* (`plugins.bundle.config`, keyed by package name): the page draws the title,
|
|
113
|
+
* the icon, and the crumb itself, so the entry renders the one-liner for the
|
|
114
|
+
* `summary` view and the bare form — with its own save control — for `page`.
|
|
115
|
+
*/
|
|
116
|
+
export function ContextGuardBundleConfig({ view, useConfig, scope, t }) {
|
|
117
|
+
const snapshot = useConfig(value => value);
|
|
118
|
+
const value = snapshot.status === 'ready' ? snapshot.value : undefined;
|
|
119
|
+
const draft = useThresholdDraft(scope, snapshot, value);
|
|
120
|
+
if (view === 'summary')
|
|
121
|
+
return _jsx("span", { className: css.description, children: t('settings.description') });
|
|
122
|
+
if (snapshot.status === 'unavailable' || value === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
return _jsx(SettingsBody, { snapshot: snapshot, draft: draft, t: t });
|
|
97
125
|
}
|
|
@@ -16,6 +16,18 @@ const THRESHOLD_RATIO_MAX = 1;
|
|
|
16
16
|
function clamp(value, min, max) {
|
|
17
17
|
return Math.min(max, Math.max(min, value));
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Unwrap one maybe-Volatile config field. rc.1 resolves volatile-marked
|
|
21
|
+
* Config fields into live references (`{ get() }`); 0.1.5 passes the plain
|
|
22
|
+
* value. The schema's volatile marker is probed per line (see
|
|
23
|
+
* ../settings.ts), so BOTH shapes arrive depending on the host line.
|
|
24
|
+
*/
|
|
25
|
+
function unwrapVolatile(value) {
|
|
26
|
+
if (value !== null && typeof value === 'object' && typeof value.get === 'function') {
|
|
27
|
+
return value.get();
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
32
|
* Normalize the entry config into the full {@link ContextGuardConfig}: every
|
|
21
33
|
* omitted field takes its documented default and every numeric field is
|
|
@@ -24,7 +36,8 @@ function clamp(value, min, max) {
|
|
|
24
36
|
* @returns the effective guard behavior.
|
|
25
37
|
*/
|
|
26
38
|
export function resolveConfig(config) {
|
|
39
|
+
const ratio = unwrapVolatile(config?.thresholdRatio);
|
|
27
40
|
return {
|
|
28
|
-
thresholdRatio: clamp(
|
|
41
|
+
thresholdRatio: clamp(typeof ratio === 'number' ? ratio : 0.8, THRESHOLD_RATIO_MIN, THRESHOLD_RATIO_MAX),
|
|
29
42
|
};
|
|
30
43
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
|
|
3
|
+
* (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
|
|
4
|
+
* share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
|
|
5
|
+
* no host runtime identity, identical on every line. Re-run the generator
|
|
6
|
+
* after adding an icon import or bumping the harness checkout.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-context-guard/client
|
|
9
|
+
*/
|
|
10
|
+
/** Shared props for every product icon component (mirrors the upstream IconProps). */
|
|
11
|
+
export interface IconProps {
|
|
12
|
+
/** Square edge in px; defaults to the glyph's own drawn size. */
|
|
13
|
+
size?: number | undefined;
|
|
14
|
+
/** Extra class for layout placement; color rides currentColor. */
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
18
|
+
export declare const IconWarningOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
|
|
3
|
+
export const IconWarningOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z", stroke: "currentColor" }), _jsx("path", { d: "M8 4.29199V9.79199", stroke: "currentColor" }), _jsx("path", { d: "M8 10.708V11.708", stroke: "currentColor" })] }));
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
* `conversation.input.right` entry — a compact button inside the composer's
|
|
4
4
|
* tool row that appears automatically when the next request's budget
|
|
5
5
|
* (`contextPressure.projectedTokens` + the output budget) crosses
|
|
6
|
-
* `thresholdRatio` of the routed model's context window — plus one
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `thresholdRatio` of the routed model's context window — plus one settings
|
|
7
|
+
* surface editing the same number live: a standalone install renders the
|
|
8
|
+
* bundle's own configuration on its Plugins-page detail view
|
|
9
|
+
* (`plugins.bundle.config`, keyed by package name), a family-bundle install
|
|
10
|
+
* renders the same card through the row-level configure entry on the
|
|
11
|
+
* BUNDLE's detail view (`plugins.row.config`, keyed
|
|
12
|
+
* `@khorsheed/dsh-bundle-conversation-toolbox#context-guard`), and 0.1.5
|
|
13
|
+
* renders the `settings.plugin.item` card in the plugin configuration tab
|
|
14
|
+
* (keyed by the settings namespace). The action
|
|
15
|
+
* rides the official `/compact` command
|
|
9
16
|
* channel (`remote.commands.execute` → host `ctx.commands` →
|
|
10
17
|
* `ctx.compaction.compactNow`); the config rides the official settings
|
|
11
|
-
* surface (host half
|
|
12
|
-
* `
|
|
18
|
+
* surface (the host half serves the section — its own Config on rc.1, a
|
|
19
|
+
* `settings.register` namespace on 0.1.5 — and this half binds it through
|
|
20
|
+
* ./scope.ts, probing rc.1's `configForms` first and 0.1.5's `settingsScope`
|
|
21
|
+
* second), so neither needs a new RPC or any edit to core packages;
|
|
13
22
|
* composing this plugin out of cordis.yml removes every surface it adds.
|
|
14
23
|
*
|
|
15
24
|
* Why this exists: the official auto-compaction fires at `agent/pre-step`
|
|
@@ -31,10 +40,16 @@ export { resolveConfig } from './config.ts';
|
|
|
31
40
|
export type { ContextGuardKey } from './locales.ts';
|
|
32
41
|
export { guardReading } from './guard.ts';
|
|
33
42
|
export type { GuardInput, GuardLevel, GuardReading } from './guard.ts';
|
|
34
|
-
export type {
|
|
43
|
+
export type { GuardScope, GuardScopeSnapshot } from './scope.ts';
|
|
44
|
+
export type { CompactGuardButtonProps, ContextGuardBundleConfigProps, ContextGuardInjected, ContextGuardSettingsCardInjected, ContextGuardSettingsCardProps, } from './slots.ts';
|
|
35
45
|
/** Dictionary namespace owned by this plugin. */
|
|
36
46
|
export { NS };
|
|
37
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Required services: the slot ledger, the command Remote, and the copy. The
|
|
49
|
+
* settings scope is deliberately NOT injected — rc.1 and 0.1.5 name different
|
|
50
|
+
* services (`configForms` vs `settingsScope`), and a composition without
|
|
51
|
+
* either must not pend the bundle (see ./scope.ts).
|
|
52
|
+
*/
|
|
38
53
|
export declare const inject: string[];
|
|
39
54
|
/**
|
|
40
55
|
* Client plugin body: register the composer-tool-row compact button and the
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { CONTEXT_GUARD_NS } from '../namespace.js';
|
|
2
2
|
import { resolveConfig } from './config.js';
|
|
3
3
|
import { en, NS, zh } from './locales.js';
|
|
4
|
+
import { bindGuardScope, GuardScopeChannel } from './scope.js';
|
|
4
5
|
import { CompactGuardButton } from './CompactGuardButton.js';
|
|
5
|
-
import { ContextGuardSettingsCard } from './SettingsCard.js';
|
|
6
|
+
import { ContextGuardBundleConfig, ContextGuardSettingsCard } from './SettingsCard.js';
|
|
6
7
|
export { resolveConfig } from './config.js';
|
|
7
8
|
export { guardReading } from './guard.js';
|
|
8
9
|
/** Dictionary namespace owned by this plugin. */
|
|
9
10
|
export { NS };
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* This bundle's package name — the key the Plugins page dispatches
|
|
13
|
+
* `plugins.bundle.config` entries on (identity triangle: cordis.patch.yml,
|
|
14
|
+
* tsdown.config.ts, invariant.ts).
|
|
15
|
+
*/
|
|
16
|
+
const PACKAGE_NAME = '@khorsheed/dsh-context-guard';
|
|
17
|
+
/**
|
|
18
|
+
* Required services: the slot ledger, the command Remote, and the copy. The
|
|
19
|
+
* settings scope is deliberately NOT injected — rc.1 and 0.1.5 name different
|
|
20
|
+
* services (`configForms` vs `settingsScope`), and a composition without
|
|
21
|
+
* either must not pend the bundle (see ./scope.ts).
|
|
22
|
+
*/
|
|
23
|
+
export const inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
12
24
|
/**
|
|
13
25
|
* Client plugin body: register the composer-tool-row compact button and the
|
|
14
26
|
* settings card over the shared `context-guard` section.
|
|
@@ -19,9 +31,11 @@ export function apply(ctx, config) {
|
|
|
19
31
|
const fallback = resolveConfig(config);
|
|
20
32
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'context-guard: dictionaries');
|
|
21
33
|
// One shared live section: the settings card writes it, the button reads
|
|
22
|
-
// it.
|
|
23
|
-
//
|
|
24
|
-
|
|
34
|
+
// it. The channel binds whichever settings face the host line serves and
|
|
35
|
+
// publishes `unavailable` until then — the button falls back to the
|
|
36
|
+
// composition-time values above, the card renders nothing.
|
|
37
|
+
const scope = new GuardScopeChannel();
|
|
38
|
+
bindGuardScope(ctx, scope);
|
|
25
39
|
// The slot is declared by ui-conversation, whose apply order relative to
|
|
26
40
|
// this plugin is unconstrained: register through slots.inject so the entry
|
|
27
41
|
// waits for the declaration instead of crashing the loader at boot.
|
|
@@ -46,10 +60,38 @@ export function apply(ctx, config) {
|
|
|
46
60
|
hooks: { config: scope },
|
|
47
61
|
}),
|
|
48
62
|
}, CompactGuardButton));
|
|
49
|
-
// The
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
63
|
+
// The settings surface follows the host line AND the install shape — three
|
|
64
|
+
// tracks, all riding slots.inject so each fires only where its declaration
|
|
65
|
+
// exists. Standalone install: the package's own Plugins-page detail view
|
|
66
|
+
// (`plugins.bundle.config`, keyed by package name). Installed as a
|
|
67
|
+
// conversation-toolbox member: the member is no longer the profile's
|
|
68
|
+
// direct dependency, so only the bundle gets a detail view — the card rides
|
|
69
|
+
// the row-level slot (`plugins.row.config`, keyed `<bundle package>#<row
|
|
70
|
+
// id>` with the row id the bundle's patch declares) and the bundle's page
|
|
71
|
+
// gains a configure entry opening it. 0.1.5: the legacy
|
|
72
|
+
// configurable-plugins tab card (`settings.plugin.item`, keyed by the
|
|
73
|
+
// settings namespace) through a string-keyed duck narrow of the same
|
|
74
|
+
// service (the slot name is gone from the alpha.2 SlotMap).
|
|
75
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
76
|
+
name: 'plugins.bundle.config',
|
|
77
|
+
key: PACKAGE_NAME,
|
|
78
|
+
locale: NS,
|
|
79
|
+
inject: () => ({
|
|
80
|
+
scope,
|
|
81
|
+
hooks: { config: scope },
|
|
82
|
+
}),
|
|
83
|
+
}, ContextGuardBundleConfig));
|
|
84
|
+
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
85
|
+
name: 'plugins.row.config',
|
|
86
|
+
key: '@khorsheed/dsh-bundle-conversation-toolbox#context-guard',
|
|
87
|
+
locale: NS,
|
|
88
|
+
inject: () => ({
|
|
89
|
+
scope,
|
|
90
|
+
hooks: { config: scope },
|
|
91
|
+
}),
|
|
92
|
+
}, ContextGuardBundleConfig));
|
|
93
|
+
const legacy = ctx.slots;
|
|
94
|
+
legacy.inject('settings.plugin.item', () => legacy.register({
|
|
53
95
|
name: 'settings.plugin.item',
|
|
54
96
|
key: CONTEXT_GUARD_NS,
|
|
55
97
|
locale: NS,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two host lines' per-namespace settings scope behind one observable.
|
|
3
|
+
*
|
|
4
|
+
* 0.1.5 binds `ctx.settingsScope`; rc.1 replaced that service with
|
|
5
|
+
* `ctx.configForms` — the two carry the SAME snapshot shape and the same
|
|
6
|
+
* field writes (only the resolutions gained an acceptance boolean, which the
|
|
7
|
+
* card's then/catch usage never reads), so one structural face covers both.
|
|
8
|
+
* Neither service is declared in the plugin's inject list: a composition
|
|
9
|
+
* without ui-settings must not pend the bundle, and the providers can mount
|
|
10
|
+
* after this plugin, so the channel publishes `unavailable` until either
|
|
11
|
+
* line's deferred probe arms it.
|
|
12
|
+
*
|
|
13
|
+
* @module @khorsheed/dsh-context-guard/client/scope
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { ContextGuardConfig } from './config.ts';
|
|
17
|
+
/** Snapshot shape shared by 0.1.5's `SettingsScopeSnapshot` and rc.1's `ConfigFormSnapshot`. */
|
|
18
|
+
export interface GuardScopeSnapshot {
|
|
19
|
+
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
20
|
+
readonly value: ContextGuardConfig | undefined;
|
|
21
|
+
/** Composition layer the value resolves over; a cleared field reverts to it. */
|
|
22
|
+
readonly base: unknown;
|
|
23
|
+
/** Raw user layer as stored; a field's PRESENCE here marks it overridden. */
|
|
24
|
+
readonly user: unknown;
|
|
25
|
+
/** Namespace revision fencing the next write. */
|
|
26
|
+
readonly revision: number | undefined;
|
|
27
|
+
readonly writable: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** The reads and field writes both lines' scopes share. */
|
|
30
|
+
export interface GuardScope {
|
|
31
|
+
getSnapshot(): GuardScopeSnapshot;
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
set(field: string, value: unknown): Promise<unknown>;
|
|
34
|
+
unset(field: string): Promise<unknown>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Observable mirror of the bound scope. The slot entries' `hooks.config` seat
|
|
38
|
+
* binds to this at registration time — before (and whether or not) a settings
|
|
39
|
+
* provider appears — and arming swaps the source in and republishes.
|
|
40
|
+
*/
|
|
41
|
+
export declare class GuardScopeChannel {
|
|
42
|
+
private scope;
|
|
43
|
+
private readonly listeners;
|
|
44
|
+
readonly getSnapshot: () => GuardScopeSnapshot;
|
|
45
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
46
|
+
readonly set: (field: string, value: unknown) => Promise<unknown>;
|
|
47
|
+
readonly unset: (field: string) => Promise<unknown>;
|
|
48
|
+
/** Bind the line-served scope; the first arm wins (the two lines never coexist). */
|
|
49
|
+
arm(scope: GuardScope): void;
|
|
50
|
+
private emit;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Arm the channel from whichever settings face the host line serves: rc.1's
|
|
54
|
+
* `configForms.get(entryId)` — the entry id IS the settings namespace, both
|
|
55
|
+
* being the row id — first, 0.1.5's `settingsScope.bind({ namespace })`
|
|
56
|
+
* second. Both probes ride deferred injects so a later-mounting provider
|
|
57
|
+
* still lands; a composition with neither leaves the channel unavailable.
|
|
58
|
+
* @param ctx - the client plugin context.
|
|
59
|
+
* @param channel - the observable the slot entries read.
|
|
60
|
+
*/
|
|
61
|
+
export declare function bindGuardScope(ctx: Context, channel: GuardScopeChannel): void;
|
|
62
|
+
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CONTEXT_GUARD_NS } from '../namespace.js';
|
|
2
|
+
/** Published while no settings provider serves the section (stable identity). */
|
|
3
|
+
const UNAVAILABLE = {
|
|
4
|
+
status: 'unavailable',
|
|
5
|
+
value: undefined,
|
|
6
|
+
base: undefined,
|
|
7
|
+
user: undefined,
|
|
8
|
+
revision: undefined,
|
|
9
|
+
writable: false,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Observable mirror of the bound scope. The slot entries' `hooks.config` seat
|
|
13
|
+
* binds to this at registration time — before (and whether or not) a settings
|
|
14
|
+
* provider appears — and arming swaps the source in and republishes.
|
|
15
|
+
*/
|
|
16
|
+
export class GuardScopeChannel {
|
|
17
|
+
scope;
|
|
18
|
+
listeners = new Set();
|
|
19
|
+
getSnapshot = () => this.scope?.getSnapshot() ?? UNAVAILABLE;
|
|
20
|
+
subscribe = (listener) => {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => { this.listeners.delete(listener); };
|
|
23
|
+
};
|
|
24
|
+
set = (field, value) => this.scope?.set(field, value) ?? Promise.resolve(false);
|
|
25
|
+
unset = (field) => this.scope?.unset(field) ?? Promise.resolve(false);
|
|
26
|
+
/** Bind the line-served scope; the first arm wins (the two lines never coexist). */
|
|
27
|
+
arm(scope) {
|
|
28
|
+
if (this.scope !== undefined)
|
|
29
|
+
return;
|
|
30
|
+
this.scope = scope;
|
|
31
|
+
scope.subscribe(() => { this.emit(); });
|
|
32
|
+
this.emit();
|
|
33
|
+
}
|
|
34
|
+
emit() {
|
|
35
|
+
for (const listener of this.listeners)
|
|
36
|
+
listener();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Arm the channel from whichever settings face the host line serves: rc.1's
|
|
41
|
+
* `configForms.get(entryId)` — the entry id IS the settings namespace, both
|
|
42
|
+
* being the row id — first, 0.1.5's `settingsScope.bind({ namespace })`
|
|
43
|
+
* second. Both probes ride deferred injects so a later-mounting provider
|
|
44
|
+
* still lands; a composition with neither leaves the channel unavailable.
|
|
45
|
+
* @param ctx - the client plugin context.
|
|
46
|
+
* @param channel - the observable the slot entries read.
|
|
47
|
+
*/
|
|
48
|
+
export function bindGuardScope(ctx, channel) {
|
|
49
|
+
ctx.inject(['configForms'], (formsCtx) => {
|
|
50
|
+
const forms = formsCtx.get('configForms');
|
|
51
|
+
if (typeof forms?.get === 'function')
|
|
52
|
+
channel.arm(forms.get(CONTEXT_GUARD_NS));
|
|
53
|
+
});
|
|
54
|
+
ctx.inject(['settingsScope'], (legacyCtx) => {
|
|
55
|
+
const legacy = legacyCtx.get('settingsScope');
|
|
56
|
+
if (typeof legacy?.bind === 'function')
|
|
57
|
+
channel.arm(legacy.bind({ namespace: CONTEXT_GUARD_NS }));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slot-facing types of the context-guard client half: the injected faces of
|
|
3
|
-
* the composer-tool-row compact button and the settings
|
|
4
|
-
* the shared `context-guard` settings scope as a hooks source so the
|
|
5
|
-
* and the card react to the same live section.
|
|
3
|
+
* the composer-tool-row compact button and the settings surfaces, both
|
|
4
|
+
* carrying the shared `context-guard` settings scope as a hooks source so the
|
|
5
|
+
* button and the card react to the same live section. The settings surface
|
|
6
|
+
* has one face per host line: the alpha.2 Plugins page renders the bundle's
|
|
7
|
+
* configuration (`plugins.bundle.config`, keyed by package name, owner prop
|
|
8
|
+
* `view`); the 0.1.5 configurable-plugins tab renders the card
|
|
9
|
+
* (`settings.plugin.item`, keyed by the settings namespace, owner props
|
|
10
|
+
* intentionally empty — the structural type below is exact).
|
|
6
11
|
*/
|
|
7
12
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
8
|
-
import type {
|
|
9
|
-
import type { ContextGuardConfig } from './config.ts';
|
|
13
|
+
import type { GuardScope } from './scope.ts';
|
|
10
14
|
/** Injected action face of the composer-tool-row entry. */
|
|
11
15
|
export interface ContextGuardInjected {
|
|
12
16
|
/** Resolved context-occupancy fraction at which the guard turns on; the boot-time fallback. */
|
|
@@ -20,20 +24,27 @@ export interface ContextGuardInjected {
|
|
|
20
24
|
compactNow: () => Promise<string | null>;
|
|
21
25
|
hooks: {
|
|
22
26
|
/** The shared `context-guard` settings section; the button binds `useConfig` to it. */
|
|
23
|
-
config:
|
|
27
|
+
config: GuardScope;
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
30
|
/** Full props of the composer-tool-row compact entry. */
|
|
27
31
|
export type CompactGuardButtonProps = PropsRuntime<'conversation.input.right'> & InjectFace<ContextGuardInjected> & PropsLocale<'context-guard'>;
|
|
28
|
-
/** Injected face of
|
|
32
|
+
/** Injected face of both settings surfaces: the same shared section, read and written here. */
|
|
29
33
|
export interface ContextGuardSettingsCardInjected {
|
|
30
34
|
/** The live section handle; the card stages edits and writes through it on save. */
|
|
31
|
-
scope:
|
|
35
|
+
scope: GuardScope;
|
|
32
36
|
hooks: {
|
|
33
37
|
/** The shared `context-guard` settings section; the card binds `useConfig` to it. */
|
|
34
|
-
config:
|
|
38
|
+
config: GuardScope;
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Full props of the 0.1.5 settings.plugin.item card entry. The removed slot's
|
|
43
|
+
* owner share was intentionally empty, so the props are the injected face and
|
|
44
|
+
* the locale seat alone; the registration goes through a duck-typed narrow of
|
|
45
|
+
* the slots service (see client/index.ts).
|
|
46
|
+
*/
|
|
47
|
+
export type ContextGuardSettingsCardProps = InjectFace<ContextGuardSettingsCardInjected> & PropsLocale<'context-guard'>;
|
|
48
|
+
/** Full props of the alpha.2 plugins.bundle.config entry (owner prop `view`). */
|
|
49
|
+
export type ContextGuardBundleConfigProps = PropsRuntime<'plugins.bundle.config'> & InjectFace<ContextGuardSettingsCardInjected> & PropsLocale<'context-guard'>;
|
|
39
50
|
//# sourceMappingURL=slots.d.ts.map
|