@khorsheed/dsh-context-guard 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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.en.md +89 -0
- package/README.i18n.yaml +6 -0
- package/README.md +89 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +461 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +50 -0
- package/lib/invariant.js +26 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/client/CompactGuardButton.d.ts +13 -0
- package/lib/types/client/CompactGuardButton.d.ts.map +1 -0
- package/lib/types/client/CompactGuardButton.js +53 -0
- package/lib/types/client/SettingsCard.d.ts +12 -0
- package/lib/types/client/SettingsCard.d.ts.map +1 -0
- package/lib/types/client/SettingsCard.js +97 -0
- package/lib/types/client/config.d.ts +42 -0
- package/lib/types/client/config.d.ts.map +1 -0
- package/lib/types/client/config.js +30 -0
- package/lib/types/client/guard.d.ts +44 -0
- package/lib/types/client/guard.d.ts.map +1 -0
- package/lib/types/client/guard.js +42 -0
- package/lib/types/client/index.d.ts +46 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +61 -0
- package/lib/types/client/locales.d.ts +48 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +39 -0
- package/lib/types/client/slots.d.ts +39 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/index.d.ts +19 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +24 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +24 -0
- package/lib/types/namespace.d.ts +9 -0
- package/lib/types/namespace.d.ts.map +1 -0
- package/lib/types/namespace.js +8 -0
- package/lib/types/settings.d.ts +20 -0
- package/lib/types/settings.d.ts.map +1 -0
- package/lib/types/settings.js +17 -0
- package/package.json +89 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@khorsheed/dsh-context-guard",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/namespace.ts
|
|
11
|
+
/**
|
|
12
|
+
* The settings namespace owned by the context-guard plugin. Plain string so
|
|
13
|
+
* both the host half (settings registration) and the browser half
|
|
14
|
+
* (settingsScope binding and the settings.plugin.item card key) import it
|
|
15
|
+
* without dragging host-only dependencies into the client bundle.
|
|
16
|
+
*/
|
|
17
|
+
/** Settings namespace owned by the context-guard plugin. */
|
|
18
|
+
const CONTEXT_GUARD_NS = "context-guard";
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/client/config.ts
|
|
21
|
+
/** Bounds of the deployment-tunable number (clamped in resolveConfig). */
|
|
22
|
+
const THRESHOLD_RATIO_MIN = .01;
|
|
23
|
+
const THRESHOLD_RATIO_MAX = 1;
|
|
24
|
+
function clamp(value, min, max) {
|
|
25
|
+
return Math.min(max, Math.max(min, value));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Normalize the entry config into the full {@link ContextGuardConfig}: every
|
|
29
|
+
* omitted field takes its documented default and every numeric field is
|
|
30
|
+
* clamped into its legal range.
|
|
31
|
+
* @param config - the unvalidated entry config, when the runner passes one.
|
|
32
|
+
* @returns the effective guard behavior.
|
|
33
|
+
*/
|
|
34
|
+
function resolveConfig(config) {
|
|
35
|
+
return { thresholdRatio: clamp(config?.thresholdRatio ?? .8, THRESHOLD_RATIO_MIN, THRESHOLD_RATIO_MAX) };
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/client/locales.ts
|
|
39
|
+
/** `context-guard` namespace dictionaries. */
|
|
40
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
41
|
+
const zh = {
|
|
42
|
+
"button.label": "压缩",
|
|
43
|
+
"button.aria": "上下文已用 {percent}%,点击压缩历史",
|
|
44
|
+
"button.error": "压缩未执行",
|
|
45
|
+
"settings.title": "压缩提醒时机",
|
|
46
|
+
"settings.description": "上下文占用达到设定比例时,输入框出现压缩按钮",
|
|
47
|
+
"settings.expand": "展开",
|
|
48
|
+
"settings.collapse": "收起",
|
|
49
|
+
"settings.field.threshold": "提醒比例",
|
|
50
|
+
"settings.field.threshold.hint": "上下文占用达到窗口的该比例时显示压缩按钮;可配置范围 0.01–1,按自己的偏好调整即可",
|
|
51
|
+
"settings.overridden": "已自定义",
|
|
52
|
+
"settings.reset": "重置",
|
|
53
|
+
"settings.save": "保存",
|
|
54
|
+
"settings.discard": "放弃",
|
|
55
|
+
"settings.saved": "已保存",
|
|
56
|
+
"settings.error": "保存失败,请重试"
|
|
57
|
+
};
|
|
58
|
+
/** The dictionary namespace owned by this plugin. */
|
|
59
|
+
const NS = "context-guard";
|
|
60
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
61
|
+
const en = {
|
|
62
|
+
"button.label": "compact",
|
|
63
|
+
"button.aria": "Context at {percent}%, click to compact history",
|
|
64
|
+
"button.error": "Compaction was not executed",
|
|
65
|
+
"settings.title": "Compaction reminder timing",
|
|
66
|
+
"settings.description": "Shows the compact button in the composer once context occupancy reaches the configured fraction",
|
|
67
|
+
"settings.expand": "Expand",
|
|
68
|
+
"settings.collapse": "Collapse",
|
|
69
|
+
"settings.field.threshold": "Reminder ratio",
|
|
70
|
+
"settings.field.threshold.hint": "Show the compact button once context occupancy reaches this fraction of the window; configurable from 0.01 to 1 — tune it to your preference",
|
|
71
|
+
"settings.overridden": "Overridden",
|
|
72
|
+
"settings.reset": "Reset",
|
|
73
|
+
"settings.save": "Save",
|
|
74
|
+
"settings.discard": "Discard",
|
|
75
|
+
"settings.saved": "Saved",
|
|
76
|
+
"settings.error": "Save failed, please retry"
|
|
77
|
+
};
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/client/guard.ts
|
|
80
|
+
/**
|
|
81
|
+
* Decide the guard level for one request budget.
|
|
82
|
+
*
|
|
83
|
+
* `overdue` means the occupancy already reached the whole window — the
|
|
84
|
+
* provider will have rejected requests before this point, so it is a
|
|
85
|
+
* defensive floor rather than a reachable state.
|
|
86
|
+
*
|
|
87
|
+
* @param input - the projection figures plus config.
|
|
88
|
+
* @returns the reading, or null when either the window or the context is unknown.
|
|
89
|
+
*/
|
|
90
|
+
function guardReading(input) {
|
|
91
|
+
const { contextWindow, projectedTokens, thresholdRatio } = input;
|
|
92
|
+
if (contextWindow === void 0 || !Number.isFinite(contextWindow) || contextWindow <= 0) return null;
|
|
93
|
+
if (!Number.isFinite(projectedTokens) || projectedTokens < 0) return null;
|
|
94
|
+
const ratio = projectedTokens / contextWindow;
|
|
95
|
+
return {
|
|
96
|
+
level: ratio >= 1 ? "overdue" : ratio >= thresholdRatio ? "warning" : "ok",
|
|
97
|
+
percent: Math.min(100, Math.round(ratio * 100)),
|
|
98
|
+
contextWindow
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region \0dsh-css:/Users/zhuyudan/code/dsh-plugins/packages/context-guard/src/client/CompactGuardButton.module.css.mjs
|
|
103
|
+
const css$1 = ".BFFw2a_wrap{align-items:center;gap:6px;display:inline-flex}.BFFw2a_button{cursor:pointer;border:none;border-radius:999px;align-items:center;gap:4px;padding:2px 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.BFFw2a_button:focus-visible{outline-offset:2px;outline:2px solid}.BFFw2a_button:disabled{opacity:.6;cursor:default}.BFFw2a_icon{color:currentColor;align-items:center;display:inline-flex}.BFFw2a_warning{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}.BFFw2a_warning:hover:not(:disabled){color:var(--dsw-alias-state-warn-primary)}.BFFw2a_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}";
|
|
104
|
+
const tagId$1 = "@khorsheed/dsh-context-guard/CompactGuardButton.module.css";
|
|
105
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
106
|
+
const tag = document.createElement("style");
|
|
107
|
+
tag.dataset.plugin = "@khorsheed/dsh-context-guard";
|
|
108
|
+
tag.dataset.pluginCss = tagId$1;
|
|
109
|
+
tag.textContent = css$1;
|
|
110
|
+
document.head.appendChild(tag);
|
|
111
|
+
}
|
|
112
|
+
var CompactGuardButton_module_css_default = {
|
|
113
|
+
"wrap": "BFFw2a_wrap",
|
|
114
|
+
"button": "BFFw2a_button",
|
|
115
|
+
"warning": "BFFw2a_warning",
|
|
116
|
+
"error": "BFFw2a_error",
|
|
117
|
+
"icon": "BFFw2a_icon"
|
|
118
|
+
};
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/client/CompactGuardButton.tsx
|
|
121
|
+
/**
|
|
122
|
+
* Composer-tool-row compact button. Renders nothing until the context
|
|
123
|
+
* occupancy (`projectedTokens / contextWindow` — the same number the
|
|
124
|
+
* composer's context ring shows) crosses the threshold; then it appears
|
|
125
|
+
* automatically in the amber warning tint. The threshold comes from the
|
|
126
|
+
* shared `context-guard` settings section (live), falling back to the
|
|
127
|
+
* composition-time value while the settings surface is absent. Clicking
|
|
128
|
+
* executes the official `/compact` command through the injected face.
|
|
129
|
+
*/
|
|
130
|
+
function CompactGuardButton({ useProjection, useConfig, t, thresholdRatio, compactNow }) {
|
|
131
|
+
const pressure = useProjection("contextPressure");
|
|
132
|
+
const config = useConfig((value) => value);
|
|
133
|
+
const effective = config.status === "ready" && config.value !== void 0 ? config.value : { thresholdRatio };
|
|
134
|
+
const reading = (0, react.useMemo)(() => guardReading({
|
|
135
|
+
projectedTokens: pressure?.projectedTokens ?? pressure?.pressureTokens ?? NaN,
|
|
136
|
+
contextWindow: pressure?.contextWindow,
|
|
137
|
+
thresholdRatio: effective.thresholdRatio
|
|
138
|
+
}), [pressure, effective.thresholdRatio]);
|
|
139
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
140
|
+
const [error, setError] = (0, react.useState)(null);
|
|
141
|
+
const aliveRef = (0, react.useRef)(true);
|
|
142
|
+
(0, react.useEffect)(() => {
|
|
143
|
+
aliveRef.current = true;
|
|
144
|
+
return () => {
|
|
145
|
+
aliveRef.current = false;
|
|
146
|
+
};
|
|
147
|
+
}, []);
|
|
148
|
+
if (reading === null || reading.level === "ok") return null;
|
|
149
|
+
const run = () => {
|
|
150
|
+
setBusy(true);
|
|
151
|
+
setError(null);
|
|
152
|
+
compactNow().then((failure) => {
|
|
153
|
+
if (!aliveRef.current) return;
|
|
154
|
+
setBusy(false);
|
|
155
|
+
setError(failure);
|
|
156
|
+
}, (reason) => {
|
|
157
|
+
if (!aliveRef.current) return;
|
|
158
|
+
setBusy(false);
|
|
159
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
163
|
+
className: CompactGuardButton_module_css_default.wrap,
|
|
164
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
165
|
+
type: "button",
|
|
166
|
+
className: `${CompactGuardButton_module_css_default.button} ${CompactGuardButton_module_css_default.warning}`,
|
|
167
|
+
"aria-label": t("button.aria", { percent: String(reading.percent) }),
|
|
168
|
+
disabled: busy,
|
|
169
|
+
onClick: run,
|
|
170
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
171
|
+
className: CompactGuardButton_module_css_default.icon,
|
|
172
|
+
"aria-hidden": true,
|
|
173
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, { size: 14 })
|
|
174
|
+
}), t("button.label")]
|
|
175
|
+
}), error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
176
|
+
className: CompactGuardButton_module_css_default.error,
|
|
177
|
+
role: "status",
|
|
178
|
+
title: error,
|
|
179
|
+
children: t("button.error")
|
|
180
|
+
})]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region \0dsh-css:/Users/zhuyudan/code/dsh-plugins/packages/context-guard/src/client/SettingsCard.module.css.mjs
|
|
185
|
+
const css = ".DKjZXq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.DKjZXq_card:hover{border-color:var(--dsw-alias-label-dimmed)}.DKjZXq_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.DKjZXq_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.DKjZXq_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.DKjZXq_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.DKjZXq_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.DKjZXq_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.DKjZXq_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.DKjZXq_chevronOpen{transform:rotate(180deg)}.DKjZXq_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:12px;margin:0 16px;padding:12px 0 8px;display:flex}.DKjZXq_field{flex-direction:column;gap:6px;display:flex}.DKjZXq_fieldHead{align-items:center;gap:8px;display:flex}.DKjZXq_fieldName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.4}.DKjZXq_badge{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);border-radius:999px;padding:2px 6px;font-size:11px;line-height:1}.DKjZXq_inputRow{align-items:center;gap:8px;display:flex}.DKjZXq_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:140px;color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 8px;font-size:13px;line-height:1.4}.DKjZXq_input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}.DKjZXq_inputInvalid{border-color:var(--dsw-alias-state-error-primary)}.DKjZXq_reset{appearance:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;padding:2px 6px;font-size:12px;line-height:1.4}.DKjZXq_reset:hover{color:var(--dsw-alias-label-primary)}.DKjZXq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.DKjZXq_footer{justify-content:flex-end;align-items:center;gap:8px;margin-top:4px;display:flex}.DKjZXq_status{flex:1;min-width:0}.DKjZXq_saved{color:var(--dsw-alias-state-success-primary);font-size:12px}.DKjZXq_errorText{color:var(--dsw-alias-state-error-primary);font-size:12px}.DKjZXq_secondary,.DKjZXq_save{appearance:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 12px;font-size:13px;line-height:1.4}.DKjZXq_secondary:disabled,.DKjZXq_save:disabled{opacity:.5;cursor:default}.DKjZXq_save{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.DKjZXq_save:hover:not(:disabled){filter:brightness(.95)}";
|
|
186
|
+
const tagId = "@khorsheed/dsh-context-guard/SettingsCard.module.css";
|
|
187
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
188
|
+
const tag = document.createElement("style");
|
|
189
|
+
tag.dataset.plugin = "@khorsheed/dsh-context-guard";
|
|
190
|
+
tag.dataset.pluginCss = tagId;
|
|
191
|
+
tag.textContent = css;
|
|
192
|
+
document.head.appendChild(tag);
|
|
193
|
+
}
|
|
194
|
+
var SettingsCard_module_css_default = {
|
|
195
|
+
"hint": "DKjZXq_hint",
|
|
196
|
+
"cardOpen": "DKjZXq_cardOpen",
|
|
197
|
+
"save": "DKjZXq_save",
|
|
198
|
+
"secondary": "DKjZXq_secondary",
|
|
199
|
+
"description": "DKjZXq_description",
|
|
200
|
+
"fieldName": "DKjZXq_fieldName",
|
|
201
|
+
"header": "DKjZXq_header",
|
|
202
|
+
"fieldHead": "DKjZXq_fieldHead",
|
|
203
|
+
"inputInvalid": "DKjZXq_inputInvalid",
|
|
204
|
+
"name": "DKjZXq_name",
|
|
205
|
+
"chevron": "DKjZXq_chevron",
|
|
206
|
+
"chevronOpen": "DKjZXq_chevronOpen",
|
|
207
|
+
"status": "DKjZXq_status",
|
|
208
|
+
"card": "DKjZXq_card",
|
|
209
|
+
"saved": "DKjZXq_saved",
|
|
210
|
+
"body": "DKjZXq_body",
|
|
211
|
+
"headText": "DKjZXq_headText",
|
|
212
|
+
"inputRow": "DKjZXq_inputRow",
|
|
213
|
+
"input": "DKjZXq_input",
|
|
214
|
+
"reset": "DKjZXq_reset",
|
|
215
|
+
"badge": "DKjZXq_badge",
|
|
216
|
+
"footer": "DKjZXq_footer",
|
|
217
|
+
"errorText": "DKjZXq_errorText",
|
|
218
|
+
"field": "DKjZXq_field"
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/client/SettingsCard.tsx
|
|
222
|
+
/** Parse a draft into a number, or NaN when it is not one. */
|
|
223
|
+
function parseNumber(text) {
|
|
224
|
+
const value = Number(text.trim());
|
|
225
|
+
return Number.isFinite(value) ? value : NaN;
|
|
226
|
+
}
|
|
227
|
+
/** Whether the user layer carries a field (presence marks an override). */
|
|
228
|
+
function overridden(snapshot, field) {
|
|
229
|
+
return snapshot.user !== void 0 && typeof snapshot.user === "object" && snapshot.user !== null && field in snapshot.user;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The context-guard settings card in the plugin configuration tab: the same
|
|
233
|
+
* collapsible chrome the official plugin cards use (a header naming the
|
|
234
|
+
* plugin and what its settings govern, disclosing the controls in place),
|
|
235
|
+
* re-implemented here because the client bundle-purity gate forbids
|
|
236
|
+
* value-importing the official card chrome. Two staged number fields write
|
|
237
|
+
* through the shared settingsScope on save; per-field reset reverts a field
|
|
238
|
+
* to the composition layer.
|
|
239
|
+
*/
|
|
240
|
+
function ContextGuardSettingsCard({ useConfig, scope, t }) {
|
|
241
|
+
const snapshot = useConfig((value) => value);
|
|
242
|
+
const value = snapshot.status === "ready" ? snapshot.value : void 0;
|
|
243
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
244
|
+
const [threshold, setThreshold] = (0, react.useState)({
|
|
245
|
+
text: "",
|
|
246
|
+
invalid: false
|
|
247
|
+
});
|
|
248
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
249
|
+
const [error, setError] = (0, react.useState)(false);
|
|
250
|
+
const aliveRef = (0, react.useRef)(true);
|
|
251
|
+
/** The section revision the drafts were last seeded from; re-seed only on a committed change. */
|
|
252
|
+
const seededRevision = (0, react.useRef)(null);
|
|
253
|
+
(0, react.useEffect)(() => {
|
|
254
|
+
aliveRef.current = true;
|
|
255
|
+
if (value === void 0) return;
|
|
256
|
+
if (seededRevision.current === snapshot.revision) return;
|
|
257
|
+
seededRevision.current = snapshot.revision ?? null;
|
|
258
|
+
setThreshold({
|
|
259
|
+
text: String(value.thresholdRatio),
|
|
260
|
+
invalid: false
|
|
261
|
+
});
|
|
262
|
+
setSaved(false);
|
|
263
|
+
setError(false);
|
|
264
|
+
return () => {
|
|
265
|
+
aliveRef.current = false;
|
|
266
|
+
};
|
|
267
|
+
}, [value, snapshot.revision]);
|
|
268
|
+
if (snapshot.status === "unavailable") return null;
|
|
269
|
+
const write = (draft, min, max) => {
|
|
270
|
+
const parsed = parseNumber(draft.text);
|
|
271
|
+
setThreshold({
|
|
272
|
+
text: draft.text,
|
|
273
|
+
invalid: !Number.isFinite(parsed) || parsed < min || parsed > max
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
const save = () => {
|
|
277
|
+
if (threshold.invalid || snapshot.writable !== true) return;
|
|
278
|
+
const ratio = parseNumber(threshold.text);
|
|
279
|
+
if (!Number.isFinite(ratio)) return;
|
|
280
|
+
setError(false);
|
|
281
|
+
setSaved(false);
|
|
282
|
+
scope.set("thresholdRatio", ratio).then(() => {
|
|
283
|
+
if (!aliveRef.current) return;
|
|
284
|
+
setSaved(true);
|
|
285
|
+
}, () => {
|
|
286
|
+
if (!aliveRef.current) return;
|
|
287
|
+
setError(true);
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
const reset = () => {
|
|
291
|
+
setError(false);
|
|
292
|
+
setSaved(false);
|
|
293
|
+
scope.unset("thresholdRatio").catch(() => {
|
|
294
|
+
if (aliveRef.current) setError(true);
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
const title = t("settings.title");
|
|
298
|
+
const dirty = threshold.text !== String(value?.thresholdRatio ?? "");
|
|
299
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
300
|
+
className: open ? `${SettingsCard_module_css_default.card} ${SettingsCard_module_css_default.cardOpen}` : SettingsCard_module_css_default.card,
|
|
301
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
302
|
+
type: "button",
|
|
303
|
+
className: SettingsCard_module_css_default.header,
|
|
304
|
+
"aria-expanded": open,
|
|
305
|
+
"aria-label": `${t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
|
|
306
|
+
onClick: () => {
|
|
307
|
+
setOpen(!open);
|
|
308
|
+
},
|
|
309
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
310
|
+
className: SettingsCard_module_css_default.headText,
|
|
311
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
312
|
+
className: SettingsCard_module_css_default.name,
|
|
313
|
+
children: title
|
|
314
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
315
|
+
className: SettingsCard_module_css_default.description,
|
|
316
|
+
children: t("settings.description")
|
|
317
|
+
})]
|
|
318
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? `${SettingsCard_module_css_default.chevron} ${SettingsCard_module_css_default.chevronOpen}` : SettingsCard_module_css_default.chevron })]
|
|
319
|
+
}), open && value !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
320
|
+
className: SettingsCard_module_css_default.body,
|
|
321
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
322
|
+
className: SettingsCard_module_css_default.field,
|
|
323
|
+
children: [
|
|
324
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
325
|
+
className: SettingsCard_module_css_default.fieldHead,
|
|
326
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
327
|
+
className: SettingsCard_module_css_default.fieldName,
|
|
328
|
+
children: t("settings.field.threshold")
|
|
329
|
+
}), overridden(snapshot, "thresholdRatio") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
330
|
+
className: SettingsCard_module_css_default.badge,
|
|
331
|
+
children: t("settings.overridden")
|
|
332
|
+
})]
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
335
|
+
className: SettingsCard_module_css_default.inputRow,
|
|
336
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
337
|
+
className: threshold.invalid ? `${SettingsCard_module_css_default.input} ${SettingsCard_module_css_default.inputInvalid}` : SettingsCard_module_css_default.input,
|
|
338
|
+
type: "number",
|
|
339
|
+
step: "0.01",
|
|
340
|
+
min: "0.01",
|
|
341
|
+
max: "1",
|
|
342
|
+
value: threshold.text,
|
|
343
|
+
"aria-label": t("settings.field.threshold"),
|
|
344
|
+
onChange: (event) => {
|
|
345
|
+
write({
|
|
346
|
+
text: event.target.value,
|
|
347
|
+
invalid: false
|
|
348
|
+
}, .01, 1);
|
|
349
|
+
}
|
|
350
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
351
|
+
type: "button",
|
|
352
|
+
className: SettingsCard_module_css_default.reset,
|
|
353
|
+
onClick: reset,
|
|
354
|
+
children: t("settings.reset")
|
|
355
|
+
})]
|
|
356
|
+
}),
|
|
357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
358
|
+
className: SettingsCard_module_css_default.hint,
|
|
359
|
+
children: t("settings.field.threshold.hint")
|
|
360
|
+
})
|
|
361
|
+
]
|
|
362
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
363
|
+
className: SettingsCard_module_css_default.footer,
|
|
364
|
+
children: [
|
|
365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
366
|
+
className: SettingsCard_module_css_default.status,
|
|
367
|
+
children: [saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
368
|
+
className: SettingsCard_module_css_default.saved,
|
|
369
|
+
children: t("settings.saved")
|
|
370
|
+
}) : null, error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
371
|
+
className: SettingsCard_module_css_default.errorText,
|
|
372
|
+
children: t("settings.error")
|
|
373
|
+
}) : null]
|
|
374
|
+
}),
|
|
375
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
376
|
+
type: "button",
|
|
377
|
+
className: SettingsCard_module_css_default.secondary,
|
|
378
|
+
disabled: !dirty,
|
|
379
|
+
onClick: () => {
|
|
380
|
+
if (value === void 0) return;
|
|
381
|
+
setThreshold({
|
|
382
|
+
text: String(value.thresholdRatio),
|
|
383
|
+
invalid: false
|
|
384
|
+
});
|
|
385
|
+
setSaved(false);
|
|
386
|
+
setError(false);
|
|
387
|
+
},
|
|
388
|
+
children: t("settings.discard")
|
|
389
|
+
}),
|
|
390
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
391
|
+
type: "button",
|
|
392
|
+
className: SettingsCard_module_css_default.save,
|
|
393
|
+
disabled: !dirty || threshold.invalid || snapshot.writable !== true,
|
|
394
|
+
onClick: save,
|
|
395
|
+
children: t("settings.save")
|
|
396
|
+
})
|
|
397
|
+
]
|
|
398
|
+
})]
|
|
399
|
+
})]
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/client/index.ts
|
|
404
|
+
/** Required services: the slot ledger, the command Remote, the settings scope, and the copy. */
|
|
405
|
+
const inject = [
|
|
406
|
+
"slots",
|
|
407
|
+
"remote",
|
|
408
|
+
"remote.commands",
|
|
409
|
+
"locale",
|
|
410
|
+
"settingsScope"
|
|
411
|
+
];
|
|
412
|
+
/**
|
|
413
|
+
* Client plugin body: register the composer-tool-row compact button and the
|
|
414
|
+
* settings card over the shared `context-guard` section.
|
|
415
|
+
* @param ctx - client root context.
|
|
416
|
+
* @param config - entry config (the section's composition base layer); defaults apply when the runner passes none.
|
|
417
|
+
*/
|
|
418
|
+
function apply(ctx, config) {
|
|
419
|
+
const fallback = resolveConfig(config);
|
|
420
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
421
|
+
zh,
|
|
422
|
+
en
|
|
423
|
+
}), "context-guard: dictionaries");
|
|
424
|
+
const scope = ctx.settingsScope.bind({ namespace: CONTEXT_GUARD_NS });
|
|
425
|
+
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
426
|
+
name: "conversation.input.right",
|
|
427
|
+
id: "context-guard",
|
|
428
|
+
order: -10,
|
|
429
|
+
locale: NS,
|
|
430
|
+
inject: (sessionId) => ({
|
|
431
|
+
thresholdRatio: fallback.thresholdRatio,
|
|
432
|
+
compactNow: async () => {
|
|
433
|
+
const result = await ctx.remote.commands.execute(sessionId, "/compact", []);
|
|
434
|
+
if (!result.ok) return `${result.error.message} (${result.error.code})`;
|
|
435
|
+
if (result.value === void 0) return "unknown command: /compact";
|
|
436
|
+
return null;
|
|
437
|
+
},
|
|
438
|
+
hooks: { config: scope }
|
|
439
|
+
})
|
|
440
|
+
}, CompactGuardButton));
|
|
441
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
442
|
+
name: "settings.plugin.item",
|
|
443
|
+
key: CONTEXT_GUARD_NS,
|
|
444
|
+
locale: NS,
|
|
445
|
+
inject: () => ({
|
|
446
|
+
scope,
|
|
447
|
+
hooks: { config: scope }
|
|
448
|
+
})
|
|
449
|
+
}, ContextGuardSettingsCard));
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
exports.NS = NS;
|
|
453
|
+
exports.apply = apply;
|
|
454
|
+
exports.guardReading = guardReading;
|
|
455
|
+
exports.inject = inject;
|
|
456
|
+
exports.resolveConfig = resolveConfig;
|
|
457
|
+
return module.exports;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["useMemo","useState","useRef","css","IconWarningOutline16","useState","useRef","css","IconChevronDownOutline14"],"sources":["../src/namespace.ts","../src/client/config.ts","../src/client/locales.ts","../src/client/guard.ts","../src/client/CompactGuardButton.tsx","../src/client/SettingsCard.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * The settings namespace owned by the context-guard plugin. Plain string so\n * both the host half (settings registration) and the browser half\n * (settingsScope binding and the settings.plugin.item card key) import it\n * without dragging host-only dependencies into the client bundle.\n */\n\n/** Settings namespace owned by the context-guard plugin. */\nexport const CONTEXT_GUARD_NS = 'context-guard'\n","/**\n * Browser-half configuration of the context-guard plugin. The runner hands\n * the plugin its validated entry config through the apply second parameter;\n * this module supplies the defaults and clamps, so a composition that does\n * not pass config still renders with the documented values and out-of-range\n * input lands inside the legal bounds.\n *\n * Once the settings surface is composed, these values arrive through the\n * `context-guard` settings section instead: the composition entry becomes the\n * section's base layer, and the settings card overrides it per field. The\n * fallback path here only serves deployments without the settings surface.\n */\n\n/**\n * Deployment-tunable guard behavior.\n *\n * The guard is a reminder: when the context OCCUPANCY (the same\n * `projectedTokens / contextWindow` number the composer's context ring shows)\n * crosses `thresholdRatio`, the compact button appears. The official\n * compaction engine auto-compacts at 80% of the window on its own; the real\n * provider rejection wall is `window − maxTokens` (the request reserves\n * output), so a request can fail before occupancy alone reaches 80% — set a\n * lower threshold for an earlier reminder. The field tunes ONLY when the\n * button appears; real compaction timing (compaction-basic's own config) is\n * never affected.\n */\nexport interface ContextGuardConfig {\n /**\n * Context occupancy fraction at which the button appears (clamped to\n * (0, 1]; default 0.8, matching compaction-basic's default pressure\n * threshold).\n */\n thresholdRatio: number\n}\n\n/** Bounds of the deployment-tunable number (clamped in resolveConfig). */\nconst THRESHOLD_RATIO_MIN = 0.01\nconst THRESHOLD_RATIO_MAX = 1\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\n/**\n * Normalize the entry config into the full {@link ContextGuardConfig}: every\n * omitted field takes its documented default and every numeric field is\n * clamped into its legal range.\n * @param config - the unvalidated entry config, when the runner passes one.\n * @returns the effective guard behavior.\n */\nexport function resolveConfig(config: Partial<ContextGuardConfig> | undefined): ContextGuardConfig {\n return {\n thresholdRatio: clamp(\n config?.thresholdRatio ?? 0.8,\n THRESHOLD_RATIO_MIN,\n THRESHOLD_RATIO_MAX,\n ),\n }\n}\n","/** `context-guard` namespace dictionaries. */\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'button.label': '压缩',\n 'button.aria': '上下文已用 {percent}%,点击压缩历史',\n 'button.error': '压缩未执行',\n 'settings.title': '压缩提醒时机',\n 'settings.description': '上下文占用达到设定比例时,输入框出现压缩按钮',\n 'settings.expand': '展开',\n 'settings.collapse': '收起',\n 'settings.field.threshold': '提醒比例',\n 'settings.field.threshold.hint': '上下文占用达到窗口的该比例时显示压缩按钮;可配置范围 0.01–1,按自己的偏好调整即可',\n 'settings.overridden': '已自定义',\n 'settings.reset': '重置',\n 'settings.save': '保存',\n 'settings.discard': '放弃',\n 'settings.saved': '已保存',\n 'settings.error': '保存失败,请重试',\n} satisfies Record<string, string>\n\n/** The context-guard namespace key union. */\nexport type ContextGuardKey = keyof typeof zh\n\n/** The dictionary namespace owned by this plugin. */\nexport const NS = 'context-guard'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The context-guard compact button's copy. */\n 'context-guard': ContextGuardKey\n }\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en = {\n 'button.label': 'compact',\n 'button.aria': 'Context at {percent}%, click to compact history',\n 'button.error': 'Compaction was not executed',\n 'settings.title': 'Compaction reminder timing',\n 'settings.description': 'Shows the compact button in the composer once context occupancy reaches the configured fraction',\n 'settings.expand': 'Expand',\n 'settings.collapse': 'Collapse',\n 'settings.field.threshold': 'Reminder ratio',\n 'settings.field.threshold.hint': 'Show the compact button once context occupancy reaches this fraction of the window; configurable from 0.01 to 1 — tune it to your preference',\n 'settings.overridden': 'Overridden',\n 'settings.reset': 'Reset',\n 'settings.save': 'Save',\n 'settings.discard': 'Discard',\n 'settings.saved': 'Saved',\n 'settings.error': 'Save failed, please retry',\n} satisfies Record<string, string>\n","/**\n * Pure guard decision math: whether the next request's prompt-plus-output\n * budget is about to overflow the model's context window, mirroring the\n * provider-side rejection rule (`prompt + max_tokens > context_length`).\n *\n * The numerator rides the official `contextPressure` projection so it answers\n * for the NEXT request rather than the last one: `projectedTokens` is the\n * provider-reported prompt sample carried forward over the surface's signed\n * movement since (compaction included), with the bare sample as the fallback\n * for logs whose projection predates that field. The occupancy is the SAME\n * number the composer's context ring shows — the guard is a reminder on top\n * of that ring, not a second figure.\n */\n\n/** The three guard states: nothing to show, about to overflow, already over. */\nexport type GuardLevel = 'ok' | 'warning' | 'overdue'\n\n/** Inputs to the guard decision; both come from the session's contextPressure projection plus config. */\nexport interface GuardInput {\n /** Current projected context tokens (projectedTokens ?? pressureTokens). */\n projectedTokens: number\n /** The routed model's advertised context window; absent until a route reports one. */\n contextWindow: number | undefined\n /** The occupancy fraction at which the guard turns on (clamped (0, 1] by config). */\n thresholdRatio: number\n}\n\n/** One guard reading: the level plus the occupancy figure behind it. */\nexport interface GuardReading {\n level: GuardLevel\n /** `projectedTokens / contextWindow`, rounded, clamped to 0–100 — the same occupancy the context ring shows. */\n percent: number\n /** The routed model's advertised context window. */\n contextWindow: number\n}\n\n/**\n * Decide the guard level for one request budget.\n *\n * `overdue` means the occupancy already reached the whole window — the\n * provider will have rejected requests before this point, so it is a\n * defensive floor rather than a reachable state.\n *\n * @param input - the projection figures plus config.\n * @returns the reading, or null when either the window or the context is unknown.\n */\nexport function guardReading(input: GuardInput): GuardReading | null {\n const { contextWindow, projectedTokens, thresholdRatio } = input\n if (contextWindow === undefined || !Number.isFinite(contextWindow) || contextWindow <= 0) {\n return null\n }\n if (!Number.isFinite(projectedTokens) || projectedTokens < 0) return null\n const ratio = projectedTokens / contextWindow\n const level: GuardLevel = ratio >= 1\n ? 'overdue'\n : ratio >= thresholdRatio\n ? 'warning'\n : 'ok'\n return {\n level,\n percent: Math.min(100, Math.round(ratio * 100)),\n contextWindow,\n }\n}\n","import { useEffect, useMemo, useRef, useState } from 'react'\nimport { IconWarningOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'\n// Type-only: pulls ui-conversation's SlotMap merge (the input.right seat and\n// its InputZone owner share).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls the `contextPressure` SessionProjectionMap merge for\n// useProjection.\nimport type {} from '@deepseek-ai/dsh-token-meter/client'\nimport { guardReading } from './guard.ts'\nimport type { CompactGuardButtonProps } from './slots.ts'\nimport css from './CompactGuardButton.module.css'\n\nexport type { CompactGuardButtonProps } from './slots.ts'\n\n/**\n * Composer-tool-row compact button. Renders nothing until the context\n * occupancy (`projectedTokens / contextWindow` — the same number the\n * composer's context ring shows) crosses the threshold; then it appears\n * automatically in the amber warning tint. The threshold comes from the\n * shared `context-guard` settings section (live), falling back to the\n * composition-time value while the settings surface is absent. Clicking\n * executes the official `/compact` command through the injected face.\n */\nexport function CompactGuardButton({\n useProjection,\n useConfig,\n t,\n thresholdRatio,\n compactNow,\n}: CompactGuardButtonProps) {\n const pressure = useProjection('contextPressure')\n const config = useConfig(value => value)\n const effective = config.status === 'ready' && config.value !== undefined\n ? config.value\n : { thresholdRatio }\n const reading = useMemo(() => guardReading({\n projectedTokens: pressure?.projectedTokens ?? pressure?.pressureTokens ?? Number.NaN,\n contextWindow: pressure?.contextWindow,\n thresholdRatio: effective.thresholdRatio,\n }), [pressure, effective.thresholdRatio])\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const aliveRef = useRef(true)\n\n useEffect(() => {\n aliveRef.current = true\n return () => {\n aliveRef.current = false\n }\n }, [])\n\n if (reading === null || reading.level === 'ok') return null\n\n const run = (): void => {\n setBusy(true)\n setError(null)\n void compactNow().then((failure) => {\n if (!aliveRef.current) return\n setBusy(false)\n setError(failure)\n }, (reason: unknown) => {\n if (!aliveRef.current) return\n setBusy(false)\n setError(reason instanceof Error ? reason.message : String(reason))\n })\n }\n\n return (\n <span className={css.wrap}>\n <button\n type=\"button\"\n className={`${css.button} ${css.warning}`}\n aria-label={t('button.aria', { percent: String(reading.percent) })}\n disabled={busy}\n onClick={run}\n >\n <span className={css.icon} aria-hidden>\n <IconWarningOutline16 size={14} />\n </span>\n {t('button.label')}\n </button>\n {/* Failure copy stays English (error-surface policy: not localized). */}\n {error !== null && (\n <span className={css.error} role=\"status\" title={error}>\n {t('button.error')}\n </span>\n )}\n </span>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ContextGuardConfig } from './config.ts'\nimport type { ContextGuardSettingsCardProps } from './slots.ts'\nimport css from './SettingsCard.module.css'\n\n/** One number field's staged draft, its validity, and the reset/save actions. */\ninterface FieldDraft {\n text: string\n invalid: boolean\n}\n\n/** Parse a draft into a number, or NaN when it is not one. */\nfunction parseNumber(text: string): number {\n const value = Number(text.trim())\n return Number.isFinite(value) ? value : Number.NaN\n}\n\n/** Whether the user layer carries a field (presence marks an override). */\nfunction overridden(snapshot: SettingsScopeSnapshot<ContextGuardConfig>, field: keyof ContextGuardConfig): boolean {\n return snapshot.user !== undefined\n && typeof snapshot.user === 'object'\n && snapshot.user !== null\n && field in snapshot.user\n}\n\n/**\n * The context-guard settings card in the plugin configuration tab: the same\n * collapsible chrome the official plugin cards use (a header naming the\n * plugin and what its settings govern, disclosing the controls in place),\n * re-implemented here because the client bundle-purity gate forbids\n * value-importing the official card chrome. Two staged number fields write\n * through the shared settingsScope on save; per-field reset reverts a field\n * to the composition layer.\n */\nexport function ContextGuardSettingsCard({ useConfig, scope, t }: ContextGuardSettingsCardProps) {\n const snapshot = useConfig(value => value)\n const value = snapshot.status === 'ready' ? snapshot.value : undefined\n const [open, setOpen] = useState(false)\n const [threshold, setThreshold] = useState<FieldDraft>({ text: '', invalid: false })\n const [saved, setSaved] = useState(false)\n const [error, setError] = useState(false)\n const aliveRef = useRef(true)\n /** The section revision the drafts were last seeded from; re-seed only on a committed change. */\n const seededRevision = useRef<number | null>(null)\n\n // Re-seed the drafts whenever the section VALUE changes (first load, a save\n // settling, an external reset). Keyed on the snapshot revision rather than\n // the value's object identity: a save settling publishes a new snapshot\n // (revision bump), while an unrelated re-render must not discard a draft in\n // flight — the staged model promises the screen shows exactly what a save\n // would store.\n useEffect(() => {\n aliveRef.current = true\n if (value === undefined) return\n if (seededRevision.current === snapshot.revision) return\n seededRevision.current = snapshot.revision ?? null\n setThreshold({ text: String(value.thresholdRatio), invalid: false })\n setSaved(false)\n setError(false)\n return () => {\n aliveRef.current = false\n }\n }, [value, snapshot.revision])\n\n if (snapshot.status === 'unavailable') return null\n\n const write = (draft: FieldDraft, min: number, max: number): void => {\n const parsed = parseNumber(draft.text)\n setThreshold({ text: draft.text, invalid: !Number.isFinite(parsed) || parsed < min || parsed > max })\n }\n\n const save = (): void => {\n if (threshold.invalid || snapshot.writable !== true) return\n const ratio = parseNumber(threshold.text)\n if (!Number.isFinite(ratio)) return\n setError(false)\n setSaved(false)\n void scope.set('thresholdRatio', ratio).then(() => {\n if (!aliveRef.current) return\n setSaved(true)\n }, () => {\n if (!aliveRef.current) return\n setError(true)\n })\n }\n\n const reset = (): void => {\n setError(false)\n setSaved(false)\n void scope.unset('thresholdRatio').catch(() => {\n if (aliveRef.current) setError(true)\n })\n }\n\n const title = t('settings.title')\n const dirty = threshold.text !== String(value?.thresholdRatio ?? '')\n\n return (\n <li className={open ? `${css.card} ${css.cardOpen}` : css.card}>\n <button\n type=\"button\"\n className={css.header}\n aria-expanded={open}\n aria-label={`${t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}\n onClick={() => { setOpen(!open) }}\n >\n <span className={css.headText}>\n <span className={css.name}>{title}</span>\n <span className={css.description}>{t('settings.description')}</span>\n </span>\n <IconChevronDownOutline14 className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron} />\n </button>\n {open && value !== undefined && (\n <div className={css.body}>\n <label className={css.field}>\n <span className={css.fieldHead}>\n <span className={css.fieldName}>{t('settings.field.threshold')}</span>\n {overridden(snapshot, 'thresholdRatio') && <span className={css.badge}>{t('settings.overridden')}</span>}\n </span>\n <span className={css.inputRow}>\n <input\n className={threshold.invalid ? `${css.input} ${css.inputInvalid}` : css.input}\n type=\"number\"\n step=\"0.01\"\n min=\"0.01\"\n max=\"1\"\n value={threshold.text}\n aria-label={t('settings.field.threshold')}\n onChange={(event) => { write({ text: event.target.value, invalid: false }, 0.01, 1) }}\n />\n <button type=\"button\" className={css.reset} onClick={reset}>\n {t('settings.reset')}\n </button>\n </span>\n <span className={css.hint}>{t('settings.field.threshold.hint')}</span>\n </label>\n <span className={css.footer}>\n <span className={css.status}>\n {saved ? <span className={css.saved}>{t('settings.saved')}</span> : null}\n {error ? <span className={css.errorText}>{t('settings.error')}</span> : null}\n </span>\n <button\n type=\"button\"\n className={css.secondary}\n disabled={!dirty}\n onClick={() => {\n if (value === undefined) return\n setThreshold({ text: String(value.thresholdRatio), invalid: false })\n setSaved(false)\n setError(false)\n }}\n >\n {t('settings.discard')}\n </button>\n <button\n type=\"button\"\n className={css.save}\n disabled={!dirty || threshold.invalid || snapshot.writable !== true}\n onClick={save}\n >\n {t('settings.save')}\n </button>\n </span>\n </div>\n )}\n </li>\n )\n}\n","/**\n * Context-guard plugin, browser half: contributes one\n * `conversation.input.right` entry — a compact button inside the composer's\n * tool row that appears automatically when the next request's budget\n * (`contextPressure.projectedTokens` + the output budget) crosses\n * `thresholdRatio` of the routed model's context window — plus one\n * `settings.plugin.item` card in the plugin configuration tab that edits the\n * same two numbers live. The action rides the official `/compact` command\n * channel (`remote.commands.execute` → host `ctx.commands` →\n * `ctx.compaction.compactNow`); the config rides the official settings\n * surface (host half registers the namespace, this half binds its\n * `settingsScope`), so neither needs a new RPC or any edit to core packages;\n * composing this plugin out of cordis.yml removes every surface it adds.\n *\n * Why this exists: the official auto-compaction fires at `agent/pre-step`\n * when the meter's context estimate crosses 80% of the window — a check that\n * excludes the request's own output budget and rides a heuristic that\n * deliberately underprices CJK/JSON. A long request can therefore push\n * context + maxTokens past the window while the estimate still sits below\n * 80%, so the provider rejects the request (CONTEXT_WINDOW_EXCEEDED). The\n * guard surfaces the danger while a manual compaction still fits — the\n * summarization call reserves only its own small output cap, so it keeps\n * running after the main request is rejected.\n * @module @khorsheed/dsh-context-guard/client\n */\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the typed `ctx.remote` (commands namespace) merge.\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the ctx.locale service merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls ui-conversation's SlotMap merge\n// ('conversation.input.right').\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: pulls the ctx.settingsScope service merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\n// Type-only: pulls ui-settings-plugins' SlotMap merge\n// ('settings.plugin.item').\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'\n// Type-only: pulls the `contextPressure` SessionProjectionMap merge for\n// useProjection.\nimport type {} from '@deepseek-ai/dsh-token-meter/client'\nimport { CONTEXT_GUARD_NS } from '../namespace.ts'\nimport { resolveConfig, type ContextGuardConfig } from './config.ts'\nimport { en, NS, zh } from './locales.ts'\nimport type { ContextGuardInjected, ContextGuardSettingsCardInjected } from './slots.ts'\nimport { CompactGuardButton } from './CompactGuardButton.tsx'\nimport { ContextGuardSettingsCard } from './SettingsCard.tsx'\n\nexport type { ContextGuardConfig } from './config.ts'\nexport { resolveConfig } from './config.ts'\nexport type { ContextGuardKey } from './locales.ts'\nexport { guardReading } from './guard.ts'\nexport type { GuardInput, GuardLevel, GuardReading } from './guard.ts'\nexport type {\n CompactGuardButtonProps, ContextGuardInjected, ContextGuardSettingsCardInjected, ContextGuardSettingsCardProps,\n} from './slots.ts'\n\n/** Dictionary namespace owned by this plugin. */\nexport { NS }\n\n/** Required services: the slot ledger, the command Remote, the settings scope, and the copy. */\nexport const inject = ['slots', 'remote', 'remote.commands', 'locale', 'settingsScope']\n\n/**\n * Client plugin body: register the composer-tool-row compact button and the\n * settings card over the shared `context-guard` section.\n * @param ctx - client root context.\n * @param config - entry config (the section's composition base layer); defaults apply when the runner passes none.\n */\nexport function apply(ctx: ClientContext, config?: Partial<ContextGuardConfig>): void {\n const fallback = resolveConfig(config)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'context-guard: dictionaries')\n\n // One shared live section: the settings card writes it, the button reads\n // it. While the settings surface is absent, the button falls back to the\n // composition-time values above.\n const scope = ctx.settingsScope.bind<ContextGuardConfig>({ namespace: CONTEXT_GUARD_NS })\n\n // The slot is declared by ui-conversation, whose apply order relative to\n // this plugin is unconstrained: register through slots.inject so the entry\n // waits for the declaration instead of crashing the loader at boot.\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'context-guard',\n // Sit before the primary send button; the seat currently has no other\n // occupants, and a leading position keeps the danger visible.\n order: -10,\n locale: NS,\n inject: (sessionId: SessionId): ContextGuardInjected => ({\n thresholdRatio: fallback.thresholdRatio,\n // Failure strings stay English (error-surface policy: not localized).\n compactNow: async () => {\n const result = await ctx.remote.commands.execute(sessionId, '/compact', [])\n if (!result.ok) return `${result.error.message} (${result.error.code})`\n if (result.value === undefined) return 'unknown command: /compact'\n return null\n },\n hooks: { config: scope },\n }),\n }, CompactGuardButton))\n\n // The plugin configuration tab keys its cards on the settings namespace, so\n // the compact-timing card registers under CONTEXT_GUARD_NS and renders\n // wherever the tab dispatches that key.\n ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({\n name: 'settings.plugin.item',\n key: CONTEXT_GUARD_NS,\n locale: NS,\n inject: (): ContextGuardSettingsCardInjected => ({\n scope,\n hooks: { config: scope },\n }),\n }, ContextGuardSettingsCard))\n}\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
//#region lib/types/namespace.js
|
|
4
|
+
/**
|
|
5
|
+
* The settings namespace owned by the context-guard plugin. Plain string so
|
|
6
|
+
* both the host half (settings registration) and the browser half
|
|
7
|
+
* (settingsScope binding and the settings.plugin.item card key) import it
|
|
8
|
+
* without dragging host-only dependencies into the client bundle.
|
|
9
|
+
*/
|
|
10
|
+
/** Settings namespace owned by the context-guard plugin. */
|
|
11
|
+
const CONTEXT_GUARD_NS = "context-guard";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region lib/types/settings.js
|
|
14
|
+
/**
|
|
15
|
+
* Host-side settings section of the context-guard plugin: the schemastery
|
|
16
|
+
* schema that validates the `context-guard` settings namespace. The browser
|
|
17
|
+
* half reads the resolved section through `ctx.settingsScope` and the
|
|
18
|
+
* settings page renders it as a plugin card; the composition entry (the
|
|
19
|
+
* cordis.yml row) becomes the section's `base` layer, so YAML values keep
|
|
20
|
+
* working as the defaults the card can override.
|
|
21
|
+
*
|
|
22
|
+
* These two fields tune ONLY when the compact button appears — the official
|
|
23
|
+
* compaction engine (compaction-basic) reads its own `thresholdRatio` /
|
|
24
|
+
* `maxTokens` config and never touches this section.
|
|
25
|
+
*/
|
|
26
|
+
/** Section shape: the context-occupancy fraction at which the button appears. */
|
|
27
|
+
const ContextGuardSettingsSchema = z.object({ thresholdRatio: z.number().min(.01).max(1).default(.8) });
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region lib/types/index.js
|
|
30
|
+
/**
|
|
31
|
+
* Context-guard plugin, node half. Registers the `context-guard` settings
|
|
32
|
+
* namespace so the web settings page can render the compact-timing card and
|
|
33
|
+
* the browser half can bind its `settingsScope` to the same section. The
|
|
34
|
+
* plugin's own runtime behavior is purely client-side (the composer compact
|
|
35
|
+
* button); this registration only exists to make the two tunables
|
|
36
|
+
* (`thresholdRatio`, `maxTokens`) editable from the GUI instead of YAML.
|
|
37
|
+
* @module @khorsheed/dsh-context-guard
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Register the compact-timing section when a settings provider exists.
|
|
41
|
+
* @param ctx - Host context whose optional settings service owns the section.
|
|
42
|
+
* @param config - the plugin's composition entry, used as the section's base layer.
|
|
43
|
+
*/
|
|
44
|
+
function apply(ctx, config = {}) {
|
|
45
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
46
|
+
settingsCtx.settings.register(settingsNamespace(CONTEXT_GUARD_NS), ContextGuardSettingsSchema, { base: config });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CONTEXT_GUARD_NS, ContextGuardSettingsSchema, apply };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@khorsheed/dsh-context-guard`.
|
|
4
|
+
* @module @khorsheed/dsh-context-guard/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@khorsheed/dsh-context-guard";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "context-guard-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: every compact action is executed through the official
|
|
13
|
+
* `/compact` command channel, whose command-log invariant companion audits the
|
|
14
|
+
* command lifecycle, and the plugin's one composer-tool-row entry proves
|
|
15
|
+
* disposal through the browser-plugin spec. No second authority exists to
|
|
16
|
+
* check at runtime.
|
|
17
|
+
*/
|
|
18
|
+
const install = () => {};
|
|
19
|
+
/**
|
|
20
|
+
* Register this package's invariant companion.
|
|
21
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
22
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
23
|
+
*/
|
|
24
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
25
|
+
//#endregion
|
|
26
|
+
export { apply, inject, name };
|