dsh-context-compression-improved 0.3.0 → 0.4.0-beta.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.
Files changed (41) hide show
  1. package/.githooks/pre-push +37 -0
  2. package/package.json +3 -2
  3. package/packages/selector/cordis.patch.yml +12 -5
  4. package/packages/selector/lib/client.d.ts +24 -0
  5. package/packages/selector/lib/client.js +506 -5
  6. package/packages/selector/lib/config.js +27 -4
  7. package/packages/selector/lib/index.d.ts +7 -0
  8. package/packages/selector/lib/index.js +229 -1
  9. package/packages/selector/lib/pruner.d.ts +254 -0
  10. package/packages/selector/lib/pruner.js +714 -25
  11. package/packages/selector/package.json +0 -1
  12. package/packages/selector/src/client/EstimatorControls.tsx +101 -0
  13. package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
  14. package/packages/selector/src/client/index.ts +17 -0
  15. package/packages/selector/src/client/locales.ts +38 -0
  16. package/packages/selector/src/client/preset-options.ts +1 -0
  17. package/packages/selector/src/client/review-scope.ts +16 -0
  18. package/packages/selector/src/client/settings-section.tsx +17 -8
  19. package/packages/selector/src/index.ts +308 -0
  20. package/packages/selector/src/profiles.ts +28 -1
  21. package/packages/selector/src/pruner/state.ts +27 -0
  22. package/packages/selector/src/pruner.ts +430 -10
  23. package/packages/selector/src/runtime/audit.ts +27 -0
  24. package/packages/selector/src/runtime/config.ts +33 -1
  25. package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
  26. package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
  27. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
  28. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
  29. package/packages/selector/src/runtime/types.ts +17 -0
  30. package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
  31. package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
  32. package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
  33. package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
  34. package/packages/selector/tests/review-routes.host.spec.ts +290 -0
  35. package/packages/selector/tests/runtime/audit.spec.ts +44 -0
  36. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
  37. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
  38. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
  39. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
  40. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
  41. package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
@@ -78,14 +78,19 @@ window.__ModuleLoader__.load({
78
78
  "estimatorModel",
79
79
  "estimatorBaseUrl",
80
80
  "estimatorApiKey",
81
- "estimatorTimeoutMs"
81
+ "estimatorTimeoutMs",
82
+ "reviewMode",
83
+ "reviewTimeoutTurns",
84
+ "cacheHitDiscountAlpha",
85
+ "reviewHighImpactTokens"
82
86
  ]);
83
87
  if (Object.keys(value).some((key) => !allowed.has(key))) return void 0;
