dsh-model-switch 0.4.19 → 0.4.20

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 CHANGED
@@ -33,6 +33,11 @@ window.__ModuleLoader__.load({
33
33
  provider: "imageProvider",
34
34
  model: "imageModel"
35
35
  });
36
+ /** Auto-preset reviewer route; both unset means the reviewer follows the session model. */
37
+ const AUTO_REVIEW_SETTINGS_FIELDS = Object.freeze({
38
+ provider: "autoReviewProvider",
39
+ model: "autoReviewModel"
40
+ });
36
41
  function deriveSubagentSettings(settings) {
37
42
  return {
38
43
  mode: settings.subagentMode,
@@ -53,6 +58,16 @@ window.__ModuleLoader__.load({
53
58
  ...settings.imageModel === void 0 ? {} : { model: settings.imageModel }
54
59
  };
55
60
  }
61
+ function deriveAutoReviewSettings(settings) {
62
+ return {
63
+ ...settings.autoReviewProvider === void 0 ? {} : { provider: settings.autoReviewProvider },
64
+ ...settings.autoReviewModel === void 0 ? {} : { model: settings.autoReviewModel }
65
+ };
66
+ }
67
+ /** Whether both reviewer fields form a complete route; an incomplete pair still follows the session model. */
68
+ function autoReviewRouteConfigured(route) {
69
+ return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
70
+ }
56
71
  //#endregion
57
72
  //#region src/client/derived-config-form.ts
58
73
  function deriveConfigForm(source, project, fields) {
@@ -686,6 +701,23 @@ window.__ModuleLoader__.load({
686
701
  })
687
702
  ]
688
703
  });
704
+ if (kind === "autoReview") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
705
+ width: "18",
706
+ height: "18",
707
+ viewBox: "0 0 18 18",
708
+ fill: "none",
709
+ "aria-hidden": "true",
710
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
711
+ d: "M9 2.5 14.5 4.8v4.1c0 3.1-2.3 5.7-5.5 6.6-3.2-.9-5.5-3.5-5.5-6.6V4.8L9 2.5Z",
712
+ stroke: "currentColor",
713
+ strokeLinejoin: "round"
714
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
715
+ d: "m6.6 8.9 1.7 1.7 3.2-3.4",
716
+ stroke: "currentColor",
717
+ strokeLinecap: "round",
718
+ strokeLinejoin: "round"
719
+ })]
720
+ });
689
721
  return null;
690
722
  }
