@khorsheed/dsh-context-guard 0.2.0 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.en.md +2 -1
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +2 -1
  5. package/lib/client.js +320 -135
  6. package/lib/index.js +47 -18
  7. package/lib/types/client/CompactGuardButton.js +2 -2
  8. package/lib/types/client/SettingsCard.d.ts +11 -6
  9. package/lib/types/client/SettingsCard.js +50 -22
  10. package/lib/types/client/config.js +14 -1
  11. package/lib/types/client/icons.d.ts +19 -0
  12. package/lib/types/client/icons.js +3 -0
  13. package/lib/types/client/index.d.ts +22 -7
  14. package/lib/types/client/index.js +52 -10
  15. package/lib/types/client/scope.d.ts +62 -0
  16. package/lib/types/client/scope.js +59 -0
  17. package/lib/types/client/slots.d.ts +22 -11
  18. package/lib/types/index.d.ts +22 -8
  19. package/lib/types/index.js +32 -9
  20. package/lib/types/namespace.d.ts +2 -2
  21. package/lib/types/namespace.js +2 -2
  22. package/lib/types/settings.d.ts +16 -10
  23. package/lib/types/settings.js +19 -6
  24. package/locale/en.json +6 -0
  25. package/locale/zh.json +6 -0
  26. package/package.json +7 -21
  27. package/lib/client.js.map +0 -1
  28. package/lib/tsconfig.tsbuildinfo +0 -1
  29. package/lib/types/client/CompactGuardButton.d.ts.map +0 -1
  30. package/lib/types/client/SettingsCard.d.ts.map +0 -1
  31. package/lib/types/client/config.d.ts.map +0 -1
  32. package/lib/types/client/guard.d.ts.map +0 -1
  33. package/lib/types/client/index.d.ts.map +0 -1
  34. package/lib/types/client/locales.d.ts.map +0 -1
  35. package/lib/types/client/slots.d.ts.map +0 -1
  36. package/lib/types/index.d.ts.map +0 -1
  37. package/lib/types/invariant.d.ts.map +0 -1
  38. package/lib/types/namespace.d.ts.map +0 -1
  39. package/lib/types/settings.d.ts.map +0 -1
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 it
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 that validates the `context-guard` settings namespace. The browser
16
- * half reads the resolved section through `ctx.settingsScope` and the
17
- * settings page renders it as a plugin card; the composition entry (the
18
- * cordis.yml row) becomes the section's `base` layer, so YAML values keep
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
- /** Section shape: the context-occupancy fraction at which the button appears. */
26
- const ContextGuardSettingsSchema = z.object({ thresholdRatio: z.number().min(.01).max(1).default(.8) });
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. Registers the `context-guard` settings
31
- * namespace so the web settings page can render the compact-timing card and
32
- * the browser half can bind its `settingsScope` to the same section. The
33
- * plugin's own runtime behavior is purely client-side (the composer compact
34
- * button); this registration only exists to make the two tunables
35
- * (`thresholdRatio`, `maxTokens`) editable from the GUI instead of YAML.
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
- * Register the compact-timing section when a settings provider exists.
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, used as the section's base layer.
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.register(CONTEXT_GUARD_NS, ContextGuardSettingsSchema, { base: config });
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 { IconWarningOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';
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(IconWarningOutline16, { size: 14 }) }), t('button.label')] }), error !== null && (_jsx("span", { className: css.error, role: "status", title: error, children: t('button.error') }))] }));
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 same
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. Two staged number fields write
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 { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
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 context-guard settings card in the plugin configuration tab: the same
19
- * collapsible chrome the official plugin cards use (a header naming the
20
- * plugin and what its settings govern, disclosing the controls in place),
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
- export function ContextGuardSettingsCard({ useConfig, scope, t }) {
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 title = t('settings.title');
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 (_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(IconChevronDownOutline14, { className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron })] }), open && value !== undefined && (_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: threshold.invalid ? `${css.input} ${css.inputInvalid}` : css.input, type: "number", step: "0.01", min: "0.01", max: "1", value: threshold.text, "aria-label": t('settings.field.threshold'), onChange: (event) => { write({ text: event.target.value, invalid: false }, 0.01, 1); } }), _jsx("button", { type: "button", className: css.reset, onClick: 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: [saved ? _jsx("span", { className: css.saved, children: t('settings.saved') }) : null, error ? _jsx("span", { className: css.errorText, children: t('settings.error') }) : null] }), _jsx("button", { type: "button", className: css.secondary, disabled: !dirty, onClick: () => {
91
- if (value === undefined)
92
- return;
93
- setThreshold({ text: String(value.thresholdRatio), invalid: false });
94
- setSaved(false);
95
- setError(false);
96
- }, children: t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: !dirty || threshold.invalid || snapshot.writable !== true, onClick: save, children: t('settings.save') })] })] }))] }));
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(config?.thresholdRatio ?? 0.8, THRESHOLD_RATIO_MIN, THRESHOLD_RATIO_MAX),
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
- * `settings.plugin.item` card in the plugin configuration tab that edits the
8
- * same two numbers live. The action rides the official `/compact` command
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 registers the namespace, this half binds its
12
- * `settingsScope`), so neither needs a new RPC or any edit to core packages;
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 { CompactGuardButtonProps, ContextGuardInjected, ContextGuardSettingsCardInjected, ContextGuardSettingsCardProps, } from './slots.ts';
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
- /** Required services: the slot ledger, the command Remote, the settings scope, and the copy. */
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
- /** Required services: the slot ledger, the command Remote, the settings scope, and the copy. */
11
- export const inject = ['slots', 'remote', 'remote.commands', 'locale', 'settingsScope'];
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. While the settings surface is absent, the button falls back to the
23
- // composition-time values above.
24
- const scope = ctx.settingsScope.bind({ namespace: CONTEXT_GUARD_NS });
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 plugin configuration tab keys its cards on the settings namespace, so
50
- // the compact-timing card registers under CONTEXT_GUARD_NS and renders
51
- // wherever the tab dispatches that key.
52
- ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
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 card, both carrying
4
- * the shared `context-guard` settings scope as a hooks source so the button
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 { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
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: SettingsScope<ContextGuardConfig>;
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 the settings card: the same shared section, read and written here. */
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: SettingsScope<ContextGuardConfig>;
35
+ scope: GuardScope;
32
36
  hooks: {
33
37
  /** The shared `context-guard` settings section; the card binds `useConfig` to it. */
34
- config: SettingsScope<ContextGuardConfig>;
38
+ config: GuardScope;
35
39
  };
36
40
  }
37
- /** Full props of the settings.plugin.item card entry. */
38
- export type ContextGuardSettingsCardProps = PropsRuntime<'settings.plugin.item'> & InjectFace<ContextGuardSettingsCardInjected> & PropsLocale<'context-guard'>;
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