84
88
  for (const key of [
85
89
  "dedupeToolResults",
86
90
  "summaryLocator",
87
91
  "prefixStabilizer",
88
- "readState"
92
+ "readState",
93
+ "reviewMode"
89
94
  ]) {
90
95
  const entry = value[key];
91
96
  if (entry !== void 0 && typeof entry !== "boolean") return void 0;
@@ -103,6 +108,12 @@ window.__ModuleLoader__.load({
103
108
  }
104
109
  const estimatorTimeoutMs = value.estimatorTimeoutMs;
105
110
  if (estimatorTimeoutMs !== void 0 && (typeof estimatorTimeoutMs !== "number" || !Number.isSafeInteger(estimatorTimeoutMs) || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 6e4)) return;
111
+ const reviewTimeoutTurns = value.reviewTimeoutTurns;
112
+ if (reviewTimeoutTurns !== void 0 && (typeof reviewTimeoutTurns !== "number" || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) return;
113
+ const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha;
114
+ if (cacheHitDiscountAlpha !== void 0 && (typeof cacheHitDiscountAlpha !== "number" || !Number.isFinite(cacheHitDiscountAlpha) || cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) return;
115
+ const reviewHighImpactTokens = value.reviewHighImpactTokens;
116
+ if (reviewHighImpactTokens !== void 0 && (typeof reviewHighImpactTokens !== "number" || !Number.isSafeInteger(reviewHighImpactTokens) || reviewHighImpactTokens < 0)) return;
106
117
  const decoded = {};
107
118
  if (value.dedupeToolResults !== void 0) decoded.dedupeToolResults = value.dedupeToolResults;
108
119
  if (value.summaryLocator !== void 0) decoded.summaryLocator = value.summaryLocator;
@@ -114,6 +125,10 @@ window.__ModuleLoader__.load({
114
125
  if (value.estimatorBaseUrl !== void 0) decoded.estimatorBaseUrl = value.estimatorBaseUrl;
115
126
  if (value.estimatorApiKey !== void 0) decoded.estimatorApiKey = value.estimatorApiKey;
116
127
  if (estimatorTimeoutMs !== void 0) decoded.estimatorTimeoutMs = estimatorTimeoutMs;
128
+ if (value.reviewMode !== void 0) decoded.reviewMode = value.reviewMode;
129
+ if (reviewTimeoutTurns !== void 0) decoded.reviewTimeoutTurns = reviewTimeoutTurns;
130
+ if (cacheHitDiscountAlpha !== void 0) decoded.cacheHitDiscountAlpha = cacheHitDiscountAlpha;
131
+ if (reviewHighImpactTokens !== void 0) decoded.reviewHighImpactTokens = reviewHighImpactTokens;
117
132
  return decoded;
118
133
  }
119
134
  /**
@@ -995,6 +1010,123 @@ window.__ModuleLoader__.load({
995
1010
  ]
996
1011
  });
997
1012
  }
1013
+ /**
1014
+ * TokenPilot-inspired review-mode card (beta). When enabled, edge/high-impact
1015
+ * reduction candidates queue for manual approval and execute in one merged
1016
+ * batch at the next turn boundary; the numeric fields tune the benefit model.
1017
+ * Numeric drafts commit on blur and only when they parse to a value the
1018
+ * runtime schema accepts, so an invalid keystroke never disables the panel.
1019
+ */
1020
+ function ReviewModeControls({ options, disabled, save, settle, t }) {
1021
+ const [turnsDraft, setTurnsDraft] = (0, react.useState)(String(options.reviewTimeoutTurns ?? 6));
1022
+ const [alphaDraft, setAlphaDraft] = (0, react.useState)(String(options.cacheHitDiscountAlpha ?? .1));
1023
+ const [highImpactDraft, setHighImpactDraft] = (0, react.useState)(String(options.reviewHighImpactTokens ?? 4e3));
1024
+ const reviewMode = options.reviewMode ?? false;
1025
+ const commit = (patch) => {
1026
+ settle(() => save(patch));
1027
+ };
1028
+ const commitTurns = () => {
1029
+ const next = Number(turnsDraft);
1030
+ if (!Number.isSafeInteger(next) || next < 1 || next === (options.reviewTimeoutTurns ?? 6)) return;
1031
+ commit({ reviewTimeoutTurns: next });
1032
+ };
1033
+ const commitAlpha = () => {
1034
+ const next = Number(alphaDraft);
1035
+ if (!Number.isFinite(next) || next <= 0 || next >= 1 || next === (options.cacheHitDiscountAlpha ?? .1)) return;
1036
+ commit({ cacheHitDiscountAlpha: next });
1037
+ };
1038
+ const commitHighImpact = () => {
1039
+ const next = Number(highImpactDraft);
1040
+ if (!Number.isSafeInteger(next) || next < 0 || next === (options.reviewHighImpactTokens ?? 4e3)) return;
1041
+ commit({ reviewHighImpactTokens: next });
1042
+ };
1043
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1044
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompact,
1045
+ "aria-labelledby": "context-compression-review-title",
1046
+ children: [
1047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1048
+ id: "context-compression-review-title",
1049
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.autoCompactTitle,
1050
+ children: t("review.title")
1051
+ }),
1052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1053
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.customNote,
1054
+ children: t("review.description")
1055
+ }),
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1057
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
1058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("review.enabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1059
+ value: reviewMode ? "on" : "off",
1060
+ disabled,
1061
+ onChange: (event) => {
1062
+ settle(() => save({ reviewMode: event.currentTarget.value === "on" }));
1063
+ },
1064
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1065
+ value: "off",
1066
+ children: t("review.enabled.off")
1067
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1068
+ value: "on",
1069
+ children: t("review.enabled.on")
1070
+ })]
1071
+ })]
1072
+ }),
1073
+ reviewMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1075
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
1076
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("review.timeoutTurns") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1077
+ type: "number",
1078
+ min: 1,
1079
+ step: 1,
1080
+ value: turnsDraft,
1081
+ disabled,
1082
+ onChange: (event) => {
1083
+ setTurnsDraft(event.currentTarget.value);
1084
+ },
1085
+ onBlur: commitTurns,
1086
+ onKeyDown: (event) => {
1087
+ if (event.key === "Enter") event.currentTarget.blur();
1088
+ }
1089
+ })]
1090
+ }),
1091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1092
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
1093
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("review.alpha") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1094
+ type: "number",
1095
+ min: .01,
1096
+ max: .99,
1097
+ step: .05,
1098
+ value: alphaDraft,
1099
+ disabled,
1100
+ onChange: (event) => {
1101
+ setAlphaDraft(event.currentTarget.value);
1102
+ },
1103
+ onBlur: commitAlpha,
1104
+ onKeyDown: (event) => {
1105
+ if (event.key === "Enter") event.currentTarget.blur();
1106
+ }
1107
+ })]
1108
+ }),
1109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1110
+ className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.field,
1111
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("review.highImpact") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1112
+ type: "number",
1113
+ min: 0,
1114
+ step: 500,
1115
+ value: highImpactDraft,
1116
+ disabled,
1117
+ onChange: (event) => {
1118
+ setHighImpactDraft(event.currentTarget.value);
1119
+ },
1120
+ onBlur: commitHighImpact,
1121
+ onKeyDown: (event) => {
1122
+ if (event.key === "Enter") event.currentTarget.blur();
1123
+ }
1124
+ })]
1125
+ })
1126
+ ] }) : null
1127
+ ]
1128
+ });
1129
+ }
998
1130
  //#endregion