691
723
  function RouteCard({ title, summary, hint, icon, open, onToggle, disabled = false, badge, badgeWarn = false, trailing, expandable = true, children }) {
@@ -857,15 +889,46 @@ window.__ModuleLoader__.load({
857
889
  const { main, subagent, draft, groups } = controller;
858
890
  const search = props.useSearchSettings((value) => value);
859
891
  const image = props.useImageSettings((value) => value);
892
+ const autoReview = props.useAutoReviewSettings((value) => value);
860
893
  const switchSettings = props.useSwitchSettings((value) => value);
861
894
  const [open, setOpen] = (0, react.useState)();
862
895
  const [subagentDraft, setSubagentDraft, resetSubagent] = useDraft(subagent);
863
896
  const [searchDraft, setSearchDraft, resetSearch] = useDraft(search);
864
897
  const [imageDraft, setImageDraft, resetImage] = useDraft(image);
898
+ const [autoReviewDraft, setAutoReviewDraft, resetAutoReview] = useDraft(autoReview);
865
899
  const [busy, setBusy] = (0, react.useState)();
866
900
  const [message, setMessage] = (0, react.useState)();
867
901
  const [compactBusy, setCompactBusy] = (0, react.useState)(false);
868
902
  const [compactError, setCompactError] = (0, react.useState)();
903
+ const planCompactValue = switchSettings.value?.planCompactThreshold ?? 2e5;
904
+ const [planCompactDraft, setPlanCompactDraft] = (0, react.useState)(() => String(planCompactValue));
905
+ const [planCompactBusy, setPlanCompactBusy] = (0, react.useState)(false);
906
+ const [planCompactError, setPlanCompactError] = (0, react.useState)();
907
+ (0, react.useEffect)(() => {
908
+ setPlanCompactDraft(String(planCompactValue));
909
+ setPlanCompactError(void 0);
910
+ }, [planCompactValue]);
911
+ /** Commit the threshold on blur/Enter only when it is a non-negative integer. */
912
+ const commitPlanCompactThreshold = () => {
913
+ const parsed = Number(planCompactDraft);
914
+ if (!Number.isInteger(parsed) || parsed < 0) {
915
+ setPlanCompactError(props.t("planCompactThresholdInvalid"));
916
+ return;
917
+ }
918
+ if (parsed === planCompactValue) {
919
+ setPlanCompactError(void 0);
920
+ return;
921
+ }
922
+ setPlanCompactBusy(true);
923
+ setPlanCompactError(void 0);
924
+ props.setPlanCompactThreshold(parsed).then(() => {
925
+ setPlanCompactBusy(false);
926
+ }, (error) => {
927
+ setPlanCompactBusy(false);
928
+ setPlanCompactDraft(String(planCompactValue));
929
+ setPlanCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
930
+ });
931
+ };
869
932
  const loadSearchCapabilities = props.loadCapabilities;
870
933
  const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
871
934
  const [searchError, setSearchError] = (0, react.useState)(void 0);
@@ -956,11 +1019,14 @@ window.__ModuleLoader__.load({
956
1019
  const searchChoices = capabilityChoices(searchGroups, searchDraft, void 0, "search");
957
1020
  const searchAvailable = searchLive?.searchProviderAdapters.available ?? false;
958
1021
  const imageChoices = capabilityChoices(groups, imageDraft, props.capabilities.imageProviderAdapters.providers ?? [], "image");
1022
+ const autoReviewChoices = deriveRouteChoices(groups, autoReviewDraft);
1023
+ const autoReviewOn = autoReviewRouteConfigured(autoReview.value);
959
1024
  const synced = [
960
1025
  main,
961
1026
  subagent,
962
1027
  search,
963
- image
1028
+ image,
1029
+ autoReview
964
1030
  ].every((snapshot) => snapshot.status === "ready");
965
1031
  const run = async (route, operation) => {
966
1032
  if (busy !== void 0) return;
@@ -1019,12 +1085,57 @@ window.__ModuleLoader__.load({
1019
1085
  if (current.value?.model !== next.model) await props.setCapability(route, "model", next.model);
1020
1086
  });
1021
1087
  };
1088
+ const saveAutoReview = () => {
1089
+ if (autoReviewDraft === void 0) return;
1090
+ run("autoReview", async () => {
1091
+ if (autoReview.value?.provider !== autoReviewDraft.provider) await props.setCapability("autoReview", "provider", autoReviewDraft.provider);
1092
+ if (autoReview.value?.model !== autoReviewDraft.model) await props.setCapability("autoReview", "model", autoReviewDraft.model);
1093
+ });
1094
+ };
1095
+ /**
1096
+ * The reviewer switch: Off unsets both stored fields (follow the session
1097
+ * model); On seeds the route from Main so the pin takes effect immediately
1098
+ * and stays editable below. A failed write reports, never flips silently.
1099
+ */
1100
+ const persistAutoReviewEnabled = (enabled) => {
1101
+ if (busy === "autoReview" || !autoReview.writable || autoReview.value === void 0) return;
1102
+ if (!enabled) {
1103
+ setOpen((current) => current === "autoReview" ? void 0 : current);
1104
+ run("autoReview", async () => {
1105
+ if (autoReview.value?.provider !== void 0) await props.setCapability("autoReview", "provider", void 0);
1106
+ if (autoReview.value?.model !== void 0) await props.setCapability("autoReview", "model", void 0);
1107
+ });
1108
+ return;
1109
+ }
1110
+ const seed = draft;
1111
+ if (seed === void 0 || seed.provider.trim() === "" || seed.model.trim() === "") return;
1112
+ setBusy("autoReview");
1113
+ setMessage(void 0);
1114
+ (async () => {
1115
+ if (autoReview.value?.provider !== seed.provider) await props.setCapability("autoReview", "provider", seed.provider);
1116
+ if (autoReview.value?.model !== seed.model) await props.setCapability("autoReview", "model", seed.model);
1117
+ })().then(() => {
1118
+ setAutoReviewDraft({
1119
+ provider: seed.provider,
1120
+ model: seed.model
1121
+ });
1122
+ setBusy(void 0);
1123
+ setOpen("autoReview");
1124
+ }, (error) => {
1125
+ setBusy(void 0);
1126
+ setMessage({
1127
+ route: "autoReview",
1128
+ text: error instanceof Error ? error.message : props.t("requestFailed")
1129
+ });
1130
+ });
1131
+ };
1022
1132
  const mainSummary = draft === void 0 ? props.t("loading") : compact(routeName(groups, draft), mainEffectiveEffort);
1023
1133
  const subagentRole = subagentDraft?.mode === "fixed" && subagentDraft.provider !== void 0 && subagentDraft.provider !== "" ? props.providerRoleOf?.(subagentDraft.provider) : void 0;
1024
1134
  const subagentOn = subagentDraft?.mode === "fixed";
1025
1135
  const subagentSummary = subagentDraft === void 0 ? props.t("loading") : subagentOn ? compact(routeName(groups, subagentRoute), isAgentRole(subagentRole) ? props.t("agentBadge") : void 0, defaultEffort === void 0 ? props.t("providerDefaultShort") : compact(props.t("providerDefaultShort"), defaultEffort)) : props.t("subagentOff");
1026
1136
  const subagentDisabled = subagent.status !== "ready" || !subagent.writable || subagentDraft === void 0 || busy === "subagent" || !subagentOn || (subagentDraft.provider ?? "").trim() === "" || (subagentDraft.model ?? "").trim() === "";
1027
1137
  const capabilityDisabled = (route, snapshot, next) => snapshot.status !== "ready" || !snapshot.writable || next === void 0 || busy === route || (next.provider ?? "").trim() === "" || (next.model ?? "").trim() === "";
1138
+ const autoReviewDisabled = autoReview.status !== "ready" || !autoReview.writable || autoReviewDraft === void 0 || busy === "autoReview" || !autoReviewOn || (autoReviewDraft.provider ?? "").trim() === "" || (autoReviewDraft.model ?? "").trim() === "";
1028
1139
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
1029
1140
  className: ModelSwitchSettings_module_css_default.section,
1030
1141
  children: [
@@ -1042,66 +1153,129 @@ window.__ModuleLoader__.load({
1042
1153
  }),
1043
1154
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1044
1155
  className: ModelSwitchSettings_module_css_default.group,
1045
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1046
- className: ModelSwitchSettings_module_css_default.groupLabel,
1047
- children: props.t("sendProtection")
1048
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1049
- className: ModelSwitchSettings_module_css_default.rowCard,
1050
- children: [
1051
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1052
- className: ModelSwitchSettings_module_css_default.routeIcon,
1053
- "aria-hidden": "true",
1054
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1055
- width: "18",
1056
- height: "18",
1057
- viewBox: "0 0 18 18",
1058
- fill: "none",
1059
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1060
- d: "M4 5.5h10M4 9h7M4 12.5h5",
1061
- stroke: "currentColor",
1062
- strokeLinecap: "round"
1063
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1064
- d: "M13 11.5v3.5l2-1.6",
1065
- stroke: "currentColor",
1066
- strokeLinecap: "round",
1067
- strokeLinejoin: "round"
1068
- })]
1156
+ children: [
1157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1158
+ className: ModelSwitchSettings_module_css_default.groupLabel,
1159
+ children: props.t("sendProtection")
1160
+ }),
1161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
+ className: ModelSwitchSettings_module_css_default.rowCard,
1163
+ children: [
1164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1165
+ className: ModelSwitchSettings_module_css_default.routeIcon,
1166
+ "aria-hidden": "true",
1167
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1168
+ width: "18",
1169
+ height: "18",
1170
+ viewBox: "0 0 18 18",
1171
+ fill: "none",
1172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1173
+ d: "M4 5.5h10M4 9h7M4 12.5h5",
1174
+ stroke: "currentColor",
1175
+ strokeLinecap: "round"
1176
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1177
+ d: "M13 11.5v3.5l2-1.6",
1178
+ stroke: "currentColor",
1179
+ strokeLinecap: "round",
1180
+ strokeLinejoin: "round"
1181
+ })]
1182
+ })
1183
+ }),
1184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
+ className: ModelSwitchSettings_module_css_default.rowCopy,
1186
+ children: [
1187
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1188
+ className: ModelSwitchSettings_module_css_default.rowTitle,
1189
+ children: props.t("compactOnSwitch")
1190
+ }),
1191
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1192
+ className: ModelSwitchSettings_module_css_default.hint,
1193
+ children: props.t("compactOnSwitchHelp")
1194
+ }),
1195
+ compactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1196
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1197
+ children: compactError
1198
+ })
1199
+ ]
1200
+ }),
1201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1202
+ checked: switchSettings.value?.compactOnSwitch !== false,
1203
+ disabled: !switchSettings.writable || compactBusy,
1204
+ label: props.t("compactOnSwitch"),
1205
+ onChange: (value) => {
1206
+ setCompactBusy(true);
1207
+ setCompactError(void 0);
1208
+ props.setCompactOnSwitch(value).then(() => {
1209
+ setCompactBusy(false);
1210
+ }, (error) => {
1211
+ setCompactBusy(false);
1212
+ setCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
1213
+ });
1214
+ }
1069
1215
  })
