dsh-context-compression-improved 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1462 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-context-compression-improved",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region src/profiles.ts
11
+ /** Public context-compression choices shared by the Host schema and browser selector. */
12
+ const COMPRESSION_PROFILES = [
13
+ "off",
14
+ "native",
15
+ "balanced",
16
+ "cache-strict",
17
+ "savings",
18
+ "adaptive",
19
+ "tokenpilot-inspired",
20
+ "custom"
21
+ ];
22
+ /** Browser-safe mirror of the Host's Balanced-equivalent Custom default. */
23
+ const DEFAULT_CUSTOM_COMPRESSION_POLICY = {
24
+ version: 3,
25
+ unit: "tokens",
26
+ fresh: {
27
+ enabled: true,
28
+ trigger: 8192,
29
+ target: 3072
30
+ },
31
+ aggregate: {
32
+ enabled: true,
33
+ trigger: 32768,
34
+ target: 12288
35
+ },
36
+ history: {
37
+ enabled: true,
38
+ trigger: 5e5,
39
+ keepRecentToolCalls: 10,
40
+ keepRecentTokens: 64e3,
41
+ minReclaim: 96e3
42
+ },
43
+ prefixPolicy: "pressure-break",
44
+ tailTrim: {
45
+ enabled: false,
46
+ trigger: 7e5
47
+ }
48
+ };
49
+ /**
50
+ * Decode the persisted codeSkeleton section with exactly the runtime schema's
51
+ * strictness: absent means the lossless off default; present values must be a
52
+ * plain object carrying only a boolean `enabled`. Anything else is invalid,
53
+ * never silently coerced.
54
+ */
55
+ function decodeCodeSkeletonSettings(value) {
56
+ if (value === void 0) return { enabled: false };
57
+ if (!isPlainRecord(value)) return void 0;
58
+ const keys = Object.keys(value);
59
+ if (keys.length !== 1 || keys[0] !== "enabled") return void 0;
60
+ const enabled = value.enabled;
61
+ return typeof enabled === "boolean" ? { enabled } : void 0;
62
+ }
63
+ /**
64
+ * Browser mirror of the runtime presetOptions section: absent inherits the
65
+ * preset defaults (decodes to `undefined`); present values must be a plain
66
+ * object carrying only the known keys with valid types.
67
+ */
68
+ function decodePresetOptionsSettings(value) {
69
+ if (value === void 0) return void 0;
70
+ if (!isPlainRecord(value)) return void 0;
71
+ const allowed = /* @__PURE__ */ new Set([
72
+ "dedupeToolResults",
73
+ "summaryLocator",
74
+ "prefixStabilizer",
75
+ "readState",
76
+ "estimatorMode",
77
+ "estimatorProvider",
78
+ "estimatorModel",
79
+ "estimatorBaseUrl",
80
+ "estimatorApiKey",
81
+ "estimatorTimeoutMs"
82
+ ]);
83
+ if (Object.keys(value).some((key) => !allowed.has(key))) return void 0;
84
+ for (const key of [
85
+ "dedupeToolResults",
86
+ "summaryLocator",
87
+ "prefixStabilizer",
88
+ "readState"
89
+ ]) {
90
+ const entry = value[key];
91
+ if (entry !== void 0 && typeof entry !== "boolean") return void 0;
92
+ }
93
+ const estimatorMode = value.estimatorMode;
94
+ if (estimatorMode !== void 0 && estimatorMode !== "" && estimatorMode !== "host" && estimatorMode !== "direct") return;
95
+ for (const key of [
96
+ "estimatorProvider",
97
+ "estimatorModel",
98
+ "estimatorBaseUrl",
99
+ "estimatorApiKey"
100
+ ]) {
101
+ const entry = value[key];
102
+ if (entry !== void 0 && typeof entry !== "string") return void 0;
103
+ }
104
+ const estimatorTimeoutMs = value.estimatorTimeoutMs;
105
+ if (estimatorTimeoutMs !== void 0 && (typeof estimatorTimeoutMs !== "number" || !Number.isSafeInteger(estimatorTimeoutMs) || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 6e4)) return;
106
+ const decoded = {};
107
+ if (value.dedupeToolResults !== void 0) decoded.dedupeToolResults = value.dedupeToolResults;
108
+ if (value.summaryLocator !== void 0) decoded.summaryLocator = value.summaryLocator;
109
+ if (value.prefixStabilizer !== void 0) decoded.prefixStabilizer = value.prefixStabilizer;
110
+ if (value.readState !== void 0) decoded.readState = value.readState;
111
+ if (estimatorMode !== void 0) decoded.estimatorMode = estimatorMode;
112
+ if (value.estimatorProvider !== void 0) decoded.estimatorProvider = value.estimatorProvider;
113
+ if (value.estimatorModel !== void 0) decoded.estimatorModel = value.estimatorModel;
114
+ if (value.estimatorBaseUrl !== void 0) decoded.estimatorBaseUrl = value.estimatorBaseUrl;
115
+ if (value.estimatorApiKey !== void 0) decoded.estimatorApiKey = value.estimatorApiKey;
116
+ if (estimatorTimeoutMs !== void 0) decoded.estimatorTimeoutMs = estimatorTimeoutMs;
117
+ return decoded;
118
+ }
119
+ /**
120
+ * The one threshold contract shared by the UI, the persisted settings, and the
121
+ * runtime resolver; mirrored browser-safe from the runtime package.
122
+ */
123
+ const AUTO_COMPACT_THRESHOLD_LIMITS = Object.freeze({
124
+ min: 50,
125
+ max: 90,
126
+ step: 1,
127
+ default: 80
128
+ });
129
+ /** Narrow one unknown value to a valid Auto Compact threshold percent. */
130
+ function isValidAutoCompactThresholdPercent(value) {
131
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= AUTO_COMPACT_THRESHOLD_LIMITS.min && value <= AUTO_COMPACT_THRESHOLD_LIMITS.max;
132
+ }
133
+ /** Accept JSON-object records while rejecting class instances and exotic prototypes. */
134
+ function isPlainRecord(value) {
135
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
136
+ const prototype = Object.getPrototypeOf(value);
137
+ return prototype === Object.prototype || prototype === null;
138
+ }
139
+ /**
140
+ * Decode the persisted autoCompact section with exactly the runtime schema's
141
+ * strictness: absent means the 80% default; present values must be a plain
142
+ * object carrying only a valid `thresholdPercent`. Anything else is invalid,
143
+ * never silently coerced.
144
+ */
145
+ function decodeAutoCompactSettings(value) {
146
+ if (value === void 0) return { thresholdPercent: AUTO_COMPACT_THRESHOLD_LIMITS.default };
147
+ if (!isPlainRecord(value)) return void 0;
148
+ const keys = Object.keys(value);
149
+ if (keys.length !== 1 || keys[0] !== "thresholdPercent") return void 0;
150
+ const thresholdPercent = value.thresholdPercent;
151
+ return isValidAutoCompactThresholdPercent(thresholdPercent) ? { thresholdPercent } : void 0;
152
+ }
153
+ /**
154
+ * Narrow an unknown settings value to a complete supported Custom policy.
155
+ * @param value - Candidate settings value received from the Host or edited locally.
156
+ * @returns Whether the value is a relation-valid Custom policy.
157
+ */
158
+ function isCustomCompressionPolicy(value) {
159
+ if (!hasExactKeys(value, value !== null && typeof value === "object" && "version" in value && value.version === 1 ? [
160
+ "version",
161
+ "unit",
162
+ "fresh",
163
+ "aggregate",
164
+ "history",
165
+ "prefixPolicy"
166
+ ] : [
167
+ "version",
168
+ "unit",
169
+ "fresh",
170
+ "aggregate",
171
+ "history",
172
+ "prefixPolicy",
173
+ "tailTrim"
174
+ ])) return false;
175
+ if (value.version !== 1 && value.version !== 2 && value.version !== 3 || value.unit !== "tokens" && value.unit !== "context-percent") return false;
176
+ if (value.prefixPolicy !== "preserve" && value.prefixPolicy !== "pressure-break") return false;
177
+ if (!isBudget(value.fresh) || !isBudget(value.aggregate)) return false;
178
+ const modernHistory = value.version === 3;
179
+ if (!hasExactKeys(value.history, modernHistory ? [
180
+ "enabled",
181
+ "trigger",
182
+ "keepRecentToolCalls",
183
+ "keepRecentTokens",
184
+ "minReclaim"
185
+ ] : [
186
+ "enabled",
187
+ "trigger",
188
+ "keepRecentTurns",
189
+ "keepRecent",
190
+ "minReclaim"
191
+ ])) return false;
192
+ if (typeof value.history.enabled !== "boolean" || typeof value.history.trigger !== "number" || typeof value.history.minReclaim !== "number") return false;
193
+ const recent = modernHistory ? value.history.keepRecentTokens : value.history.keepRecent;
194
+ const calls = modernHistory ? value.history.keepRecentToolCalls : value.history.keepRecentTurns;
195
+ if (typeof recent !== "number" || typeof calls !== "number" || !Number.isSafeInteger(calls) || calls < 0) return false;
196
+ let tailTrimTrigger;
197
+ if (value.version !== 1) {
198
+ const tailTrim = value.tailTrim;
199
+ if (!hasExactKeys(tailTrim, ["enabled", "trigger"]) || typeof tailTrim.enabled !== "boolean" || typeof tailTrim.trigger !== "number") return false;
200
+ tailTrimTrigger = tailTrim.trigger;
201
+ }
202
+ const measured = [
203
+ value.fresh.trigger,
204
+ value.fresh.target,
205
+ value.aggregate.trigger,
206
+ value.aggregate.target,
207
+ value.history.trigger,
208
+ recent,
209
+ value.history.minReclaim,
210
+ ...tailTrimTrigger === void 0 ? [] : [tailTrimTrigger]
211
+ ];
212
+ if (!measured.every((entry) => typeof entry === "number" && Number.isFinite(entry))) return false;
213
+ if (value.fresh.trigger <= 0 || value.fresh.target <= 0 || value.aggregate.trigger <= 0 || value.aggregate.target <= 0 || value.history.trigger <= 0 || recent < 0 || value.history.minReclaim <= 0 || tailTrimTrigger !== void 0 && tailTrimTrigger <= 0) return false;
214
+ if (value.unit === "tokens" && !measured.every(Number.isSafeInteger)) return false;
215
+ if (value.unit === "context-percent" && !measured.every((entry) => entry <= 100)) return false;
216
+ return value.fresh.target < value.fresh.trigger && value.aggregate.target < value.aggregate.trigger && value.history.minReclaim <= value.history.trigger;
217
+ }
218
+ function isBudget(value) {
219
+ return hasExactKeys(value, [
220
+ "enabled",
221
+ "trigger",
222
+ "target"
223
+ ]) && typeof value.enabled === "boolean" && typeof value.trigger === "number" && typeof value.target === "number";
224
+ }
225
+ /**
226
+ * Canonicalize one validated Custom document to version 3, mirroring the
227
+ * runtime's `canonicalizeCustomPolicy` exactly so the browser and runtime
228
+ * boundaries hand the SAME complete document to the UI and the policy
229
+ * resolver: legacy v1/v2 History working sets upgrade to the 10-call default,
230
+ * and a v1 document gains the default-disabled TailTrim stage.
231
+ */
232
+ function canonicalizeCustomPolicy(policy) {
233
+ if (policy.version === 3) return structuredClone(policy);
234
+ return {
235
+ version: 3,
236
+ unit: policy.unit,
237
+ fresh: structuredClone(policy.fresh),
238
+ aggregate: structuredClone(policy.aggregate),
239
+ history: {
240
+ enabled: policy.history.enabled,
241
+ trigger: policy.history.trigger,
242
+ keepRecentToolCalls: 10,
243
+ keepRecentTokens: policy.history.keepRecent,
244
+ minReclaim: policy.history.minReclaim
245
+ },
246
+ prefixPolicy: policy.prefixPolicy,
247
+ tailTrim: policy.version === 1 ? {
248
+ enabled: false,
249
+ trigger: 7e5
250
+ } : structuredClone(policy.tailTrim)
251
+ };
252
+ }
253
+ function hasExactKeys(value, expected) {
254
+ if (!isPlainRecord(value)) return false;
255
+ const keys = Object.keys(value);
256
+ return keys.length === expected.length && keys.every((key) => expected.includes(key));
257
+ }
258
+ //#endregion
259
+ //#region \0dsh-context-compression-css:466eb745356d-CompressionProfileSelector.module.css.mjs
260
+ const css = ".rLocJG_settingsSection{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.rLocJG_settingsTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.rLocJG_settingsDescription{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.rLocJG_root{width:100%}.rLocJG_profileGrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.rLocJG_profileCard{border:1px solid var(--dsw-alias-border-l2);min-height:112px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border-radius:10px;flex-direction:column;gap:8px;padding:14px;display:flex}.rLocJG_profileCard:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.rLocJG_profileCard[aria-pressed=true]{border-color:var(--dsw-alias-label-primary)}.rLocJG_profileCard:active:not(:disabled){transform:scale(.99)}.rLocJG_profileCard:disabled{cursor:default;opacity:.6}.rLocJG_profileCard:focus-visible{outline-offset:2px;outline:2px solid}.rLocJG_profileCardTop{align-items:center;gap:8px;display:flex}.rLocJG_profileCardTitle{font-size:14px;font-weight:500;line-height:20px}.rLocJG_profileCurrent{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);border-radius:999px;padding:1px 6px;font-size:10px;line-height:14px}.rLocJG_profileCardDetail{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.rLocJG_button{border:1px solid var(--dsw-alias-border-l2);width:100%;min-height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border-radius:10px;align-items:center;gap:8px;padding:6px 8px;display:flex}.rLocJG_button:hover{background:var(--dsw-alias-interactive-bg-hover)}.rLocJG_button:disabled{cursor:default;opacity:.6}.rLocJG_copy{flex:1;min-width:0}.rLocJG_label{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:14px}.rLocJG_value{text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:17px;overflow:hidden}.rLocJG_chevron{color:var(--dsw-alias-label-secondary);flex:none}.rLocJG_menuCopy{flex-direction:column;gap:2px;min-width:220px;display:flex}.rLocJG_menuTitle{font-size:13px;line-height:17px}.rLocJG_menuDetail{white-space:normal;max-width:290px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:15px}.rLocJG_error{color:var(--dsw-alias-state-error-primary);padding:4px 8px 0;font-size:11px;line-height:15px}.rLocJG_unavailable{color:var(--dsw-alias-label-tertiary);padding:4px 8px 0;font-size:11px;line-height:15px}.rLocJG_settingsHint{color:var(--dsw-alias-label-secondary);padding:4px 8px 0;font-size:11px;line-height:15px}.rLocJG_pricing{color:var(--dsw-alias-label-tertiary);padding:4px 8px 0;font-size:11px;line-height:15px}.rLocJG_custom{border-top:1px solid var(--dsw-alias-border-l2);margin-top:16px;padding:16px 0 0}.rLocJG_customTitle{margin:0 0 6px;font-size:13px;line-height:17px}.rLocJG_customNote{color:var(--dsw-alias-label-tertiary);margin:4px 0;font-size:11px;line-height:15px}.rLocJG_stage{border:0;border-top:1px solid var(--dsw-alias-border-l2);margin:12px 0 0;padding:12px 0 0}.rLocJG_stageToggle{align-items:center;gap:6px;font-size:12px;line-height:16px;display:inline-flex}.rLocJG_fieldGrid{grid-template-columns:1fr;gap:8px;display:grid}.rLocJG_field{min-width:0;color:var(--dsw-alias-label-secondary);flex-direction:column;gap:4px;margin-top:8px;font-size:11px;line-height:15px;display:flex}.rLocJG_field input,.rLocJG_field select{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;min-width:0;min-height:30px;color:var(--dsw-alias-label-primary);border-radius:7px;padding:4px 6px}.rLocJG_field input:focus-visible,.rLocJG_field select:focus-visible,.rLocJG_actions button:focus-visible{outline-offset:2px;outline:2px solid}.rLocJG_actions{flex-wrap:wrap;gap:8px;margin-top:10px;display:flex}.rLocJG_actions button{border:1px solid var(--dsw-alias-border-l2);min-height:30px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:7px;padding:4px 8px}.rLocJG_actions button:active:not(:disabled){transform:scale(.98)}.rLocJG_actions button:disabled{opacity:.6}@media (width<=560px){.rLocJG_profileGrid{grid-template-columns:1fr}}.rLocJG_autoCompact{border:1px solid #80808059;border-radius:8px;margin-top:24px;padding:16px}.rLocJG_autoCompactTitle{margin:0 0 8px;font-size:15px;font-weight:600}.rLocJG_autoCompactRisk{opacity:.9;margin:8px 0 0;font-size:12px}";
261
+ const tagId = "dsh-context-compression-improved/CompressionProfileSelector.module.css";
262
+ if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${tagId}"]`) === null) {
263
+ const tag = document.createElement("style");
264
+ tag.dataset.plugin = "dsh-context-compression-improved";
265
+ tag.dataset.pluginCss = tagId;
266
+ tag.textContent = css;
267
+ document.head.appendChild(tag);
268
+ }
269
+ var _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default = {
270
+ "actions": "rLocJG_actions",
271
+ "autoCompact": "rLocJG_autoCompact",
272
+ "autoCompactRisk": "rLocJG_autoCompactRisk",
273
+ "autoCompactTitle": "rLocJG_autoCompactTitle",
274
+ "button": "rLocJG_button",
275
+ "chevron": "rLocJG_chevron",
276
+ "copy": "rLocJG_copy",
277
+ "custom": "rLocJG_custom",
278
+ "customNote": "rLocJG_customNote",
279
+ "customTitle": "rLocJG_customTitle",
280
+ "error": "rLocJG_error",
281
+ "field": "rLocJG_field",
282
+ "fieldGrid": "rLocJG_fieldGrid",
283
+ "label": "rLocJG_label",
284
+ "menuCopy": "rLocJG_menuCopy",
285
+ "menuDetail": "rLocJG_menuDetail",
286
+ "menuTitle": "rLocJG_menuTitle",
287
+ "pricing": "rLocJG_pricing",
288
+ "profileCard": "rLocJG_profileCard",
289
+ "profileCardDetail": "rLocJG_profileCardDetail",
290
+ "profileCardTitle": "rLocJG_profileCardTitle",
291
+ "profileCardTop": "rLocJG_profileCardTop",
292
+ "profileCurrent": "rLocJG_profileCurrent",
293
+ "profileGrid": "rLocJG_profileGrid",
294
+ "root": "rLocJG_root",
295
+ "settingsDescription": "rLocJG_settingsDescription",
296
+ "settingsHint": "rLocJG_settingsHint",
297
+ "settingsSection": "rLocJG_settingsSection",
298
+ "settingsTitle": "rLocJG_settingsTitle",
299
+ "stage": "rLocJG_stage",
300
+ "stageToggle": "rLocJG_stageToggle",
301
+ "unavailable": "rLocJG_unavailable",
302
+ "value": "rLocJG_value"
303
+ };
304
+ //#endregion
305
+ //#region src/client/CustomPolicyEditor.tsx
306
+ function CustomPolicyEditor({ value, disabled, setValue, save, reset, settle, t }) {
307
+ const valid = isCustomCompressionPolicy(value);
308
+ const unitStep = value.unit === "tokens" ? 1 : .01;
309
+ const unitMax = value.unit === "tokens" ? void 0 : 100;
310
+ const unitBounds = unitMax === void 0 ? {} : { max: unitMax };
311
+ const setBudget = (stage, patch) => {
312
+ setValue({
313
+ ...value,
314
+ [stage]: {
315
+ ...value[stage],
316
+ ...patch
317
+ }
318
+ });
319
+ };
320
+ const setHistory = (patch) => {
321
+ setValue({
322
+ ...value,
323
+ history: {
324
+ ...value.history,
325
+ ...patch
326
+ }
327
+ });
328
+ };
329
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
330
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.custom,
331
+ "aria-labelledby": "context-compression-custom-title",
332
+ children: [
333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
334
+ id: "context-compression-custom-title",
335
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customTitle,
336
+ children: t("custom.title")
337
+ }),
338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
339
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
340
+ children: t("custom.sessionScope")
341
+ }),
342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
343
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
344
+ children: t("custom.measurement")
345
+ }),
346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
347
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
348
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("custom.unit") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
349
+ value: value.unit,
350
+ disabled,
351
+ onChange: (event) => {
352
+ setValue({
353
+ ...value,
354
+ unit: event.currentTarget.value
355
+ });
356
+ },
357
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
358
+ value: "tokens",
359
+ children: t("custom.unit.tokens")
360
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
361
+ value: "context-percent",
362
+ children: t("custom.unit.contextPercent")
363
+ })]
364
+ })]
365
+ }),
366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StageFields, {
367
+ t,
368
+ title: t("custom.fresh.enabled"),
369
+ enabled: value.fresh.enabled,
370
+ disabled,
371
+ onEnabled: (enabled) => {
372
+ setBudget("fresh", { enabled });
373
+ },
374
+ fields: [{
375
+ label: t("custom.fresh.trigger"),
376
+ value: value.fresh.trigger,
377
+ set: (trigger) => {
378
+ setBudget("fresh", { trigger });
379
+ },
380
+ ...unitBounds
381
+ }, {
382
+ label: t("custom.fresh.target"),
383
+ value: value.fresh.target,
384
+ set: (target) => {
385
+ setBudget("fresh", { target });
386
+ },
387
+ ...unitBounds
388
+ }],
389
+ step: unitStep
390
+ }),
391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StageFields, {
392
+ t,
393
+ title: t("custom.aggregate.enabled"),
394
+ enabled: value.aggregate.enabled,
395
+ disabled,
396
+ onEnabled: (enabled) => {
397
+ setBudget("aggregate", { enabled });
398
+ },
399
+ fields: [{
400
+ label: t("custom.aggregate.trigger"),
401
+ value: value.aggregate.trigger,
402
+ set: (trigger) => {
403
+ setBudget("aggregate", { trigger });
404
+ },
405
+ ...unitBounds
406
+ }, {
407
+ label: t("custom.aggregate.target"),
408
+ value: value.aggregate.target,
409
+ set: (target) => {
410
+ setBudget("aggregate", { target });
411
+ },
412
+ ...unitBounds
413
+ }],
414
+ step: unitStep
415
+ }),
416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StageFields, {
417
+ t,
418
+ title: t("custom.history.enabled"),
419
+ enabled: value.history.enabled,
420
+ disabled,
421
+ onEnabled: (enabled) => {
422
+ setHistory({ enabled });
423
+ },
424
+ fields: [
425
+ {
426
+ label: t("custom.history.trigger"),
427
+ value: value.history.trigger,
428
+ set: (trigger) => {
429
+ setHistory({ trigger });
430
+ },
431
+ ...unitBounds
432
+ },
433
+ {
434
+ label: t("custom.history.keepRecentToolCalls"),
435
+ value: value.history.keepRecentToolCalls,
436
+ set: (keepRecentToolCalls) => {
437
+ setHistory({ keepRecentToolCalls });
438
+ },
439
+ integer: true,
440
+ allowZero: true
441
+ },
442
+ {
443
+ label: t("custom.history.keepRecentTokens"),
444
+ value: value.history.keepRecentTokens,
445
+ set: (keepRecentTokens) => {
446
+ setHistory({ keepRecentTokens });
447
+ },
448
+ allowZero: true,
449
+ ...unitBounds
450
+ },
451
+ {
452
+ label: t("custom.history.minReclaim"),
453
+ value: value.history.minReclaim,
454
+ set: (minReclaim) => {
455
+ setHistory({ minReclaim });
456
+ },
457
+ ...unitBounds
458
+ }
459
+ ],
460
+ step: unitStep,
461
+ fieldsEnabled: value.history.enabled || value.tailTrim.enabled
462
+ }),
463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
464
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
465
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("custom.prefixPolicy") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
466
+ value: value.prefixPolicy,
467
+ disabled: disabled || !value.history.enabled,
468
+ onChange: (event) => {
469
+ setValue({
470
+ ...value,
471
+ prefixPolicy: event.currentTarget.value
472
+ });
473
+ },
474
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
475
+ value: "preserve",
476
+ children: t("custom.prefixPolicy.preserve")
477
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
478
+ value: "pressure-break",
479
+ children: t("custom.prefixPolicy.pressureBreak")
480
+ })]
481
+ })]
482
+ }),
483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
484
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
485
+ children: t("custom.experimental")
486
+ }),
487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StageFields, {
488
+ t,
489
+ title: t("custom.tailTrim.enabled"),
490
+ enabled: value.tailTrim.enabled,
491
+ disabled,
492
+ onEnabled: (enabled) => {
493
+ setValue({
494
+ ...value,
495
+ tailTrim: {
496
+ ...value.tailTrim,
497
+ enabled
498
+ }
499
+ });
500
+ },
501
+ fields: [{
502
+ label: t("custom.tailTrim.trigger"),
503
+ value: value.tailTrim.trigger,
504
+ set: (trigger) => {
505
+ setValue({
506
+ ...value,
507
+ tailTrim: {
508
+ ...value.tailTrim,
509
+ trigger
510
+ }
511
+ });
512
+ },
513
+ ...unitBounds
514
+ }],
515
+ step: unitStep
516
+ }),
517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
518
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
519
+ children: t("custom.tailTrim.warning")
520
+ }),
521
+ valid ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
522
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.error,
523
+ role: "alert",
524
+ children: t("custom.invalid")
525
+ }),
526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
527
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.actions,
528
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
529
+ type: "button",
530
+ disabled: disabled || !valid,
531
+ onClick: () => {
532
+ settle(save);
533
+ },
534
+ children: t("custom.save")
535
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
536
+ type: "button",
537
+ disabled,
538
+ onClick: () => {
539
+ settle(reset);
540
+ },
541
+ children: t("custom.reset")
542
+ })]
543
+ })
544
+ ]
545
+ });
546
+ }
547
+ function StageFields({ title, enabled, disabled, onEnabled, fields, step, fieldsEnabled = enabled, t }) {
548
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
549
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.stage,
550
+ disabled,
551
+ children: [
552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: title }),
553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
554
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("custom.enabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
556
+ "aria-label": title,
557
+ value: enabled ? "on" : "off",
558
+ onChange: (event) => {
559
+ onEnabled(event.currentTarget.value === "on");
560
+ },
561
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
562
+ value: "on",
563
+ children: t("custom.enabled.on")
564
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
565
+ value: "off",
566
+ children: t("custom.enabled.off")
567
+ })]
568
+ })]
569
+ }),
570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
571
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.fieldGrid,
572
+ children: fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
573
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
574
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: field.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
575
+ type: "number",
576
+ value: field.value,
577
+ min: field.allowZero === true ? 0 : field.integer === true ? 1 : step,
578
+ max: field.max,
579
+ step: field.integer === true ? 1 : step,
580
+ disabled: !fieldsEnabled || disabled,
581
+ onChange: (event) => {
582
+ field.set(Number(event.currentTarget.value));
583
+ }
584
+ })]
585
+ }, field.label))
586
+ })
587
+ ]
588
+ });
589
+ }
590
+ /** Convert a legacy custom policy to V3 format. */
591
+ function editableCustom(value) {
592
+ if (value.version === 3) return structuredClone(value);
593
+ return {
594
+ version: 3,
595
+ unit: value.unit,
596
+ fresh: structuredClone(value.fresh),
597
+ aggregate: structuredClone(value.aggregate),
598
+ history: {
599
+ enabled: value.history.enabled,
600
+ trigger: value.history.trigger,
601
+ keepRecentToolCalls: 10,
602
+ keepRecentTokens: value.history.keepRecent,
603
+ minReclaim: value.history.minReclaim
604
+ },
605
+ prefixPolicy: value.prefixPolicy,
606
+ tailTrim: value.version === 1 ? {
607
+ enabled: false,
608
+ trigger: 7e5
609
+ } : structuredClone(value.tailTrim)
610
+ };
611
+ }
612
+ //#endregion
613
+ //#region src/client/CompressionProfileControls.tsx
614
+ /**
615
+ * CompressionProfileControls: dropdown selector for compression profiles,
616
+ * plus AutoCompactThresholdControls and CodeSkeletonControls sub-components.
617
+ *
618
+ * Extracted from CompressionProfileSelector.tsx to reduce god-module size.
619
+ *
620
+ * @module dsh-context-compression-improved/client/CompressionProfileControls
621
+ */
622
+ /**
623
+ * The authoritative Auto Compact threshold editor for the context-compression
624
+ * section. A typed number input and its save path are kept deliberately simple;
625
+ * values outside the recommended 70–85 band warn without blocking.
626
+ */
627
+ function AutoCompactThresholdControls({ value, disabled, save, settle, t }) {
628
+ const [draft, setDraft] = (0, react.useState)(String(value));
629
+ (0, react.useEffect)(() => {
630
+ setDraft(String(value));
631
+ }, [value]);
632
+ const parsed = Number(draft);
633
+ const valid = isValidAutoCompactThresholdPercent(parsed);
634
+ const risk = !valid ? "autoCompact.invalid" : parsed < 70 ? "autoCompact.riskLow" : parsed > 85 ? "autoCompact.riskHigh" : void 0;
635
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
636
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompact,
637
+ "aria-labelledby": "context-compression-autocompact-title",
638
+ children: [
639
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
640
+ id: "context-compression-autocompact-title",
641
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactTitle,
642
+ children: t("autoCompact.title")
643
+ }),
644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
645
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
646
+ children: t("autoCompact.description")
647
+ }),
648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
649
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("autoCompact.inputLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
651
+ type: "number",
652
+ value: draft,
653
+ min: AUTO_COMPACT_THRESHOLD_LIMITS.min,
654
+ max: AUTO_COMPACT_THRESHOLD_LIMITS.max,
655
+ step: AUTO_COMPACT_THRESHOLD_LIMITS.step,
656
+ disabled,
657
+ "aria-invalid": !valid,
658
+ onChange: (event) => {
659
+ setDraft(event.currentTarget.value);
660
+ }
661
+ })]
662
+ }),
663
+ risk === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
664
+ className: risk === "autoCompact.invalid" ? _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.error : _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactRisk,
665
+ role: risk === "autoCompact.invalid" ? "alert" : "note",
666
+ children: t(risk)
667
+ }),
668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
669
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.actions,
670
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
671
+ type: "button",
672
+ disabled: disabled || !valid || parsed === value,
673
+ onClick: () => {
674
+ settle(() => save(parsed));
675
+ },
676
+ children: t("autoCompact.save")
677
+ })
678
+ })
679
+ ]
680
+ });
681
+ }
682
+ /**
683
+ * The authoritative code-skeleton reducer gate for the context-compression
684
+ * section. Deliberately minimal — an on/off select plus its own save path —
685
+ * because the gate is orthogonal to every profile and carries no parameters.
686
+ */
687
+ function CodeSkeletonControls({ value, disabled, save, settle, t }) {
688
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
689
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompact,
690
+ "aria-labelledby": "context-compression-codeskeleton-title",
691
+ children: [
692
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
693
+ id: "context-compression-codeskeleton-title",
694
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactTitle,
695
+ children: t("codeSkeleton.title")
696
+ }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
698
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
699
+ children: t("codeSkeleton.description")
700
+ }),
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
702
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
703
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("codeSkeleton.enabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
704
+ value: value ? "on" : "off",
705
+ disabled,
706
+ onChange: (event) => {
707
+ settle(() => save(event.currentTarget.value === "on"));
708
+ },
709
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
710
+ value: "on",
711
+ children: t("codeSkeleton.enabled.on")
712
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
713
+ value: "off",
714
+ children: t("codeSkeleton.enabled.off")
715
+ })]
716
+ })]
717
+ })
718
+ ]
719
+ });
720
+ }
721
+ //#endregion
722
+ //#region src/client/EstimatorControls.tsx
723
+ /**
724
+ * TokenPilot-inspired estimator channel card components.
725
+ *
726
+ * Extracted from CompressionProfileSelector.tsx to reduce god-module size.
727
+ *
728
+ * @module dsh-context-compression-improved/client/EstimatorControls
729
+ */
730
+ /**
731
+ * The estimator card is gated on the tokenpilot-inspired profile, because
732
+ * `presetOptions` is merged into that profile alone. A card that merely
733
+ * disappears reads as a missing feature — the first real-machine report was
734
+ * exactly that — so keep the heading and its anchor id in place and spend them
735
+ * on the reason plus the profile that unlocks the card. The gate itself is
736
+ * unchanged: no estimator control exists outside tokenpilot-inspired.
737
+ */
738
+ function EstimatorInactiveNotice({ profile, t }) {
739
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
740
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompact,
741
+ "aria-labelledby": "context-compression-estimator-title",
742
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
743
+ id: "context-compression-estimator-title",
744
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactTitle,
745
+ children: t("estimator.title")
746
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
747
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
748
+ children: t("estimator.inactive").replace("{profile}", profile)
749
+ })]
750
+ });
751
+ }
752
+ const ESTIMATOR_CATALOG_ROUTES = ["/api/dsh-context-compression-improved/estimator-catalog"];
753
+ /**
754
+ * TokenPilot-inspired estimator channel card. Shown only while the
755
+ * tokenpilot-inspired profile is selected, and split by channel:
756
+ *
757
+ * - `host` reuses the providers and credentials already configured in DSH
758
+ * through the Harness `llm` service, so this card names a provider and a
759
+ * model and accepts NO API key — the key field belongs to the direct channel
760
+ * alone.
761
+ * - `direct` talks to a native OpenAI-compatible endpoint, the only channel
762
+ * carrying its own base URL and write-only key.
763
+ *
764
+ * The whole card is advisory: an unconfigured or failing endpoint keeps every
765
+ * consumer on its rule-only fallback.
766
+ */
767
+ function EstimatorControls({ options, disabled, save, settle, t }) {
768
+ const [keyDraft, setKeyDraft] = (0, react.useState)("");
769
+ const [baseUrl, setBaseUrl] = (0, react.useState)(options.estimatorBaseUrl ?? "");
770
+ const [model, setModel] = (0, react.useState)(options.estimatorModel ?? "");
771
+ const [provider, setProvider] = (0, react.useState)(options.estimatorProvider ?? "");
772
+ const mode = options.estimatorMode ?? "";
773
+ const [catalog, setCatalog] = (0, react.useState)();
774
+ (0, react.useEffect)(() => {
775
+ if (mode !== "host") return;
776
+ let alive = true;
777
+ let attempts = 0;
778
+ const load = async (routes) => {
779
+ for (const route of routes) try {
780
+ const response = await fetch(route, { headers: { "cache-control": "no-cache" } });
781
+ if (response.ok) return await response.json();
782
+ } catch {}
783
+ };
784
+ const tick = () => {
785
+ attempts += 1;
786
+ load(ESTIMATOR_CATALOG_ROUTES).then((body) => {
787
+ if (!alive) return;
788
+ if (body !== void 0 && (body.providers?.length ?? 0) > 0) {
789
+ setCatalog(body);
790
+ return;
791
+ }
792
+ if (attempts < 10) setTimeout(tick, 3e3);
793
+ });
794
+ };
795
+ tick();
796
+ return () => {
797
+ alive = false;
798
+ };
799
+ }, [mode]);
800
+ const hostProviders = catalog?.providers ?? [];
801
+ const providerDraft = provider;
802
+ const hostModels = hostProviders.filter((entry) => providerDraft === "" || entry.id === providerDraft).flatMap((entry) => entry.models.map((model) => ({
803
+ ...model,
804
+ provider: entry.id
805
+ })));
806
+ const hostProvider = hostProviders.find((entry) => entry.id === providerDraft) ?? hostProviders.find((entry) => entry.id === (options.estimatorProvider ?? ""));
807
+ const hasKey = (options.estimatorApiKey ?? "") !== "";
808
+ const commit = (patch) => {
809
+ settle(() => save(patch));
810
+ };
811
+ const overrideProvider = options.estimatorProvider ?? "";
812
+ const overrideModel = options.estimatorModel ?? "";
813
+ const effectiveProvider = overrideProvider !== "" ? overrideProvider : catalog?.selection?.provider ?? "";
814
+ const effectiveModel = overrideModel !== "" ? overrideModel : catalog?.selection?.model ?? "";
815
+ const effectiveRoute = effectiveProvider !== "" && effectiveModel !== "" ? `${effectiveProvider} / ${effectiveModel}` : t("estimator.hostUnresolved");
816
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
817
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompact,
818
+ "aria-labelledby": "context-compression-estimator-title",
819
+ children: [
820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
821
+ id: "context-compression-estimator-title",
822
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactTitle,
823
+ children: t("estimator.title")
824
+ }),
825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
826
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
827
+ children: t("estimator.description")
828
+ }),
829
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
830
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
831
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.mode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
832
+ value: mode,
833
+ disabled,
834
+ onChange: (event) => {
835
+ settle(() => save({ estimatorMode: event.currentTarget.value }));
836
+ },
837
+ children: [
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
839
+ value: "",
840
+ children: t("estimator.mode.off")
841
+ }),
842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
843
+ value: "host",
844
+ children: t("estimator.mode.host")
845
+ }),
846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
847
+ value: "direct",
848
+ children: t("estimator.mode.direct")
849
+ })
850
+ ]
851
+ })]
852
+ }),
853
+ mode === "" ? null : mode === "host" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
854
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
855
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
856
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.provider") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
857
+ type: "text",
858
+ list: "estimator-provider-options",
859
+ value: provider,
860
+ disabled,
861
+ placeholder: t("estimator.provider.placeholder"),
862
+ onChange: (event) => {
863
+ const next = event.currentTarget.value;
864
+ setProvider(next);
865
+ if (next !== "" && hostProviders.some((entry) => entry.id === next)) commit({ estimatorProvider: next });
866
+ },
867
+ onBlur: () => {
868
+ if (provider !== (options.estimatorProvider ?? "")) commit({ estimatorProvider: provider });
869
+ }
870
+ })]
871
+ }),
872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
873
+ id: "estimator-provider-options",
874
+ children: hostProviders.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
875
+ value: entry.id,
876
+ children: [entry.name === "" ? entry.id : entry.name, entry.error === void 0 ? "" : ` (${entry.error})`]
877
+ }, entry.id))
878
+ }),
879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
880
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
882
+ type: "text",
883
+ list: "estimator-model-options",
884
+ value: model,
885
+ disabled,
886
+ placeholder: t("estimator.model.placeholder"),
887
+ onChange: (event) => {
888
+ const next = event.currentTarget.value;
889
+ setModel(next);
890
+ if (next !== "" && hostModels.some((entry) => entry.id === next)) commit({ estimatorModel: next });
891
+ },
892
+ onBlur: () => {
893
+ if (model !== (options.estimatorModel ?? "")) commit({ estimatorModel: model });
894
+ }
895
+ })]
896
+ }),
897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
898
+ id: "estimator-model-options",
899
+ children: hostModels.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
900
+ value: entry.id,
901
+ children: entry.name
902
+ }, `${entry.provider}\0${entry.id}`))
903
+ }),
904
+ hostProvider?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
905
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
906
+ children: String(hostProvider.error)
907
+ }),
908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
909
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
910
+ children: t("estimator.hostReuse")
911
+ }),
912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
913
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
914
+ children: t("estimator.hostRoute").replace("{route}", effectiveRoute)
915
+ })
916
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
918
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.baseUrl") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
920
+ type: "text",
921
+ value: baseUrl,
922
+ disabled,
923
+ placeholder: "https://127.0.0.1:8000/v1",
924
+ onChange: (event) => {
925
+ setBaseUrl(event.currentTarget.value);
926
+ },
927
+ onBlur: () => {
928
+ if (baseUrl !== (options.estimatorBaseUrl ?? "")) commit({ estimatorBaseUrl: baseUrl });
929
+ }
930
+ })]
931
+ }),
932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
933
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
934
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
935
+ type: "text",
936
+ value: model,
937
+ disabled,
938
+ placeholder: t("estimator.model.placeholder"),
939
+ onChange: (event) => {
940
+ setModel(event.currentTarget.value);
941
+ },
942
+ onBlur: () => {
943
+ if (model !== (options.estimatorModel ?? "")) commit({ estimatorModel: model });
944
+ }
945
+ })]
946
+ }),
947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
948
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
949
+ children: [
950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("estimator.apiKey") }),
951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
952
+ style: {
953
+ display: "flex",
954
+ gap: "6px"
955
+ },
956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
957
+ type: "password",
958
+ autoComplete: "off",
959
+ spellCheck: false,
960
+ value: keyDraft,
961
+ disabled,
962
+ placeholder: hasKey ? t("estimator.apiKey.set") : t("estimator.apiKey.placeholder"),
963
+ onChange: (event) => {
964
+ setKeyDraft(event.currentTarget.value);
965
+ },
966
+ onBlur: () => {
967
+ const next = keyDraft.trim();
968
+ if (next === "") return;
969
+ settle(() => save({ estimatorApiKey: next }));
970
+ setKeyDraft("");
971
+ },
972
+ onKeyDown: (event) => {
973
+ if (event.key === "Enter") event.currentTarget.blur();
974
+ }
975
+ }), hasKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
976
+ type: "button",
977
+ disabled,
978
+ onClick: () => {
979
+ settle(() => save({ estimatorApiKey: void 0 }));
980
+ },
981
+ children: t("estimator.apiKey.clear")
982
+ }) : null]
983
+ }),
984
+ hasKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
985
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
986
+ children: t("estimator.apiKey.overwrite")
987
+ }) : null
988
+ ]
989
+ })
990
+ ] })
991
+ ]
992
+ });
993
+ }
994
+ //#endregion
995
+ //#region src/client/settings-section.tsx
996
+ /**
997
+ * Full-page Settings surface backed by the same durable selector state.
998
+ *
999
+ * Extracted from CompressionProfileSelector.tsx to reduce god-module size.
1000
+ *
1001
+ * @module dsh-context-compression-improved/client/settings-section
1002
+ */
1003
+ /** Full-page Settings surface backed by the same durable selector state. */
1004
+ function ContextCompressionSettingsSection(props) {
1005
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCompressionProfileControls, { ...props });
1006
+ }
1007
+ function SettingsCompressionProfileControls({ useCompression, useSessions, select, saveCustom, resetCustom, saveAutoCompact, saveCodeSkeleton, savePresetOptions, t }) {
1008
+ const state = useCompression((snapshot) => snapshot);
1009
+ const selectorAvailable = useSessions((sessions) => {
1010
+ const current = sessions.current;
1011
+ return current === void 0 ? void 0 : sessions.byId[current]?.agentPreset;
1012
+ }) !== "minimal";
1013
+ const [saving, setSaving] = (0, react.useState)(false);
1014
+ const [saveError, setSaveError] = (0, react.useState)(null);
1015
+ const [draft, setDraft] = (0, react.useState)(null);
1016
+ const current = state.value?.profile ?? "balanced";
1017
+ (0, react.useEffect)(() => {
1018
+ const custom = state.value?.custom;
1019
+ setDraft(current === "custom" && custom !== void 0 ? editableCustom(custom) : null);
1020
+ }, [current, state.value?.custom]);
1021
+ if (state.status === "unavailable") return null;
1022
+ const busy = state.status === "loading" || saving;
1023
+ const selectProfile = (profile) => {
1024
+ if (!selectorAvailable || !state.writable || profile === current) return;
1025
+ setSaveError(null);
1026
+ setSaving(true);
1027
+ select(profile).then(() => {
1028
+ setSaving(false);
1029
+ }, (error) => {
1030
+ setSaving(false);
1031
+ setSaveError(error instanceof Error && error.message !== "" ? error.message : t("status.saveFailed"));
1032
+ });
1033
+ };
1034
+ const settle = (operation) => {
1035
+ setSaveError(null);
1036
+ setSaving(true);
1037
+ operation().then(() => {
1038
+ setSaving(false);
1039
+ }, (error) => {
1040
+ setSaving(false);
1041
+ setSaveError(error instanceof Error && error.message !== "" ? error.message : t("status.saveFailed"));
1042
+ });
1043
+ };
1044
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1045
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.settingsSection,
1046
+ children: [
1047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1048
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.settingsTitle,
1049
+ children: t("settings.title")
1050
+ }),
1051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1052
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.settingsDescription,
1053
+ children: t("settings.description")
1054
+ }),
1055
+ selectorAvailable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1056
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileGrid,
1057
+ "aria-label": t("label"),
1058
+ children: COMPRESSION_PROFILES.map((profile) => {
1059
+ const selected = profile === current;
1060
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1061
+ type: "button",
1062
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileCard,
1063
+ "aria-pressed": selected,
1064
+ disabled: busy || !state.writable,
1065
+ onClick: () => {
1066
+ selectProfile(profile);
1067
+ },
1068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1069
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileCardTop,
1070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1071
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileCardTitle,
1072
+ children: t(`profile.${profile}`)
1073
+ }), selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1074
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileCurrent,
1075
+ children: t("profile.current")
1076
+ }) : null]
1077
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1078
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.profileCardDetail,
1079
+ children: t(`detail.${profile}`)
1080
+ })]
1081
+ }, profile);
1082
+ })
1083
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1084
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.unavailable,
1085
+ role: "status",
1086
+ children: t("status.minimalUnavailable")
1087
+ }),
1088
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoCompactThresholdControls, {
1089
+ value: state.value?.autoCompact?.thresholdPercent ?? AUTO_COMPACT_THRESHOLD_LIMITS.default,
1090
+ disabled: busy || !state.writable || !selectorAvailable,
1091
+ save: saveAutoCompact,
1092
+ settle,
1093
+ t
1094
+ }),
1095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeSkeletonControls, {
1096
+ value: state.value?.codeSkeleton?.enabled ?? false,
1097
+ disabled: busy || !state.writable || !selectorAvailable,
1098
+ save: saveCodeSkeleton,
1099
+ settle,
1100
+ t
1101
+ }),
1102
+ current !== "tokenpilot-inspired" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorInactiveNotice, {
1103
+ profile: t(`profile.${current}`),
1104
+ t
1105
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorControls, {
1106
+ options: state.value?.presetOptions ?? {},
1107
+ disabled: busy || !state.writable || !selectorAvailable,
1108
+ save: savePresetOptions,
1109
+ settle,
1110
+ t
1111
+ }),
1112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1113
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.pricing,
1114
+ children: t("pricing.disclosure")
1115
+ }),
1116
+ current !== "custom" || draft === null || !selectorAvailable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomPolicyEditor, {
1117
+ value: draft,
1118
+ disabled: busy || !state.writable,
1119
+ setValue: setDraft,
1120
+ save: () => saveCustom(structuredClone(draft)),
1121
+ reset: resetCustom,
1122
+ settle,
1123
+ t
1124
+ }),
1125
+ saveError === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1126
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.error,
1127
+ role: "alert",
1128
+ children: saveError
1129
+ })
1130
+ ]
1131
+ });
1132
+ }
1133
+ //#endregion
1134
+ //#region src/client/decode.ts
1135
+ /** Browser-safe settings decoding shared by the client entry and node tests. */
1136
+ /**
1137
+ * Decode one stored context-compression settings document with exactly the
1138
+ * runtime schema's strictness: a plain object with only `profile`, `custom`,
1139
+ * `autoCompact`, and `codeSkeleton` keys, a supported profile, a valid Custom
1140
+ * document canonicalized to v3 exactly as the runtime resolver would, a
1141
+ * strictly-shaped autoCompact section (absent inherits the 80% default), and
1142
+ * a strictly-shaped codeSkeleton gate (absent inherits off). Anything else
1143
+ * decodes to `undefined` so the UI reports the document as unreadable instead
1144
+ * of silently disagreeing with the runtime.
1145
+ */
1146
+ function decodeSettings(value) {
1147
+ if (!isPlainRecord(value)) return void 0;
1148
+ if (Object.keys(value).some((key) => key !== "profile" && key !== "custom" && key !== "autoCompact" && key !== "codeSkeleton" && key !== "presetOptions")) return;
1149
+ const profile = value.profile;
1150
+ const custom = value.custom;
1151
+ const autoCompact = decodeAutoCompactSettings(value.autoCompact);
1152
+ const codeSkeleton = decodeCodeSkeletonSettings(value.codeSkeleton);
1153
+ const presetOptions = decodePresetOptionsSettings(value.presetOptions);
1154
+ return typeof profile === "string" && COMPRESSION_PROFILES.includes(profile) && isCustomCompressionPolicy(custom) && autoCompact !== void 0 && codeSkeleton !== void 0 ? {
1155
+ profile,
1156
+ custom: canonicalizeCustomPolicy(custom),
1157
+ autoCompact,
1158
+ codeSkeleton,
1159
+ ...presetOptions === void 0 ? {} : { presetOptions }
1160
+ } : void 0;
1161
+ }
1162
+ //#endregion
1163
+ //#region src/client/locales.ts
1164
+ /** Simplified Chinese copy for the context-compression selector. */
1165
+ const zh = {
1166
+ "nav": "上下文压缩选择器",
1167
+ "settings.title": "上下文压缩选择器",
1168
+ "settings.description": "为当前会话选择压缩 Profile,并配置该 Profile 提供的参数。",
1169
+ "label": "上下文压缩",
1170
+ "status.loading": "加载中",
1171
+ "status.unavailable": "不可用",
1172
+ "status.presetUnavailable": "此会话的 preset 未提供上下文压缩,或能力尚未确认。",
1173
+ "status.minimalUnavailable": "极简模式不会为此会话加载上下文压缩能力,因此选择器在本会话中等效为关闭,仅保留 Harness 原生行为。切换到标准、PTC/Coding、创造模式,或支持该能力的自定义 preset 后即可配置。",
1174
+ "status.saveFailed": "保存失败,请重试",
1175
+ "pricing.disclosure": "DeepSeek 官方价格目录复核于 2026-08-25。Asia/Shanghai 周一至周五 09:00–12:00、14:00–18:00 为峰时,其余为谷时;跨边界请求按成本区间处理。",
1176
+ "profile.balanced": "平衡模式",
1177
+ "profile.cache-strict": "Cache Strict(前缀保护)",
1178
+ "profile.savings": "节省模式",
1179
+ "profile.adaptive": "Adaptive(保守成本)",
1180
+ "profile.tokenpilot-inspired": "TokenPilot 启发模式",
1181
+ "estimator.title": "估计器(可选)",
1182
+ "estimator.description": "辅助小模型零样本判断旧文件读取是否仍有引用价值,仅建议性加速历史清理;未配置或失败时自动退回纯规则通道,不影响主流程。",
1183
+ "estimator.mode": "通道",
1184
+ "estimator.mode.off": "关闭(纯规则)",
1185
+ "estimator.followHost": "跟随宿主默认模型",
1186
+ "estimator.mode.host": "宿主模型(复用已配置供应商)",
1187
+ "estimator.mode.direct": "直连 OpenAI 兼容端点",
1188
+ "estimator.inactive": "估计器只随「TokenPilot 启发模式」提供:该模式的预设选项(去重指针、摘要定位块、读取状态语义与估计器通道)不会合并进其他 Profile,因此当前 Profile「{profile}」下没有可配置的估计器通道。选择「TokenPilot 启发模式」后,本区块会出现「通道」选择,可复用已配置供应商(宿主模型)或直连 OpenAI 兼容端点。",
1189
+ "estimator.provider": "供应商",
1190
+ "estimator.provider.placeholder": "留空则跟随会话默认模型,可从下拉选择或自定义输入",
1191
+ "estimator.model.placeholder": "留空则跟随会话默认模型,可从下拉选择或自定义输入",
1192
+ "estimator.hostReuse": "宿主通道直接复用你在 DSH 中已配置的供应商与凭据,无需填写 API Key;此通道也不接收 API Key。",
1193
+ "estimator.hostRoute": "当前生效路由:{route}。",
1194
+ "estimator.hostUnresolved": "尚未确定(请在 DSH 设置中选择默认模型,或在此指定供应商与模型)",
1195
+ "estimator.baseUrl": "端点地址(/v1)",
1196
+ "estimator.model": "模型",
1197
+ "estimator.apiKey": "API Key(仅写入,不回显)",
1198
+ "estimator.apiKey.placeholder": "输入端点密钥并失焦保存",
1199
+ "estimator.apiKey.set": "已设置 · 输入新值覆盖",
1200
+ "estimator.apiKey.clear": "清除",
1201
+ "estimator.apiKey.overwrite": "已设置保密值,输入新值并失焦即可覆盖。",
1202
+ "detail.tokenpilot-inspired": "在平衡模式之上叠加去重指针、恢复豁免、摘要定位块、前缀稳定与读取状态语义;估计器需另行配置端点",
1203
+ "profile.custom": "Custom/实验模式",
1204
+ "profile.native": "原生对照",
1205
+ "profile.off": "插件关闭",
1206
+ "profile.current": "当前选择",
1207
+ "detail.balanced": "确定性压缩新工具结果;高水位时老化旧结果",
1208
+ "detail.cache-strict": "仅在确认容量压力时老化已发送历史;服务端缓存命中仍是 best-effort",
1209
+ "detail.savings": "使用更小目标并更早清理旧工具结果;不保证每个请求更便宜",
1210
+ "detail.adaptive": "Fresh/Aggregate 与平衡模式一致;仅当紧邻官方 usage 与当前官方价格证明历史压缩明确更省钱时老化历史,否则保留",
1211
+ "detail.custom": "为新会话选择已实现的压缩阶段和计量阈值",
1212
+ "detail.native": "只使用 DeepSeek Harness 原生头尾裁剪",
1213
+ "detail.off": "关闭确定性选择器;原生 auto-compact 仍由 Harness 配置决定",
1214
+ "autoCompact.title": "Auto Compact 触发水位",
1215
+ "autoCompact.description": "模型驱动 Auto Compact 在请求占用达到该水位时触发。调整后,标准 Profile 的 History 触发值、最小回收量与近期尾窗随水位联动;修改只影响新会话。",
1216
+ "autoCompact.inputLabel": "Auto Compact 阈值(%)",
1217
+ "autoCompact.sliderLabel": "Auto Compact 阈值滑杆",
1218
+ "autoCompact.quick": "快捷值",
1219
+ "autoCompact.riskLow": "低于推荐范围:更早触发会增加摘要调用与前缀重建。",
1220
+ "autoCompact.riskHigh": "高于推荐范围:上下文容量为请求与输出共享,过晚触发会减少单次大输出、推理与工具 schema 的余量。",
1221
+ "autoCompact.invalid": "Auto Compact 阈值必须是 50–90 之间的整数。",
1222
+ "autoCompact.save": "保存 Auto Compact 阈值",
1223
+ "autoCompact.summaryHint": "Auto Compact 阈值:{percent}%。可在设置中修改。",
1224
+ "codeSkeleton.title": "代码骨架压缩(备选)",
1225
+ "codeSkeleton.description": "正交开关:独立于上方 Profile。开启后,首次曝光的超大源码类工具结果会先尝试保留导入与声明的骨架(省略函数体并保留错误行),失败时自动回退到原头部裁剪;需要精确 tokenizer,修改只影响新会话。",
1226
+ "codeSkeleton.enabled": "代码骨架压缩",
1227
+ "codeSkeleton.enabled.on": "开",
1228
+ "codeSkeleton.enabled.off": "关(默认)",
1229
+ "custom.title": "Custom 策略",
1230
+ "custom.settingsHint": "具体参数请前往“设置 > 上下文压缩选择器”中编辑。",
1231
+ "custom.sessionScope": "保存后的修改会在当前压缩运行时随后首次观察某个 Session 时生效;已被该运行时观察的 Session 继续使用其冻结策略。",
1232
+ "custom.measurement": "首选 DeepSeek 精确 tokenizer;不可用时回退到带校准的 tokenizer estimate,绝不使用 chars/4。缓存归因仍未知。",
1233
+ "custom.unit": "规范单位",
1234
+ "custom.unit.tokens": "Tokens",
1235
+ "custom.unit.contextPercent": "上下文百分比",
1236
+ "custom.enabled": "是否启用",
1237
+ "custom.enabled.on": "开",
1238
+ "custom.enabled.off": "关",
1239
+ "custom.fresh.enabled": "启用 Fresh",
1240
+ "custom.fresh.trigger": "Fresh 触发值",
1241
+ "custom.fresh.target": "Fresh 目标值",
1242
+ "custom.aggregate.enabled": "启用 Aggregate",
1243
+ "custom.aggregate.trigger": "Aggregate 触发值",
1244
+ "custom.aggregate.target": "Aggregate 目标值",
1245
+ "custom.history.enabled": "启用 History",
1246
+ "custom.history.trigger": "History 触发值",
1247
+ "custom.history.keepRecentToolCalls": "保护近期工具调用数",
1248
+ "custom.history.keepRecentTokens": "保护近期工具结果尾窗",
1249
+ "custom.history.minReclaim": "最小回收量",
1250
+ "custom.prefixPolicy": "已发送前缀策略",
1251
+ "custom.prefixPolicy.preserve": "仅在容量压力时改写",
1252
+ "custom.prefixPolicy.pressureBreak": "允许常规历史老化",
1253
+ "custom.experimental": "Experimental:以下功能仅用于 Custom,不会加入标准 Profile。",
1254
+ "custom.tailTrim.enabled": "启用 TailTrim(实验)",
1255
+ "custom.tailTrim.trigger": "TailTrim 触发值",
1256
+ "custom.tailTrim.warning": "TailTrim 只在精确 tokenizer 可用时,把一个完整、已结束且非错误的纯工具组替换为可恢复引用;它与 History 共用近期工具调用数、工具结果尾窗和最小回收参数。它会改写已发送前缀,可能降低缓存命中。",
1257
+ "custom.save": "保存 Custom 策略",
1258
+ "custom.reset": "重置 Custom 策略",
1259
+ "custom.invalid": "Custom 策略参数无效。"
1260
+ };
1261
+ /** English copy matching every simplified Chinese selector key. */
1262
+ const en = {
1263
+ "nav": "Context compression selector",
1264
+ "settings.title": "Context compression selector",
1265
+ "settings.description": "Choose a compression profile for the current session and configure the parameters it provides.",
1266
+ "label": "Context compression",
1267
+ "status.loading": "Loading",
1268
+ "status.unavailable": "Unavailable",
1269
+ "status.presetUnavailable": "This session’s preset does not provide context compression, or availability is not yet confirmed.",
1270
+ "status.minimalUnavailable": "Minimal mode does not load context compression for this session. The selector is effectively off and Harness native behavior remains. Switch to Standard, PTC / Coding, Creative, or a capable custom preset to configure it.",
1271
+ "status.saveFailed": "Save failed. Try again.",
1272
+ "pricing.disclosure": "DeepSeek official prices checked 2026-08-25. Peak Mon–Fri 09:00–12:00 and 14:00–18:00 Asia/Shanghai; otherwise off-peak. Cross-boundary requests use a cost range.",
1273
+ "profile.balanced": "Balanced",
1274
+ "profile.cache-strict": "Cache Strict (prefix protection)",
1275
+ "profile.savings": "Savings",
1276
+ "profile.adaptive": "Adaptive (conservative cost)",
1277
+ "profile.tokenpilot-inspired": "TokenPilot-inspired",
1278
+ "estimator.title": "Estimator (optional)",
1279
+ "estimator.description": "A small auxiliary model zero-shots whether old file reads are still likely referenced, advisory-only for history aging; unconfigured or failing endpoints fall back to rule-only behavior.",
1280
+ "estimator.mode": "Channel",
1281
+ "estimator.mode.off": "Off (rule-only)",
1282
+ "estimator.followHost": "Follow the host default model",
1283
+ "estimator.mode.host": "Host model (reuse configured providers)",
1284
+ "estimator.mode.direct": "Direct OpenAI-compatible endpoint",
1285
+ "estimator.inactive": "The estimator ships only with the TokenPilot-inspired profile: that profile’s preset options (dedupe pointers, summary locators, read-state semantics, and the estimator channel) are never merged into another profile, so the current profile “{profile}” has no estimator channel to configure. Select TokenPilot-inspired and this section gains a Channel choice — reuse configured providers (host model) or a direct OpenAI-compatible endpoint.",
1286
+ "estimator.provider": "Provider",
1287
+ "estimator.provider.placeholder": "Empty follows the session default model; pick from the dropdown or type a custom id",
1288
+ "estimator.model.placeholder": "Empty follows the session default model; pick from the dropdown or type a custom id",
1289
+ "estimator.hostReuse": "The host channel reuses the providers and credentials you already configured in DSH, so no API key is needed — and none is accepted here.",
1290
+ "estimator.hostRoute": "Effective route: {route}.",
1291
+ "estimator.hostUnresolved": "not determined yet (choose a default model in DSH settings, or name a provider and model here)",
1292
+ "estimator.baseUrl": "Endpoint base URL (/v1)",
1293
+ "estimator.model": "Model",
1294
+ "estimator.apiKey": "API key (write-only, never echoed)",
1295
+ "estimator.apiKey.placeholder": "Type the endpoint key; blur to save",
1296
+ "estimator.apiKey.set": "Set · type a new value to overwrite",
1297
+ "estimator.apiKey.clear": "Clear",
1298
+ "estimator.apiKey.overwrite": "A secret is stored; type a new value and blur to overwrite it.",
1299
+ "detail.tokenpilot-inspired": "Layered on Balanced: dedupe pointers, recovery exemption, summary locators, prefix stabilization, and read-state semantics; the estimator needs an endpoint configured separately",
1300
+ "profile.custom": "Custom / Experimental",
1301
+ "profile.native": "Native baseline",
1302
+ "profile.off": "Plugin off",
1303
+ "profile.current": "Current profile",
1304
+ "detail.balanced": "Reduce fresh tool results deterministically; age old results at high watermarks",
1305
+ "detail.cache-strict": "Age sent history only under confirmed capacity pressure; provider cache hits remain best-effort",
1306
+ "detail.savings": "Use smaller targets and age old tool results earlier; does not guarantee a cheaper request",
1307
+ "detail.adaptive": "Use Balanced Fresh/Aggregate; age history only when adjacent official usage and current official prices prove a clear saving",
1308
+ "detail.custom": "Choose implemented stages and measured thresholds for new sessions",
1309
+ "detail.native": "Use only the Harness native head/tail pruner",
1310
+ "detail.off": "Disable the deterministic selector; native auto-compact remains separately configured",
1311
+ "autoCompact.title": "Auto Compact trigger level",
1312
+ "autoCompact.description": "Model-driven Auto Compact triggers once request usage crosses this level. Standard-profile History triggers, minimum reclaim, and the recent tail follow the level; changes affect new sessions only.",
1313
+ "autoCompact.inputLabel": "Auto Compact threshold (%)",
1314
+ "autoCompact.sliderLabel": "Auto Compact threshold slider",
1315
+ "autoCompact.quick": "Quick values",
1316
+ "autoCompact.riskLow": "Below the recommended band: triggering earlier increases summarization calls and prefix rebuilds.",
1317
+ "autoCompact.riskHigh": "Above the recommended band: context capacity is shared by requests and output, so triggering later reduces headroom for single large outputs, reasoning, and tool schemas.",
1318
+ "autoCompact.invalid": "The Auto Compact threshold must be an integer between 50 and 90.",
1319
+ "autoCompact.save": "Save Auto Compact threshold",
1320
+ "autoCompact.summaryHint": "Auto Compact threshold: {percent}%. Change it in Settings.",
1321
+ "codeSkeleton.title": "Code skeleton compression (opt-in)",
1322
+ "codeSkeleton.description": "Orthogonal switch, independent of the profiles above. When enabled, an oversized fresh source-code tool result first tries a skeleton that keeps imports and declarations (bodies elided, error lines kept) and falls back to the original head pruning on failure. Requires the exact tokenizer; changes affect new sessions only.",
1323
+ "codeSkeleton.enabled": "Code skeleton compression",
1324
+ "codeSkeleton.enabled.on": "On",
1325
+ "codeSkeleton.enabled.off": "Off (default)",
1326
+ "custom.title": "Custom policy",
1327
+ "custom.settingsHint": "Edit detailed parameters in Settings > Context compression selector.",
1328
+ "custom.sessionScope": "Saved changes apply when the current compression runtime next observes a Session for the first time. A Session already observed by that runtime keeps its frozen policy.",
1329
+ "custom.measurement": "Exact DeepSeek tokenizer first; tokenizer estimate with calibration fallback. Never chars/4. Cache attribution remains unknown.",
1330
+ "custom.unit": "Canonical unit",
1331
+ "custom.unit.tokens": "Tokens",
1332
+ "custom.unit.contextPercent": "Context percent",
1333
+ "custom.enabled": "Enabled",
1334
+ "custom.enabled.on": "On",
1335
+ "custom.enabled.off": "Off",
1336
+ "custom.fresh.enabled": "Enable Fresh",
1337
+ "custom.fresh.trigger": "Fresh trigger",
1338
+ "custom.fresh.target": "Fresh target",
1339
+ "custom.aggregate.enabled": "Enable Aggregate",
1340
+ "custom.aggregate.trigger": "Aggregate trigger",
1341
+ "custom.aggregate.target": "Aggregate target",
1342
+ "custom.history.enabled": "Enable History",
1343
+ "custom.history.trigger": "History trigger",
1344
+ "custom.history.keepRecentToolCalls": "Protected recent tool calls",
1345
+ "custom.history.keepRecentTokens": "Protected recent tool-result tail",
1346
+ "custom.history.minReclaim": "Minimum reclaim",
1347
+ "custom.prefixPolicy": "Sent-prefix policy",
1348
+ "custom.prefixPolicy.preserve": "Preserve until capacity pressure",
1349
+ "custom.prefixPolicy.pressureBreak": "Allow routine history aging",
1350
+ "custom.experimental": "Experimental: these controls are Custom-only and never added to standard profiles.",
1351
+ "custom.tailTrim.enabled": "Enable TailTrim (experimental)",
1352
+ "custom.tailTrim.trigger": "TailTrim trigger",
1353
+ "custom.tailTrim.warning": "TailTrim requires the exact tokenizer and replaces at most one complete, finished, non-error tool-only group with a recoverable reference. It shares Protected recent tool calls, Protected recent tool-result tail, and Minimum reclaim with History. It rewrites a sent prefix and may reduce cache hits.",
1354
+ "custom.save": "Save Custom policy",
1355
+ "custom.reset": "Reset Custom policy",
1356
+ "custom.invalid": "Custom policy values are invalid."
1357
+ };
1358
+ //#endregion
1359
+ //#region src/client/preset-options.ts
1360
+ /** Every field a patch may address, in the schema's own order. */
1361
+ const PRESET_OPTION_KEYS = [
1362
+ "dedupeToolResults",
1363
+ "summaryLocator",
1364
+ "prefixStabilizer",
1365
+ "readState",
1366
+ "estimatorMode",
1367
+ "estimatorProvider",
1368
+ "estimatorModel",
1369
+ "estimatorBaseUrl",
1370
+ "estimatorApiKey",
1371
+ "estimatorTimeoutMs"
1372
+ ];
1373
+ /**
1374
+ * Apply one patch over the stored section.
1375
+ *
1376
+ * @param current - the decoded `presetOptions` section, when one is stored.
1377
+ * @param patch - the fields to write or clear.
1378
+ * @returns the complete section to store.
1379
+ */
1380
+ function mergePresetOptionsPatch(current, patch) {
1381
+ const source = current;
1382
+ const merged = {};
1383
+ for (const key of PRESET_OPTION_KEYS) {
1384
+ const stored = source?.[key];
1385
+ if (stored !== void 0) merged[key] = stored;
1386
+ }
1387
+ for (const key of Object.keys(patch)) {
1388
+ const value = patch[key];
1389
+ if (value === void 0) delete merged[key];
1390
+ else merged[key] = value;
1391
+ }
1392
+ return merged;
1393
+ }
1394
+ /**
1395
+ * Compare two sections field by field, so an unchanged patch neither rewrites
1396
+ * the document nor reports a save the Host never committed.
1397
+ *
1398
+ * @param left - one section (or none).
1399
+ * @param right - the other section.
1400
+ * @returns whether every known field holds the same value.
1401
+ */
1402
+ function presetOptionsEqual(left, right) {
1403
+ const a = left;
1404
+ const b = right;
1405
+ return PRESET_OPTION_KEYS.every((key) => a?.[key] === b?.[key]);
1406
+ }
1407
+ //#endregion
1408
+ //#region src/client/index.ts
1409
+ const inject = [
1410
+ "slots",
1411
+ "locale",
1412
+ "settingsScope"
1413
+ ];
1414
+ const NS = "context-compression";
1415
+ function sameCustomPolicy(left, right) {
1416
+ if (left.version !== 3 || right.version !== 3) return false;
1417
+ return left.version === right.version && left.unit === right.unit && left.prefixPolicy === right.prefixPolicy && left.fresh.enabled === right.fresh.enabled && left.fresh.trigger === right.fresh.trigger && left.fresh.target === right.fresh.target && left.aggregate.enabled === right.aggregate.enabled && left.aggregate.trigger === right.aggregate.trigger && left.aggregate.target === right.aggregate.target && left.history.enabled === right.history.enabled && left.history.trigger === right.history.trigger && left.history.keepRecentToolCalls === right.history.keepRecentToolCalls && left.history.keepRecentTokens === right.history.keepRecentTokens && left.history.minReclaim === right.history.minReclaim && left.tailTrim.enabled === right.tailTrim.enabled && left.tailTrim.trigger === right.tailTrim.trigger;
1418
+ }
1419
+ function apply(ctx) {
1420
+ ctx.effect(() => ctx.locale.register(NS, {
1421
+ zh,
1422
+ en
1423
+ }), "ui-context-compression: dictionaries");
1424
+ const scope = ctx.settingsScope.bind({
1425
+ namespace: NS,
1426
+ decode: decodeSettings
1427
+ });
1428
+ const writeAndConfirm = async (write, accepts) => {
1429
+ const beforeRevision = scope.getSnapshot().revision;
1430
+ await write();
1431
+ const after = scope.getSnapshot();
1432
+ if (after.status !== "ready" || after.value === void 0 || after.revision === beforeRevision || !accepts(after.value)) throw new Error("Context compression settings were not saved.");
1433
+ };
1434
+ const injected = () => ({
1435
+ hooks: { compression: scope },
1436
+ select: (profile) => writeAndConfirm(() => scope.set("profile", profile), (settings) => settings.profile === profile),
1437
+ saveCustom: (custom) => writeAndConfirm(() => scope.set("custom", custom), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, custom)),
1438
+ resetCustom: () => writeAndConfirm(() => scope.set("custom", structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY)), (settings) => isCustomCompressionPolicy(settings.custom) && sameCustomPolicy(settings.custom, DEFAULT_CUSTOM_COMPRESSION_POLICY)),
1439
+ saveAutoCompact: (thresholdPercent) => writeAndConfirm(() => scope.set("autoCompact", { thresholdPercent }), (settings) => settings.autoCompact.thresholdPercent === thresholdPercent),
1440
+ saveCodeSkeleton: (enabled) => writeAndConfirm(() => scope.set("codeSkeleton", { enabled }), (settings) => settings.codeSkeleton.enabled === enabled),
1441
+ savePresetOptions: (options) => {
1442
+ const current = scope.getSnapshot().value?.presetOptions;
1443
+ const next = mergePresetOptionsPatch(current, options);
1444
+ if (presetOptionsEqual(current, next)) return Promise.resolve();
1445
+ return writeAndConfirm(() => scope.set("presetOptions", next), (settings) => presetOptionsEqual(settings.presetOptions, next));
1446
+ }
1447
+ });
1448
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1449
+ name: "settings.section",
1450
+ id: "context-compression",
1451
+ order: 17,
1452
+ label: () => ctx.locale.bind(NS)("nav"),
1453
+ locale: NS,
1454
+ inject: injected
1455
+ }, ContextCompressionSettingsSection));
1456
+ }
1457
+ //#endregion
1458
+ exports.apply = apply;
1459
+ exports.inject = inject;
1460
+ return module.exports;
1461
+ }
1462
+ });