999
1131
  //#region src/client/settings-section.tsx
1000
1132
  /**
@@ -1098,13 +1230,19 @@ window.__ModuleLoader__.load({
1098
1230
  current !== "tokenpilot-inspired" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorInactiveNotice, {
1099
1231
  profile: t(`profile.${current}`),
1100
1232
  t
1101
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorControls, {
1233
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EstimatorControls, {
1102
1234
  options: state.value?.presetOptions ?? {},
1103
1235
  disabled: busy || !state.writable || false,
1104
1236
  save: savePresetOptions,
1105
1237
  settle,
1106
1238
  t
1107
- }),
1239
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReviewModeControls, {
1240
+ options: state.value?.presetOptions ?? {},
1241
+ disabled: busy || !state.writable || false,
1242
+ save: savePresetOptions,
1243
+ settle,
1244
+ t
1245
+ })] }),
1108
1246
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1109
1247
  className: _dsh_context_compression_css_466eb745356d_CompressionProfileSelector_module_css_default.pricing,
1110
1248
  children: t("pricing.disclosure")
@@ -1194,6 +1332,25 @@ window.__ModuleLoader__.load({
1194
1332
  "estimator.apiKey.set": "已设置 · 输入新值覆盖",
1195
1333
  "estimator.apiKey.clear": "清除",
1196
1334
  "estimator.apiKey.overwrite": "已设置保密值,输入新值并失焦即可覆盖。",
1335
+ "review.title": "人工审查(beta)",
1336
+ "review.description": "开启后,边缘区间与高影响的压缩候选不再自动执行,而是进入待审队列并在你批准后的下一个回合边界批量执行;未处理的提案超过过期轮数后自动作废。仅随「TokenPilot 启发模式」提供,默认关闭。",
1337
+ "review.enabled": "审查模式",
1338
+ "review.enabled.on": "开(提案等待人工批准)",
1339
+ "review.enabled.off": "关(默认,全自动)",
1340
+ "review.timeoutTurns": "提案过期轮数",
1341
+ "review.alpha": "缓存命中折扣 α",
1342
+ "review.highImpact": "高影响门槛(tokens)",
1343
+ "review.badge": "待审",
1344
+ "review.summary.autoApplied": "自动应用",
1345
+ "review.summary.reviewApplied": "审查应用",
1346
+ "review.summary.expired": "已过期",
1347
+ "review.summary.voided": "已作废",
1348
+ "review.row.payback": "回本轮数",
1349
+ "review.row.expectedSaving": "预期节省",
1350
+ "review.row.estimated": "估计",
1351
+ "review.action.approve": "批准",
1352
+ "review.action.reject": "驳回",
1353
+ "review.action.ignore": "本会话忽略",
1197
1354
  "detail.tokenpilot-inspired": "在平衡模式之上叠加去重指针、恢复豁免、摘要定位块、前缀稳定与读取状态语义;估计器需另行配置端点",
1198
1355
  "profile.custom": "Custom/实验模式",
1199
1356
  "profile.native": "原生对照",
@@ -1290,6 +1447,25 @@ window.__ModuleLoader__.load({
1290
1447
  "estimator.apiKey.set": "Set · type a new value to overwrite",
1291
1448
  "estimator.apiKey.clear": "Clear",
1292
1449
  "estimator.apiKey.overwrite": "A secret is stored; type a new value and blur to overwrite it.",
1450
+ "review.title": "Review mode (beta)",
1451
+ "review.description": "When enabled, edge-band and high-impact reduction candidates no longer apply automatically: they queue for manual approval and execute in one merged batch at the next turn boundary after approval. Unhandled proposals expire after the configured number of turns. Ships with the TokenPilot-inspired profile only, off by default.",
1452
+ "review.enabled": "Review mode",
1453
+ "review.enabled.on": "On (proposals wait for manual approval)",
1454
+ "review.enabled.off": "Off (default, fully automatic)",
1455
+ "review.timeoutTurns": "Proposal expiry (turns)",
1456
+ "review.alpha": "Cache-hit discount α",
1457
+ "review.highImpact": "High-impact threshold (tokens)",
1458
+ "review.badge": "Review",
1459
+ "review.summary.autoApplied": "Auto-applied",
1460
+ "review.summary.reviewApplied": "Review-applied",
1461
+ "review.summary.expired": "Expired",
1462
+ "review.summary.voided": "Voided",
1463
+ "review.row.payback": "Payback",
1464
+ "review.row.expectedSaving": "Expected saving",
1465
+ "review.row.estimated": "estimated",
1466
+ "review.action.approve": "Approve",
1467
+ "review.action.reject": "Reject",
1468
+ "review.action.ignore": "Ignore",
1293
1469
  "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",
1294
1470
  "profile.custom": "Custom / Experimental",
1295
1471
  "profile.native": "Native baseline",
@@ -1364,7 +1540,11 @@ window.__ModuleLoader__.load({
1364
1540
  "estimatorModel",
1365
1541
  "estimatorBaseUrl",
1366
1542
  "estimatorApiKey",
1367
- "estimatorTimeoutMs"
1543
+ "estimatorTimeoutMs",
1544
+ "reviewMode",
1545
+ "reviewTimeoutTurns",
1546
+ "cacheHitDiscountAlpha",
1547
+ "reviewHighImpactTokens"
1368
1548
  ];
1369
1549
  /**
1370
1550
  * Plan the path ops one patch needs against the section currently stored.
@@ -1418,6 +1598,314 @@ window.__ModuleLoader__.load({
1418
1598
  });
1419
1599
  }
1420
1600
  //#endregion
1601
+ //#region src/client/ReviewOverlay.tsx
1602
+ /**
1603
+ * TokenPilot-inspired R4: the review floating window.
1604
+ *
1605
+ * Mounted on the host `shell.overlay` slot (dsh-tidychat precedent: the layer
1606
+ * is click-through by default and only the card opts back in), showing a
1607
+ * bottom-right badge while any session has pending proposals and a card with
1608
+ * the four-state summary row plus one row per proposal. Every 10s it polls the
1609
+ * review-queue route; when the queue is empty or review mode is off the
1610
+ * component renders null, so it never disturbs the session.
1611
+ *
1612
+ * Styles carry the `dsh-cc-review-` prefix and ride a one-shot style tag.
1613
+ */
1614
+ const QUEUE_ROUTES = ["/api/dsh-context-compression-improved/review-queue", "/endpoint/dsh-context-compression-improved/review-queue"];
1615
+ const DECIDE_ROUTES = ["/api/dsh-context-compression-improved/review-decide", "/endpoint/dsh-context-compression-improved/review-decide"];
1616
+ const CSS = `
1617
+ .dsh-cc-review-badge {
1618
+ position: fixed;
1619
+ right: 20px;
1620
+ bottom: 20px;
1621
+ z-index: 70;
1622
+ pointer-events: auto;
1623
+ box-sizing: border-box;
1624
+ min-width: 34px;
1625
+ height: 34px;
1626
+ padding: 0 10px;
1627
+ border-radius: 17px;
1628
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
1629
+ background: var(--dsw-alias-bg-layer-3, #fff);
1630
+ color: var(--dsw-alias-label-primary, #222);
1631
+ font-size: 13px;
1632
+ display: flex;
1633
+ align-items: center;
1634
+ justify-content: center;
1635
+ gap: 6px;
1636
+ cursor: pointer;
1637
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
1638
+ }
1639
+ .dsh-cc-review-card {
1640
+ position: fixed;
1641
+ right: 20px;
1642
+ bottom: 62px;
1643
+ z-index: 70;
1644
+ pointer-events: auto;
1645
+ box-sizing: border-box;
1646
+ width: min(420px, calc(100vw - 40px));
1647
+ max-height: min(60vh, 520px);
1648
+ overflow: auto;
1649
+ background: var(--dsw-alias-bg-layer-3, #fff);
1650
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
1651
+ border-radius: 12px;
1652
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
1653
+ padding: 12px 14px;
1654
+ color: var(--dsw-alias-label-primary, #222);
1655
+ font-size: 13px;
1656
+ }
1657
+ .dsh-cc-review-title {
1658
+ font-weight: 600;
1659
+ margin: 0 0 6px;
1660
+ font-size: 13px;
1661
+ }
1662
+ .dsh-cc-review-summary {
1663
+ display: flex;
1664
+ flex-wrap: wrap;
1665
+ gap: 4px 12px;
1666
+ color: var(--dsw-alias-label-tertiary, #888);
1667
+ font-size: 12px;
1668
+ margin-bottom: 8px;
1669
+ }
1670
+ .dsh-cc-review-row {
1671
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25));
1672
+ padding: 8px 0;
1673
+ }
1674
+ .dsh-cc-review-row-meta {
1675
+ color: var(--dsw-alias-label-tertiary, #888);
1676
+ font-size: 12px;
1677
+ margin-bottom: 4px;
1678
+ }
1679
+ .dsh-cc-review-actions {
1680
+ display: flex;
1681
+ gap: 8px;
1682
+ }
1683
+ .dsh-cc-review-btn {
1684
+ appearance: none;
1685
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
1686
+ background: transparent;
1687
+ color: inherit;
1688
+ border-radius: 6px;
1689
+ padding: 3px 10px;
1690
+ font-size: 12px;
1691
+ cursor: pointer;
1692
+ }
1693
+ .dsh-cc-review-btn-primary {
1694
+ background: var(--dsw-alias-state-business-primary, #3b82f6);
1695
+ border-color: transparent;
1696
+ color: #fff;
1697
+ }
1698
+ `;
1699
+ function injectOnce() {
1700
+ const tag = document.createElement("style");
1701
+ tag.setAttribute("data-plugin-css", "dsh-context-compression-improved-review");
1702
+ tag.textContent = CSS;
1703
+ document.head.appendChild(tag);
1704
+ return () => {
1705
+ tag.remove();
1706
+ };
1707
+ }
1708
+ async function fetchJson(route, init) {
1709
+ const response = await fetch(route, {
1710
+ headers: { "cache-control": "no-cache" },
1711
+ ...init
1712
+ });
1713
+ if (!response.ok) return void 0;
1714
+ return response.json();
1715
+ }
1716
+ async function pollQueue() {
1717
+ for (const route of QUEUE_ROUTES) {
1718
+ const body = await fetchJson(route);
1719
+ if (body?.ok === true) return {
1720
+ pending: body.pending ?? [],
1721
+ summary: body.summary
1722
+ };
1723
+ }
1724
+ }
1725
+ async function postDecide(proposal, decision) {
1726
+ for (const route of DECIDE_ROUTES) try {
1727
+ const response = await fetch(route, {
1728
+ method: "POST",
1729
+ headers: { "content-type": "application/json" },
1730
+ body: JSON.stringify({
1731
+ sessionId: proposal.sessionId,
1732
+ proposalId: proposal.id,
1733
+ decision
1734
+ })
1735
+ });
1736
+ if (response.status !== 404) return response.ok;
1737
+ } catch {}
1738
+ return false;
1739
+ }
1740
+ /**
1741
+ * Slot factory helper: the client entry is a .ts file and cannot carry JSX,
1742
+ * so the element construction lives here.
1743
+ */
1744
+ function renderReviewOverlay(scope, t) {
1745
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReviewOverlay, {
1746
+ scope,
1747
+ t
1748
+ });
1749
+ }
1750
+ /**
1751
+ * The floating window itself: renders null (and stays silent) while review
1752
+ * mode is off or nothing is pending.
1753
+ */
1754
+ function ReviewOverlay({ scope, t }) {
1755
+ const [reviewMode, setReviewMode] = (0, react.useState)(false);
1756
+ const [pending, setPending] = (0, react.useState)([]);
1757
+ const [summary, setSummary] = (0, react.useState)();
1758
+ const [expanded, setExpanded] = (0, react.useState)(false);
1759
+ const [busy, setBusy] = (0, react.useState)(false);
1760
+ (0, react.useEffect)(injectOnce, []);
1761
+ (0, react.useEffect)(() => {
1762
+ const pull = () => {
1763
+ try {
1764
+ const snapshot = scope.getSnapshot();
1765
+ setReviewMode(snapshot.status === "ready" && snapshot.value?.presetOptions?.reviewMode === true);
1766
+ } catch {
1767
+ setReviewMode(false);
1768
+ }
1769
+ };
1770
+ pull();
1771
+ let unsubscribe = () => {};
1772
+ try {
1773
+ unsubscribe = scope.subscribe(pull);
1774
+ } catch {
1775
+ unsubscribe = () => {};
1776
+ }
1777
+ return unsubscribe;
1778
+ }, [scope]);
1779
+ (0, react.useEffect)(() => {
1780
+ if (!reviewMode) return;
1781
+ let alive = true;
1782
+ let timer;
1783
+ const tick = () => {
1784
+ pollQueue().then((result) => {
1785
+ if (!alive) return;
1786
+ setPending(result?.pending ?? []);
1787
+ setSummary(result?.summary);
1788
+ timer = setTimeout(tick, 1e4);
1789
+ });
1790
+ };
1791
+ tick();
1792
+ return () => {
1793
+ alive = false;
1794
+ if (timer !== void 0) clearTimeout(timer);
1795
+ };
1796
+ }, [reviewMode]);
1797
+ if (!reviewMode || pending.length === 0) return null;
1798
+ const decide = (proposal, decision) => {
1799
+ setBusy(true);
1800
+ postDecide(proposal, decision).then(() => {
1801
+ return pollQueue().then((result) => {
1802
+ setPending(result?.pending ?? []);
1803
+ setSummary(result?.summary);
1804
+ setBusy(false);
1805
+ });
1806
+ }).catch(() => {
1807
+ setBusy(false);
1808
+ });
1809
+ };
1810
+ const seqRange = (proposal) => {
1811
+ const seqs = proposal.items.map((item) => item.seq);
1812
+ const min = Math.min(...seqs);
1813
+ const max = Math.max(...seqs);
1814
+ return min === max ? `#${String(min)}` : `#${String(min)}–#${String(max)}`;
1815
+ };
1816
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1817
+ className: "dsh-cc-review-card",
1818
+ children: [
1819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1820
+ className: "dsh-cc-review-title",
1821
+ children: t("review.title")
1822
+ }),
1823
+ summary !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1824
+ className: "dsh-cc-review-summary",
1825
+ children: [
1826
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1827
+ t("review.summary.autoApplied"),
1828
+ ": ",
1829
+ String(summary.autoApplied)
1830
+ ] }),
1831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1832
+ t("review.summary.reviewApplied"),
1833
+ ": ",
1834
+ String(summary.reviewApplied)
1835
+ ] }),
1836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1837
+ t("review.summary.expired"),
1838
+ ": ",
1839
+ String(summary.expired)
1840
+ ] }),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1842
+ t("review.summary.voided"),
1843
+ ": ",
1844
+ String(summary.voided)
1845
+ ] })
1846
+ ]
1847
+ }) : null,
1848
+ pending.map((proposal) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1849
+ className: "dsh-cc-review-row",
1850
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1851
+ className: "dsh-cc-review-row-meta",
1852
+ children: [
1853
+ proposal.kind,
1854
+ " · ",
1855
+ seqRange(proposal),
1856
+ " · R ≈ ",
1857
+ String(proposal.benefit.recoveredTokens),
1858
+ proposal.benefit.paybackTurns !== void 0 ? ` · ${t("review.row.payback")}: ${String(Math.round(proposal.benefit.paybackTurns * 100) / 100)}` : "",
1859
+ proposal.benefit.expectedSaving !== void 0 ? ` · ${t("review.row.expectedSaving")}: ${String(Math.round(proposal.benefit.expectedSaving))} (${t("review.row.estimated")})` : ""
1860
+ ]
1861
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1862
+ className: "dsh-cc-review-actions",
1863
+ children: [
1864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1865
+ type: "button",
1866
+ className: "dsh-cc-review-btn dsh-cc-review-btn-primary",
1867
+ disabled: busy,
1868
+ onClick: () => {
1869
+ decide(proposal, "approved");
1870
+ },
1871
+ children: t("review.action.approve")
1872
+ }),
1873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1874
+ type: "button",
1875
+ className: "dsh-cc-review-btn",
1876
+ disabled: busy,
1877
+ onClick: () => {
1878
+ decide(proposal, "rejected");
1879
+ },
1880
+ children: t("review.action.reject")
1881
+ }),
1882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1883
+ type: "button",
1884
+ className: "dsh-cc-review-btn",
1885
+ disabled: busy,
1886
+ onClick: () => {
1887
+ decide(proposal, "ignored");
1888
+ },
1889
+ children: t("review.action.ignore")
1890
+ })
1891
+ ]
1892
+ })]
1893
+ }, proposal.id))
1894
+ ]
1895
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1896
+ type: "button",
1897
+ className: "dsh-cc-review-badge",
1898
+ onClick: () => {
1899
+ setExpanded((value) => !value);
1900
+ },
1901
+ children: [
1902
+ t("review.badge"),
1903
+ " ",
1904
+ String(pending.length)
1905
+ ]
1906
+ })] });
1907
+ }
1908
+ //#endregion
1421
1909
  //#region src/client/index.ts