1070
- }),
1071
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1072
- className: ModelSwitchSettings_module_css_default.rowCopy,
1073
- children: [
1074
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1075
- className: ModelSwitchSettings_module_css_default.rowTitle,
1076
- children: props.t("compactOnSwitch")
1077
- }),
1078
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1079
- className: ModelSwitchSettings_module_css_default.hint,
1080
- children: props.t("compactOnSwitchHelp")
1081
- }),
1082
- compactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1083
- className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1084
- children: compactError
1216
+ ]
1217
+ }),
1218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1219
+ className: ModelSwitchSettings_module_css_default.rowCard,
1220
+ children: [
1221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1222
+ className: ModelSwitchSettings_module_css_default.routeIcon,
1223
+ "aria-hidden": "true",
1224
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1225
+ width: "18",
1226
+ height: "18",
1227
+ viewBox: "0 0 18 18",
1228
+ fill: "none",
1229
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1230
+ d: "M4 5.5h10M4 9h7M4 12.5h5",
1231
+ stroke: "currentColor",
1232
+ strokeLinecap: "round"
1233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1234
+ d: "M13 11.5v3.5l2-1.6",
1235
+ stroke: "currentColor",
1236
+ strokeLinecap: "round",
1237
+ strokeLinejoin: "round"
1238
+ })]
1085
1239
  })
1086
- ]
1087
- }),
1088
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1089
- checked: switchSettings.value?.compactOnSwitch !== false,
1090
- disabled: !switchSettings.writable || compactBusy,
1091
- label: props.t("compactOnSwitch"),
1092
- onChange: (value) => {
1093
- setCompactBusy(true);
1094
- setCompactError(void 0);
1095
- props.setCompactOnSwitch(value).then(() => {
1096
- setCompactBusy(false);
1097
- }, (error) => {
1098
- setCompactBusy(false);
1099
- setCompactError(error instanceof Error ? error.message : props.t("requestFailed"));
1100
- });
1101
- }
1102
- })
1103
- ]
1104
- })]
1240
+ }),
1241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1242
+ className: ModelSwitchSettings_module_css_default.rowCopy,
1243
+ children: [
1244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1245
+ className: ModelSwitchSettings_module_css_default.rowTitle,
1246
+ children: props.t("planCompactThreshold")
1247
+ }),
1248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1249
+ className: ModelSwitchSettings_module_css_default.hint,
1250
+ children: props.t("planCompactThresholdHelp")
1251
+ }),
1252
+ planCompactError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1253
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
1254
+ children: planCompactError
1255
+ })
1256
+ ]
1257
+ }),
1258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1259
+ className: ModelSwitchSettings_module_css_default.input,
1260
+ type: "number",
1261
+ min: 0,
1262
+ step: 1e3,
1263
+ inputMode: "numeric",
1264
+ "aria-label": props.t("planCompactThreshold"),
1265
+ value: planCompactDraft,
1266
+ disabled: !switchSettings.writable || planCompactBusy,
1267
+ onChange: (event) => {
1268
+ setPlanCompactDraft(event.target.value);
1269
+ setPlanCompactError(void 0);
1270
+ },
1271
+ onBlur: commitPlanCompactThreshold,
1272
+ onKeyDown: (event) => {
1273
+ if (event.key === "Enter") commitPlanCompactThreshold();
1274
+ }
1275
+ })
1276
+ ]
1277
+ })
1278
+ ]
1105
1279
  }),
1106
1280
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1107
1281
  className: ModelSwitchSettings_module_css_default.group,