1422
1910
  const inject = [
1423
1911
  "slots",
@@ -1471,6 +1959,19 @@ window.__ModuleLoader__.load({
1471
1959
  } catch (error) {
1472
1960
  console.warn("[dsh-context-compression-improved] settings.section 注册失败(新宿主已收编):", error);
1473
1961
  }
1962
+ try {
1963
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1964
+ name: "shell.overlay",
1965
+ id: "context-compression-review"
1966
+ }, () => {
1967
+ return renderReviewOverlay(ctx.settingsScope.bind({
1968
+ namespace: NS,
1969
+ decode: decodeSettings
1970
+ }), ctx.locale.bind(NS));
1971
+ }));
1972
+ } catch (error) {
1973
+ console.warn("[dsh-context-compression-improved] shell.overlay 注册失败(宿主无浮层或已收编):", error);
1974
+ }
1474
1975
  }
1475
1976
  //#endregion
1476
1977
  exports.apply = apply;
@@ -390,7 +390,11 @@ function parsePresetOptionsSettings(value) {
390
390
  "estimatorModel",
391
391
  "estimatorBaseUrl",
392
392
  "estimatorApiKey",
393
- "estimatorTimeoutMs"
393
+ "estimatorTimeoutMs",
394
+ "reviewMode",
395
+ "reviewTimeoutTurns",
396
+ "cacheHitDiscountAlpha",
397
+ "reviewHighImpactTokens"
394
398
  ]);
395
399
  const unknown = Object.keys(value).find((key) => !allowed.has(key));
396
400
  if (unknown !== void 0) throw new TypeError(`Context-compression presetOptions: unknown key "${unknown}"`);
@@ -398,7 +402,8 @@ function parsePresetOptionsSettings(value) {
398
402
  "dedupeToolResults",
399
403
  "summaryLocator",
400
404
  "prefixStabilizer",
401
- "readState"
405
+ "readState",
406
+ "reviewMode"
402
407
  ]) {
403
408
  const entry = value[key];
404
409
  if (entry !== void 0 && typeof entry !== "boolean") throw new TypeError(`Context-compression presetOptions.${key} must be a boolean`);
@@ -407,6 +412,12 @@ function parsePresetOptionsSettings(value) {
407
412
  if (estimatorMode !== void 0 && estimatorMode !== "" && estimatorMode !== "host" && estimatorMode !== "direct") throw new TypeError("Context-compression presetOptions.estimatorMode must be \"\", \"host\", or \"direct\"");
408
413
  const estimatorTimeoutMs = value.estimatorTimeoutMs;
409
414
  if (estimatorTimeoutMs !== void 0 && (typeof estimatorTimeoutMs !== "number" || !Number.isSafeInteger(estimatorTimeoutMs) || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 6e4)) throw new TypeError("Context-compression presetOptions.estimatorTimeoutMs must be an integer between 100 and 60000");
415
+ const reviewTimeoutTurns = value.reviewTimeoutTurns;
416
+ if (reviewTimeoutTurns !== void 0 && (typeof reviewTimeoutTurns !== "number" || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) throw new TypeError("Context-compression presetOptions.reviewTimeoutTurns must be an integer of at least 1");
417
+ const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha;
418
+ if (cacheHitDiscountAlpha !== void 0 && (typeof cacheHitDiscountAlpha !== "number" || !Number.isFinite(cacheHitDiscountAlpha) || cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) throw new TypeError("Context-compression presetOptions.cacheHitDiscountAlpha must be a number strictly between 0 and 1");
419
+ const reviewHighImpactTokens = value.reviewHighImpactTokens;
420
+ if (reviewHighImpactTokens !== void 0 && (typeof reviewHighImpactTokens !== "number" || !Number.isSafeInteger(reviewHighImpactTokens) || reviewHighImpactTokens < 0)) throw new TypeError("Context-compression presetOptions.reviewHighImpactTokens must be a non-negative integer");
410
421
  for (const key of [
411
422
  "estimatorProvider",
412
423
  "estimatorModel",
@@ -427,6 +438,10 @@ function parsePresetOptionsSettings(value) {
427
438
  if (value.estimatorBaseUrl !== void 0) result.estimatorBaseUrl = value.estimatorBaseUrl;
428
439
  if (value.estimatorApiKey !== void 0) result.estimatorApiKey = value.estimatorApiKey;
429
440
  if (estimatorTimeoutMs !== void 0) result.estimatorTimeoutMs = estimatorTimeoutMs;
441
+ if (value.reviewMode !== void 0) result.reviewMode = value.reviewMode;
442
+ if (reviewTimeoutTurns !== void 0) result.reviewTimeoutTurns = reviewTimeoutTurns;
443
+ if (cacheHitDiscountAlpha !== void 0) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha;
444
+ if (reviewHighImpactTokens !== void 0) result.reviewHighImpactTokens = reviewHighImpactTokens;
430
445
  return result;
431
446
  }
432
447
  /** Settings schema used by the user-facing profile selector. */
@@ -629,7 +644,11 @@ const PRESET_OPTION_DEFAULTS = deepFreeze({
629
644
  summaryLocator: true,
630
645
  prefixStabilizer: true,
631
646
  readState: true,
632
- estimator: { mode: "" }
647
+ estimator: { mode: "" },
648
+ reviewMode: false,
649
+ reviewTimeoutTurns: 6,
650
+ cacheHitDiscountAlpha: .1,
651
+ reviewHighImpactTokens: 4e3
633
652
  });
634
653
  /**
635
654
  * Merge persisted presetOptions overrides over the tokenpilot-inspired
@@ -645,7 +664,11 @@ function mergePresetOptions(overrides) {
645
664
  summaryLocator: overrides.summaryLocator ?? PRESET_OPTION_DEFAULTS.summaryLocator,
646
665
  prefixStabilizer: overrides.prefixStabilizer ?? PRESET_OPTION_DEFAULTS.prefixStabilizer,
647
666
  readState: overrides.readState ?? PRESET_OPTION_DEFAULTS.readState,
648
- estimator: { mode: overrides.estimatorMode ?? PRESET_OPTION_DEFAULTS.estimator.mode }
667
+ estimator: { mode: overrides.estimatorMode ?? PRESET_OPTION_DEFAULTS.estimator.mode },
668
+ reviewMode: overrides.reviewMode ?? PRESET_OPTION_DEFAULTS.reviewMode,
669
+ reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
670
+ cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
671
+ reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens
649
672
  });
650
673
  }
651
674
  /**
@@ -17,6 +17,13 @@ interface Config {
17
17
  * covers both arrival orders.
18
18
  */
19
19
  estimatorCatalogRoute?: boolean;
20
+ /**
21
+ * Register the review pipeline's HTTP routes (pending-queue read + decide
22
+ * write) on this row. Same Bundle opt-in semantics as
23
+ * `estimatorCatalogRoute`; without the routes the floating window has no
24
+ * transport and simply never appears.
25
+ */
26
+ reviewQueueRoute?: boolean;
20
27
  }
21
28
  /** Loader validation for the standalone Bundle opt-in. */
22
29
  declare const Config: z<Config>;