@@ -1394,6 +1568,70 @@ window.__ModuleLoader__.load({
1394
1568
  disabled: true,
1395
1569
  badge: props.t("unavailable"),
1396
1570
  badgeWarn: true
1571
+ }),
1572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteCard, {
1573
+ title: props.t("autoReview"),
1574
+ summary: autoReviewDraft === void 0 ? props.t("loading") : autoReviewOn ? routeName(groups, autoReviewDraft) : props.t("autoReviewFollow"),
1575
+ hint: props.t("autoReviewHelp"),
1576
+ icon: "autoReview",
1577
+ open: open === "autoReview" && autoReviewOn,
1578
+ onToggle: () => {
1579
+ if (autoReviewOn) toggle("autoReview");
1580
+ },
1581
+ expandable: autoReviewOn,
1582
+ trailing: autoReviewDraft === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
1583
+ checked: autoReviewOn,
1584
+ disabled: busy === "autoReview" || !autoReview.writable,
1585
+ label: props.t("autoReview"),
1586
+ onChange: persistAutoReviewEnabled
1587
+ }),
1588
+ children: autoReviewDraft === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1589
+ className: ModelSwitchSettings_module_css_default.hint,
1590
+ children: props.t("loading")
1591
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1593
+ className: ModelSwitchSettings_module_css_default.hint,
1594
+ children: props.t("autoReviewRouteHelp")
1595
+ }),
1596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1597
+ className: ModelSwitchSettings_module_css_default.formGrid,
1598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1599
+ label: props.t("provider"),
1600
+ value: autoReviewDraft.provider ?? "",
1601
+ disabled: busy === "autoReview" || !autoReview.writable,
1602
+ choices: autoReviewChoices.providers,
1603
+ onChange: (provider) => {
1604
+ const first = groups.find((group) => group.id === provider)?.models[0];
1605
+ setAutoReviewDraft({
1606
+ provider,
1607
+ ...first === void 0 ? {} : { model: first.id }
1608
+ });
1609
+ }
1610
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1611
+ label: props.t("model"),
1612
+ value: autoReviewDraft.model ?? "",
1613
+ disabled: busy === "autoReview" || !autoReview.writable,
1614
+ choices: autoReviewChoices.models,
1615
+ onChange: (model) => {
1616
+ setAutoReviewDraft({
1617
+ ...autoReviewDraft,
1618
+ model
1619
+ });
1620
+ }
1621
+ })]
1622
+ }),
1623
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Actions, {
1624
+ t: props.t,
1625
+ busy: busy === "autoReview",
1626
+ disabled: autoReviewDisabled,
1627
+ ...message?.route === "autoReview" ? { message: message.text } : {},
1628
+ onCancel: () => {
1629
+ resetAutoReview();
1630
+ setMessage(void 0);
1631
+ },
1632
+ onSave: saveAutoReview
1633
+ })
1634
+ ] })
1397
1635
  })
1398
1636
  ]
1399
1637
  })
@@ -1438,6 +1676,13 @@ window.__ModuleLoader__.load({
1438
1676
  requestFailed: "保存失败",
1439
1677
  catalogFailed: "无法加载模型目录。",
1440
1678
  conflict: "设置已在其他位置更改,请检查最新值后重试。",
1679
+ autoReview: "自动审查模型",
1680
+ autoReviewFollow: "跟随会话模型",
1681
+ autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。",
1682
+ autoReviewRouteHelp: "审查请求固定使用这里的 Provider/模型,与会话模型无关;保存后对下一次审查立即生效。",
1683
+ planCompactThreshold: "计划执行前压缩阈值",
1684
+ planCompactThresholdHelp: "切换模型执行计划时,若上下文估算超过该 Token 数,先用切换后的模型压缩一次历史再提交执行;未切换模型不触发。0 = 关闭。压缩会产生额外模型用量,摘要可能省略细节。",
1685
+ planCompactThresholdInvalid: "请输入不小于 0 的整数。",
1441
1686
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
1442
1687
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
1443
1688
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -1481,6 +1726,13 @@ window.__ModuleLoader__.load({
1481
1726
  requestFailed: "Save failed",
1482
1727
  catalogFailed: "Could not load the model catalog.",
1483
1728
  conflict: "Settings changed elsewhere. Review the latest values and retry.",
1729
+ autoReview: "Auto review model",
1730
+ autoReviewFollow: "Follows the session model",
1731
+ autoReviewHelp: "Pin the reviewer model for the official auto-review gate (per-call review under the Auto permission preset). Requires the official auto-review bundle. When off, reviews follow the session model.",
1732
+ autoReviewRouteHelp: "Review requests use exactly this provider/model regardless of the session model; saved changes apply to the very next review.",
1733
+ planCompactThreshold: "Plan-exit compaction threshold",
1734
+ planCompactThresholdHelp: "When a Plan is approved with a switched model and the estimated context exceeds this many tokens, compact history once with the switched-to model before committing. Approvals without a model switch never trigger it. 0 disables. Compaction uses extra model quota and may omit details.",
1735
+ planCompactThresholdInvalid: "Enter an integer of 0 or more.",
1484
1736
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
1485
1737
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
1486
1738
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -1505,6 +1757,7 @@ window.__ModuleLoader__.load({
1505
1757
  id: question.id,
1506
1758
  question: question.question,
1507
1759
  plan: question.detail,
1760
+ ...intent.callId === void 0 ? {} : { callId: intent.callId },
1508
1761
  approve,
1509
1762
  ...decline === void 0 ? {} : { decline }
1510
1763
  };
@@ -1573,6 +1826,14 @@ window.__ModuleLoader__.load({
1573
1826
  return false;
1574
1827
  }
1575
1828
  }
1829
+ //#endregion
1830
+ //#region src/picker/plan-compact.ts
1831
+ /** Runtime guard for the wire value returned by the Host RPC. */
1832
+ function isPlanCompactOutcome(value) {
1833
+ if (value === null || typeof value !== "object") return false;
1834
+ const status = value.status;
1835
+ return status === "disabled" || status === "skipped" || status === "compacted" || status === "empty" || status === "error";
1836
+ }
1576
1837
  const CONTEXT_SUFFIX = /-(\d+)(k|m)$/iu;
1577
1838
  /** Peel Fast and `-<n>k` / `-<n>m` in either order. Product names like `-max` stay. */
1578
1839
  function parsePickerId(id) {
@@ -2785,6 +3046,79 @@ window.__ModuleLoader__.load({
2785
3046
  });
2786
3047
  }
2787
3048
  //#endregion
3049
+ //#region src/client/picker/plan-preview.ts
3050
+ /** Registration id ui-plan uses for its `conversation.plan-review.actions` entry. */
3051
+ const PLAN_REVIEW_OPEN_ID = "@deepseek-ai/dsh-client-ui-plan";
3052
+ /** Identities whose preview was already opened this process (official parity: ui-plan's store keeps them too). */
3053
+ const opened = /* @__PURE__ */ new Set();
3054
+ /**
3055
+ * Stable identity of one review's sidebar preview.
3056
+ * @param sessionId - session that owns the review.
3057
+ * @param review - narrowed plan review.
3058
+ * @param requestKey - pending-question request key.
3059
+ * @returns `session:call:<callId>` for a logged plan, else `session:review:<requestKey>`.
3060
+ */
3061
+ function planPreviewIdentity(sessionId, review, requestKey) {
3062
+ const identity = review.callId === void 0 ? `review:${requestKey}` : `call:${review.callId}`;
3063
+ return `${String(sessionId)}:${identity}`;
3064
+ }
3065
+ function faceOf(slots, sessionId) {
3066
+ const entry = slots.entries("conversation.plan-review.actions").find((candidate) => candidate.options.id === PLAN_REVIEW_OPEN_ID && typeof candidate.inject === "function");
3067
+ if (entry === void 0 || entry.inject === void 0) return void 0;
3068
+ try {
3069
+ const face = entry.inject(sessionId);
3070
+ if (face === void 0 || typeof face.openReview !== "function") return void 0;
3071
+ if (face.hooks === void 0 || typeof face.hooks.sidebarMounted?.getSnapshot !== "function") return void 0;
3072
+ return face;
3073
+ } catch {
3074
+ return;
3075
+ }
3076
+ }
3077
+ /**
3078
+ * Bind the preview navigation to one slot registry.
3079
+ * @param slots - the live registry ledger.
3080
+ * @returns an idempotent binding usable from the card's render and effects.
3081
+ */
3082
+ function createPlanPreviewBinding(slots) {
3083
+ const open = (sessionId, review, requestKey) => {
3084
+ const face = faceOf(slots, sessionId);
3085
+ if (face === void 0) return false;
3086
+ opened.add(planPreviewIdentity(sessionId, review, requestKey));
3087
+ face.openReview(review, requestKey);
3088
+ return true;
3089
+ };
3090
+ return {
3091
+ available: () => slots.entries("conversation.plan-review.actions").some((candidate) => candidate.options.id === PLAN_REVIEW_OPEN_ID),
3092
+ open,
3093
+ autoOpen: (sessionId, review, requestKey) => {
3094
+ const identity = planPreviewIdentity(sessionId, review, requestKey);
3095
+ if (opened.has(identity)) return () => void 0;
3096
+ const face = faceOf(slots, sessionId);
3097
+ if (face === void 0) return () => void 0;
3098
+ const mounted = face.hooks.sidebarMounted;
3099
+ if (mounted.getSnapshot() !== void 0) {
3100
+ opened.add(identity);
3101
+ face.openReview(review, requestKey);
3102
+ return () => void 0;
3103
+ }
3104
+ let disposed = false;
3105
+ let openedHere = false;
3106
+ const unsubscribe = mounted.subscribe(() => {
3107
+ if (disposed || openedHere || opened.has(identity)) return;
3108
+ if (mounted.getSnapshot() === void 0) return;
3109
+ openedHere = true;
3110
+ opened.add(identity);
3111
+ unsubscribe();
3112
+ face.openReview(review, requestKey);
3113
+ });
3114
+ return () => {
3115
+ disposed = true;
3116
+ unsubscribe();
3117
+ };
3118
+ }
3119
+ };
3120
+ }
3121
+ //#endregion
2788
3122
  //#region node_modules/.pnpm/dsh-llm-providers-ui@https+_f1dadb1b56406564fc1bb8f2d953b82d/node_modules/dsh-llm-providers-ui/lib/order.js
2789
3123
  /** Display order for installed provider cards when the user has not saved one. */
2790
3124
  const PROVIDER_ITEM_ORDER = [
@@ -3139,7 +3473,7 @@ window.__ModuleLoader__.load({
3139
3473
  };
3140
3474
  //#endregion
3141
3475
  //#region \0dsh-css:src/client/picker/PlanReviewCard.module.css.mjs
3142
- const css = ".BylfoW_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.BylfoW_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:visible}.BylfoW_card,.BylfoW_card *{box-sizing:border-box}.BylfoW_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);border-radius:20px 20px 0 0;flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.BylfoW_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.BylfoW_stripTitle{flex:none;font-weight:600}.BylfoW_headerPicker{flex:0 auto;align-items:center;min-width:0;max-width:55%;margin-left:auto;display:flex}.BylfoW_headerPicker>*{min-width:0;max-width:100%}.BylfoW_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.BylfoW_footer{z-index:2;flex-direction:column;flex-shrink:0;gap:6px;padding:8px 16px 12px;display:flex;position:relative;overflow:visible}.BylfoW_feedback{min-height:0;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.BylfoW_feedback:empty{display:none}.BylfoW_feedbackRow{align-items:center;gap:8px;min-width:0;display:flex}.BylfoW_feedbackInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-minor);min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);border-radius:8px;flex:auto;padding:0 10px}.BylfoW_feedbackInput::placeholder{color:var(--dsw-alias-label-tertiary)}.BylfoW_feedbackInput:disabled{opacity:.6}.BylfoW_bar{flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;min-height:32px;display:flex}.BylfoW_picker{flex:16rem;min-width:0;max-width:16rem}.BylfoW_pickerError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.BylfoW_actions{flex-wrap:nowrap;flex:none;justify-content:flex-end;align-items:center;gap:8px;margin-left:auto;display:flex}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{white-space:nowrap;height:32px;min-height:32px;padding:0 10px}.BylfoW_discuss,.BylfoW_recover{color:var(--dsw-alias-label-secondary)}.BylfoW_discuss:hover:not(:disabled),.BylfoW_recover:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BylfoW_approve{padding:0 12px}@media (width<=720px){.BylfoW_frame{padding-right:max(10px, env(safe-area-inset-right));padding-bottom:max(10px, env(safe-area-inset-bottom));padding-left:max(10px, env(safe-area-inset-left))}.BylfoW_card{border-radius:16px}.BylfoW_strip,.BylfoW_body,.BylfoW_footer{padding-left:12px;padding-right:12px}.BylfoW_strip{flex-wrap:wrap;gap:6px}.BylfoW_headerPicker{flex:100%;max-width:100%;margin-left:0}.BylfoW_bar{justify-content:flex-end;gap:6px}.BylfoW_actions{flex:100%;justify-content:flex-end;gap:6px;min-width:0}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{overflow-wrap:anywhere;white-space:normal;flex:1 1 0;min-width:0;height:auto;padding:4px;line-height:14px}.BylfoW_feedbackInput{height:auto;min-height:32px}}";
3476
+ const css = ".BylfoW_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.BylfoW_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:visible}.BylfoW_card,.BylfoW_card *{box-sizing:border-box}.BylfoW_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);border-radius:20px 20px 0 0;flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.BylfoW_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.BylfoW_stripTitle{flex:none;font-weight:600}.BylfoW_previewLink{color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:0;flex:none;align-items:center;gap:4px;padding:0;display:inline-flex}.BylfoW_previewLink:hover{opacity:1;text-decoration:underline}.BylfoW_previewLink:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.BylfoW_headerPicker{flex:0 auto;align-items:center;min-width:0;max-width:55%;margin-left:auto;display:flex}.BylfoW_headerPicker>*{min-width:0;max-width:100%}.BylfoW_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.BylfoW_summary{min-width:0}.BylfoW_summaryTitle{text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:15px;font-weight:500;line-height:22px;overflow:hidden}.BylfoW_summaryText{-webkit-line-clamp:2;color:var(--dsw-alias-label-secondary);-webkit-box-orient:vertical;margin:8px 0 0;font-size:14px;line-height:24px;display:-webkit-box;overflow:hidden}.BylfoW_footer{z-index:2;flex-direction:column;flex-shrink:0;gap:6px;padding:8px 16px 12px;display:flex;position:relative;overflow:visible}.BylfoW_feedback{min-height:0;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.BylfoW_feedback:empty{display:none}.BylfoW_feedbackRow{align-items:center;gap:8px;min-width:0;display:flex}.BylfoW_feedbackInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-input-minor);min-width:0;height:32px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);border-radius:8px;flex:auto;padding:0 10px}.BylfoW_feedbackInput::placeholder{color:var(--dsw-alias-label-tertiary)}.BylfoW_feedbackInput:disabled{opacity:.6}.BylfoW_bar{flex-wrap:nowrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;min-height:32px;display:flex}.BylfoW_picker{flex:16rem;min-width:0;max-width:16rem}.BylfoW_pickerError{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.BylfoW_actions{flex-wrap:nowrap;flex:none;justify-content:flex-end;align-items:center;gap:8px;margin-left:auto;display:flex}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{white-space:nowrap;height:32px;min-height:32px;padding:0 10px}.BylfoW_discuss,.BylfoW_recover{color:var(--dsw-alias-label-secondary)}.BylfoW_discuss:hover:not(:disabled),.BylfoW_recover:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.BylfoW_approve{padding:0 12px}@media (width<=720px){.BylfoW_frame{padding-right:max(10px, env(safe-area-inset-right));padding-bottom:max(10px, env(safe-area-inset-bottom));padding-left:max(10px, env(safe-area-inset-left))}.BylfoW_card{border-radius:16px}.BylfoW_strip,.BylfoW_body,.BylfoW_footer{padding-left:12px;padding-right:12px}.BylfoW_strip{flex-wrap:wrap;gap:6px}.BylfoW_headerPicker{flex:100%;max-width:100%;margin-left:0}.BylfoW_bar{justify-content:flex-end;gap:6px}.BylfoW_actions{flex:100%;justify-content:flex-end;gap:6px;min-width:0}.BylfoW_discuss,.BylfoW_keep,.BylfoW_goalRun,.BylfoW_approve,.BylfoW_recover{overflow-wrap:anywhere;white-space:normal;flex:1 1 0;min-width:0;height:auto;padding:4px;line-height:14px}.BylfoW_feedbackInput{height:auto;min-height:32px}}";
3143
3477
  const tagId = "dsh-model-switch/PlanReviewCard.module.css";
3144
3478
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
3145
3479
  const tag = document.createElement("style");
@@ -3166,9 +3500,13 @@ window.__ModuleLoader__.load({
3166
3500
  "keep": "BylfoW_keep",
3167
3501
  "picker": "BylfoW_picker",
3168
3502
  "pickerError": "BylfoW_pickerError",
3503
+ "previewLink": "BylfoW_previewLink",
3169
3504
  "recover": "BylfoW_recover",
3170
3505
  "strip": "BylfoW_strip",
3171
- "stripTitle": "BylfoW_stripTitle"
3506
+ "stripTitle": "BylfoW_stripTitle",
3507
+ "summary": "BylfoW_summary",
3508
+ "summaryText": "BylfoW_summaryText",
3509
+ "summaryTitle": "BylfoW_summaryTitle"
3172
3510
  };
3173
3511
  //#endregion
3174
3512
  //#region src/client/picker/PlanReviewCard.tsx
@@ -3285,11 +3623,13 @@ window.__ModuleLoader__.load({
3285
3623
  directory: pickerDirectoryViewOrdered(snapshot, props, order, props.catalogRoutes?.() ?? {}),
3286
3624
  t: props.t,
3287
3625
  goalRemote: props.goalRemote,
3626
+ planPreview: props.planPreview,
3627
+ ...props.planCompact === void 0 ? {} : { planCompact: props.planCompact },
3288
3628
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
3289
3629
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
3290
3630
  }, props.matched.key);
3291
3631
  }
3292
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, resolveInteractionOperations, roleOf }) {
3632
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, planPreview, planCompact, resolveInteractionOperations, roleOf }) {
3293
3633
  const { snapshot, getDirectorySnapshot, load, select } = directory;
3294
3634
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
3295
3635
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -3305,6 +3645,26 @@ window.__ModuleLoader__.load({
3305
3645
  if (execution === void 0 && snapshot.current !== null) setExecution(snapshot.current);
3306
3646
  }, [execution, snapshot.current]);
3307
3647
  /**
3648
+ * The takeover hides the official panel, so this card drives ui-plan's
3649
+ * registered opener: the full plan opens in the right sidebar as soon as a
3650
+ * Session is on screen (identity-deduped inside the binding).
3651
+ */
3652
+ (0, react.useEffect)(() => planPreview.autoOpen(matched.sessionId, review, matched.key), [
3653
+ planPreview,
3654
+ matched.sessionId,
3655
+ matched.key,
3656
+ review
3657
+ ]);
3658
+ /** Official two-part summary: first line as title, first paragraph as body. */
3659
+ const summary = (0, react.useMemo)(() => {
3660
+ const title = (0, _deepseek_ai_dsh_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-line" });
3661
+ const description = (0, _deepseek_ai_dsh_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-paragraph" });
3662
+ return {
3663
+ title,
3664
+ description: description === title ? "" : description
3665
+ };
3666
+ }, [review.plan]);
3667
+ /**
3308
3668
  * Run one takeover action. `force` reaches past a terminal answer state: the
3309
3669
  * withdrawal action must stay reachable, or a rejected answer would leave the
3310
3670
  * card with no way back to the composer.
@@ -3355,9 +3715,38 @@ window.__ModuleLoader__.load({
3355
3715
  throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
3356
3716
  }
3357
3717
  };
3718
+ /**
3719
+ * Compact the history before a switched approval commits anything.
3720
+ *
3721
+ * Only an approval that changes provider/model triggers the Host check —
3722
+ * effort-only changes and same-model approvals execute untouched. A failed
3723
+ * compaction keeps the Plan pending and retryable; `empty` announces that
3724
+ * nothing safe could be reduced and execution proceeds.
3725
+ */
3726
+ const compactBeforeRun = async () => {
3727
+ if (execution === void 0) return;
3728
+ const current = snapshot.current;
3729
+ if (!(current === null || current === void 0 || current.provider !== execution.provider || current.model !== execution.model)) return;
3730
+ if (planCompact === void 0) {
3731
+ console.warn("dsh-model-switch: plan compaction seam absent; executing the switched Plan without it");
3732
+ return;
3733
+ }
3734
+ setNotice(t("plan.compacting", { model: execution.model }));
3735
+ let outcome;
3736
+ try {
3737
+ outcome = await planCompact(matched.sessionId, execution.provider, execution.model);
3738
+ } catch (cause) {
3739
+ throw new Error(t("plan.compactFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
3740
+ }
3741
+ if (outcome.status === "error") throw new Error(t("plan.compactFailed", { message: outcome.message ?? "unknown failure" }));
3742
+ setNotice(outcome.status === "empty" ? t("plan.compactEmpty") : outcome.status === "compacted" ? t("plan.compacted") : null);
3743
+ };
3358
3744
  const onApprove = () => {
3359
3745
  if (execution === void 0 || !executionAllowed || !available || busy || blocked) return;
3360
- settle(() => approveWith(execution));
3746
+ settle(async () => {
3747
+ await compactBeforeRun();
3748
+ await approveWith(execution);
3749
+ });
3361
3750
  };
3362
3751
  const goal = goalRemote();
3363
3752
  const goalReason = goal === void 0 ? t("plan.goalUnsupported") : void 0;
@@ -3370,6 +3759,7 @@ window.__ModuleLoader__.load({
3370
3759
  const onRunAsGoal = () => {
3371
3760
  if (execution === void 0 || !executionAllowed || !available || busy || blocked || goal === void 0) return;
3372
3761
  settle(async () => {
3762
+ await compactBeforeRun();
3373
3763
  const handoff = await setPlanAsGoal(goal, matched.sessionId, review.plan);
3374
3764
  if (handoff.kind === "unsupported") throw new Error(t("plan.goalUnsupported"));
3375
3765
  if (handoff.kind === "failed") {
@@ -3397,6 +3787,15 @@ window.__ModuleLoader__.load({
3397
3787
  className: PlanReviewCard_module_css_default.stripTitle,
3398
3788
  children: t("plan.header")
3399
3789
  }),
3790
+ planPreview.available() && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3791
+ type: "button",
3792
+ className: PlanReviewCard_module_css_default.previewLink,
3793
+ "data-plan-preview-open": true,
3794
+ onClick: () => {
3795
+ planPreview.open(matched.sessionId, review, matched.key);
3796
+ },
3797
+ children: t("plan.openFull")
3798
+ }),
3400
3799
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3401
3800
  className: PlanReviewCard_module_css_default.headerPicker,
3402
3801
  "aria-label": t("plan.execution"),
@@ -3428,15 +3827,16 @@ window.__ModuleLoader__.load({
3428
3827
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3429
3828
  className: PlanReviewCard_module_css_default.body,
3430
3829
  "data-plan-review-scroll": true,
3431
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
3432
- text: review.plan,
3433
- labels: {
3434
- code: {
3435
- copyLabel: t("markdown.copy"),
3436
- copiedLabel: t("markdown.copied")
3437
- },
3438
- footnotes: t("markdown.footnotes")
3439
- }
3830
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3831
+ className: PlanReviewCard_module_css_default.summary,
3832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3833
+ className: PlanReviewCard_module_css_default.summaryTitle,
3834
+ "data-plan-review-summary": true,
3835
+ children: summary.title
3836
+ }), summary.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3837
+ className: PlanReviewCard_module_css_default.summaryText,
3838
+ children: summary.description
3839
+ })]
3440
3840
  })
3441
3841
  }),
3442
3842
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3581,6 +3981,7 @@ window.__ModuleLoader__.load({
3581
3981
  "choice.unavailable": "目录没有这个组合",
3582
3982
  "plan.kicker": "Plan",
3583
3983
  "plan.header": "计划待审",
3984
+ "plan.openFull": "查看完整计划",
3584
3985
  "plan.execution": "执行模型",
3585
3986
  "plan.approve": "确认执行",
3586
3987
  "plan.keep": "拒绝",
@@ -3597,6 +3998,10 @@ window.__ModuleLoader__.load({
3597
3998
  "plan.goalUnsupported": "当前部署未挂载 goal 服务,无法把计划设为目标",
3598
3999
  "plan.responseRejected": "计划答复已被另一客户端处理;已提交的模型切换无法由插件回滚。",
3599
4000
  "plan.cancelRejected": "计划审查已被另一客户端处理,无法返回讨论。",
4001
+ "plan.compacting": "正在压缩上下文,将使用 {model}…",
4002
+ "plan.compacted": "上下文已压缩,开始执行。",
4003
+ "plan.compactEmpty": "没有可压缩的历史,继续执行。",
4004
+ "plan.compactFailed": "压缩上下文失败:{message}(计划仍待批准,可重试)",
3600
4005
  "markdown.copy": "复制代码",
3601
4006
  "markdown.copied": "已复制",
3602
4007
  "markdown.footnotes": "脚注"
@@ -3636,6 +4041,7 @@ window.__ModuleLoader__.load({
3636
4041
  "choice.unavailable": "This combination is not in the catalog",
3637
4042
  "plan.kicker": "Plan",
3638
4043
  "plan.header": "Plan review",
4044
+ "plan.openFull": "View full plan",
3639
4045
  "plan.execution": "Execution model",
3640
4046
  "plan.approve": "Approve",
3641
4047
  "plan.keep": "Keep planning",
@@ -3652,6 +4058,10 @@ window.__ModuleLoader__.load({
3652
4058
  "plan.goalUnsupported": "This deployment mounts no goal service, so the Plan cannot be set as a goal",
3653
4059
  "plan.responseRejected": "Another client already handled this Plan response. A committed model change cannot be rolled back by this plugin.",
3654
4060
  "plan.cancelRejected": "Another client already handled this Plan review, so it cannot return to discussion.",
4061
+ "plan.compacting": "Compacting context with {model}…",
4062
+ "plan.compacted": "Context compacted; starting execution.",
4063
+ "plan.compactEmpty": "No compactable history; continuing.",
4064
+ "plan.compactFailed": "Context compaction failed: {message} (the Plan stays pending and can be retried)",
3655
4065
  "markdown.copy": "Copy code",
3656
4066
  "markdown.copied": "Copied",
3657
4067
  "markdown.footnotes": "Footnotes"
@@ -3674,6 +4084,45 @@ window.__ModuleLoader__.load({
3674
4084
  }
3675
4085
  const EMPTY_ORDER = [];
3676
4086
  /**
4087
+ * Ask the Host to compact this session before a switched approval executes.
4088
+ *
4089
+ * An absent connection degrades to `skipped` (with a console note): the
4090
+ * mechanism protects against long-context timeouts, it must not brick plan
4091
+ * approvals in environments that expose no Host RPC at all.
4092
+ * @param rpcOf - resolves the live connection RPC.
4093
+ * @returns The card-facing runner bound to this seat's connection.
4094
+ */
4095
+ function planCompactFrom(rpcOf) {
4096
+ return async (sessionId, provider, model) => {
4097
+ const rpc = rpcOf();
4098
+ if (rpc === void 0) {
4099
+ console.warn("dsh-model-switch: Host RPC unavailable; executing the Plan without a compaction check");
4100
+ return { status: "skipped" };
4101
+ }
4102
+ try {
4103
+ const response = await callPluginRpc(rpc, "plugin-rpc/model-switch", "plan-compact", {
4104
+ sessionId,
4105
+ provider,
4106
+ model
4107
+ });
4108
+ if (!response.ok) return {
4109
+ status: "error",
4110
+ message: "the Host rejected the plan compaction request"
4111
+ };
4112
+ if (!isPlanCompactOutcome(response.value)) return {
4113
+ status: "error",
4114
+ message: "the Host returned an unreadable plan compaction verdict"
4115
+ };
4116
+ return response.value;
4117
+ } catch (cause) {
4118
+ return {
4119
+ status: "error",
4120
+ message: cause instanceof Error ? cause.message : String(cause)
4121
+ };
4122
+ }
4123
+ };
4124
+ }
4125
+ /**
3677
4126
  * Resolve the released goal remote without hard-depending on it.
3678
4127
  *
3679
4128
  * `remote.goals` exists only when the deployment mounts `@deepseek-ai/dsh-goal`,
@@ -3805,6 +4254,7 @@ window.__ModuleLoader__.load({
3805
4254
  });
3806
4255
  });
3807
4256
  const resolveInteractionOperations = () => interactionOperationsFrom(scope);
4257
+ const planPreview = createPlanPreviewBinding(scope.slots);
3808
4258
  const rpcOf = () => {
3809
4259
  return scope.get("connection", false)?.rpc;
3810
4260
  };
@@ -3831,6 +4281,8 @@ window.__ModuleLoader__.load({
3831
4281
  subscribeMainDefaults: (listener) => mainDefaults.subscribe(listener),
3832
4282
  getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
3833
4283
  goalRemote: () => goalRemoteFrom(scope),
4284
+ planPreview,
4285
+ planCompact: planCompactFrom(rpcOf),
3834
4286
  hooks: {
3835
4287
  directory: directory.store,
3836
4288
  providerOrder: orderStore
@@ -3972,6 +4424,7 @@ window.__ModuleLoader__.load({
3972
4424
  const subagent = deriveConfigForm(owned, deriveSubagentSettings, SUBAGENT_SETTINGS_FIELDS);
3973
4425
  const search = deriveConfigForm(owned, deriveSearchSettings, SEARCH_SETTINGS_FIELDS);
3974
4426
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4427
+ const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
3975
4428
  const saveMain = async (next, expectedRevision) => {
3976
4429
  if (!await mainForm.mutate([
3977
4430
  {
@@ -4036,18 +4489,23 @@ window.__ModuleLoader__.load({
4036
4489
  subagentSettings: subagent,
4037
4490
  searchSettings: search,
4038
4491
  imageSettings: image,
4492
+ autoReviewSettings: autoReview,
4039
4493
  switchSettings: owned
4040
4494
  },
4041
4495
  capabilities: RUNTIME_CAPABILITIES,
4042
4496
  saveMain,
4043
4497
  setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
4044
4498
  setCapability: (route, field, value) => {
4045
- const scope = route === "search" ? search : image;
4499
+ const scope = route === "search" ? search : route === "image" ? image : autoReview;
4046
4500
  return value === void 0 ? scope.unset(field) : scope.set(field, value);
4047
4501
  },
4048
4502
  setCompactOnSwitch: async (value) => {
4049
4503
  if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");
4050
4504
  },
4505
+ setPlanCompactThreshold: async (value) => {
4506
+ if (!Number.isInteger(value) || value < 0) throw new Error("settings-rejected");
4507
+ if (!await owned.set("planCompactThreshold", value)) throw new Error("settings-rejected");
4508
+ },
4051
4509
  providerRoleOf,
4052
4510
  loadCatalog: async () => {
4053
4511
  const response = await remote.session.modelCatalog();