dsh-model-switch 0.4.18 → 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 = [
@@ -3063,6 +3397,63 @@ window.__ModuleLoader__.load({
3063
3397
  throw new PlanDismissalUnsupportedError("this DSH Host exposes neither PendingQuestion.dismiss() (0.2.x) nor PendingQuestion.cancel() (0.1.x)");
3064
3398
  }
3065
3399
  //#endregion
3400
+ //#region src/picker/plan-goal.ts
3401
+ function failure(result, fallback) {
3402
+ if (result.ok) return { message: fallback };
3403
+ const { code, message } = result.error;
3404
+ return {
3405
+ code,
3406
+ message: message.trim() === "" ? fallback : message
3407
+ };
3408
+ }
3409
+ /**
3410
+ * Make the plan text this session's armed goal, replacing any unfinished goal.
3411
+ *
3412
+ * Ordering mirrors the card's own commit-then-answer rule: a refusal here keeps
3413
+ * the Plan pending instead of approving work that no goal tracks. The replace
3414
+ * path clears rather than edits so a new plan receives a fresh round budget —
3415
+ * `edit` would inherit the rounds the previous goal already spent.
3416
+ *
3417
+ * @param remote - the released goal remote, absent when no goal package is mounted.
3418
+ * @param sessionId - the session that owns both the review and the goal.
3419
+ * @param plan - the plan markdown under review.
3420
+ * @returns Whether the goal now tracks this plan, or why it does not.
3421
+ */
3422
+ async function setPlanAsGoal(remote, sessionId, plan) {
3423
+ if (remote === void 0) return { kind: "unsupported" };
3424
+ const objective = plan.trim();
3425
+ if (objective === "") return {
3426
+ kind: "failed",
3427
+ code: "GOAL_INVALID_OBJECTIVE",
3428
+ message: "goal objective must be a non-empty string"
3429
+ };
3430
+ const created = await remote.create(sessionId, { objective });
3431
+ if (created.ok) return { kind: "created" };
3432
+ if (created.error.code !== "GOAL_ALREADY_EXISTS") return {
3433
+ kind: "failed",
3434
+ ...failure(created, "the goal could not be created")
3435
+ };
3436
+ const current = await remote.get(sessionId);
3437
+ if (!current.ok || current.value === void 0 || current.value === null) return {
3438
+ kind: "failed",
3439
+ ...failure(current, "the current goal could not be read for replacement")
3440
+ };
3441
+ const cleared = await remote.clear(sessionId, {
3442
+ id: current.value.id,
3443
+ revision: current.value.revision
3444
+ });
3445
+ if (!cleared.ok) return {
3446
+ kind: "failed",
3447
+ ...failure(cleared, "the current goal could not be cleared")
3448
+ };
3449
+ const retry = await remote.create(sessionId, { objective });
3450
+ if (retry.ok) return { kind: "replaced" };
3451
+ return {
3452
+ kind: "failed",
3453
+ ...failure(retry, "the goal could not be created")
3454
+ };
3455
+ }
3456
+ //#endregion
3066
3457
  //#region src/client/picker/RetryBoundary.tsx
3067
3458
  /** Own retryable React error-boundary state while callers own presentation and copy. */
3068
3459
  var RetryBoundary = class extends react.Component {
@@ -3082,7 +3473,7 @@ window.__ModuleLoader__.load({
3082
3473
  };
3083
3474
  //#endregion
3084
3475
  //#region \0dsh-css:src/client/picker/PlanReviewCard.module.css.mjs
3085
- 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_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_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}}";
3086
3477
  const tagId = "dsh-model-switch/PlanReviewCard.module.css";
3087
3478
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
3088
3479
  const tag = document.createElement("style");
@@ -3104,13 +3495,18 @@ window.__ModuleLoader__.load({
3104
3495
  "feedbackRow": "BylfoW_feedbackRow",
3105
3496
  "footer": "BylfoW_footer",
3106
3497
  "frame": "BylfoW_frame",
3498
+ "goalRun": "BylfoW_goalRun",
3107
3499
  "headerPicker": "BylfoW_headerPicker",
3108
3500
  "keep": "BylfoW_keep",
3109
3501
  "picker": "BylfoW_picker",
3110
3502
  "pickerError": "BylfoW_pickerError",
3503
+ "previewLink": "BylfoW_previewLink",
3111
3504
  "recover": "BylfoW_recover",
3112
3505
  "strip": "BylfoW_strip",
3113
- "stripTitle": "BylfoW_stripTitle"
3506
+ "stripTitle": "BylfoW_stripTitle",
3507
+ "summary": "BylfoW_summary",
3508
+ "summaryText": "BylfoW_summaryText",
3509
+ "summaryTitle": "BylfoW_summaryTitle"
3114
3510
  };
3115
3511
  //#endregion
3116
3512
  //#region src/client/picker/PlanReviewCard.tsx
@@ -3226,11 +3622,14 @@ window.__ModuleLoader__.load({
3226
3622
  ...settingsUnavailableReason === void 0 ? {} : { settingsUnavailableReason },
3227
3623
  directory: pickerDirectoryViewOrdered(snapshot, props, order, props.catalogRoutes?.() ?? {}),
3228
3624
  t: props.t,
3625
+ goalRemote: props.goalRemote,
3626
+ planPreview: props.planPreview,
3627
+ ...props.planCompact === void 0 ? {} : { planCompact: props.planCompact },
3229
3628
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
3230
3629
  ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
3231
3630
  }, props.matched.key);
3232
3631
  }
3233
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, resolveInteractionOperations, roleOf }) {
3632
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, settingsUnavailableReason, directory, t, goalRemote, planPreview, planCompact, resolveInteractionOperations, roleOf }) {
3234
3633
  const { snapshot, getDirectorySnapshot, load, select } = directory;
3235
3634
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
3236
3635
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -3246,6 +3645,26 @@ window.__ModuleLoader__.load({
3246
3645
  if (execution === void 0 && snapshot.current !== null) setExecution(snapshot.current);
3247
3646
  }, [execution, snapshot.current]);
3248
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
+ /**
3249
3668
  * Run one takeover action. `force` reaches past a terminal answer state: the
3250
3669
  * withdrawal action must stay reachable, or a rejected answer would leave the
3251
3670
  * card with no way back to the composer.
@@ -3280,22 +3699,75 @@ window.__ModuleLoader__.load({
3280
3699
  blocked,
3281
3700
  error
3282
3701
  }, available, executionAllowed);
3702
+ /** Approve this review after committing its execution model; a refused commit keeps the Plan pending. */
3703
+ const approveWith = async (model) => {
3704
+ if (!await approvePlanReview({
3705
+ select,
3706
+ selection: model,
3707
+ current: snapshot.current,
3708
+ answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true),
3709
+ onNotRestored: (switched) => {
3710
+ setNotice(t("plan.notRestored", { model: switched }));
3711
+ console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", switched);
3712
+ }
3713
+ })) {
3714
+ const message = getDirectorySnapshot().error;
3715
+ throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
3716
+ }
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
+ };
3283
3744
  const onApprove = () => {
3284
3745
  if (execution === void 0 || !executionAllowed || !available || busy || blocked) return;
3285
3746
  settle(async () => {
3286
- if (!await approvePlanReview({
3287
- select,
3288
- selection: execution,
3289
- current: snapshot.current,
3290
- answer: () => respondAnswer(matched, review.id, review.approve.label, t("plan.responseRejected"), true),
3291
- onNotRestored: (model) => {
3292
- setNotice(t("plan.notRestored", { model }));
3293
- console.warn("dsh-model-switch: the Session model changed but the Main default was not restored", model);
3294
- }
3295
- })) {
3296
- const message = getDirectorySnapshot().error;
3297
- throw new Error(message === null ? t("plan.modelFailed") : t("error.action", { message }));
3747
+ await compactBeforeRun();
3748
+ await approveWith(execution);
3749
+ });
3750
+ };
3751
+ const goal = goalRemote();
3752
+ const goalReason = goal === void 0 ? t("plan.goalUnsupported") : void 0;
3753
+ /**
3754
+ * Arm this plan as the session's goal first, then approve. The goal must be
3755
+ * set before the answer because a refusal must leave the Plan pending, and
3756
+ * because only an armed goal lets the round driver carry the plan after plan
3757
+ * mode exits.
3758
+ */
3759
+ const onRunAsGoal = () => {
3760
+ if (execution === void 0 || !executionAllowed || !available || busy || blocked || goal === void 0) return;
3761
+ settle(async () => {
3762
+ await compactBeforeRun();
3763
+ const handoff = await setPlanAsGoal(goal, matched.sessionId, review.plan);
3764
+ if (handoff.kind === "unsupported") throw new Error(t("plan.goalUnsupported"));
3765
+ if (handoff.kind === "failed") {
3766
+ const message = handoff.code === void 0 ? handoff.message : `${handoff.code}: ${handoff.message}`;
3767
+ throw new Error(t("plan.goalFailed", { message }));
3298
3768
  }
3769
+ if (handoff.kind === "replaced") setNotice(t("plan.goalReplaced"));
3770
+ await approveWith(execution);
3299
3771
  });
3300
3772
  };
3301
3773
  const decline = review.decline;
@@ -3315,6 +3787,15 @@ window.__ModuleLoader__.load({
3315
3787
  className: PlanReviewCard_module_css_default.stripTitle,
3316
3788
  children: t("plan.header")
3317
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
+ }),
3318
3799
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3319
3800
  className: PlanReviewCard_module_css_default.headerPicker,
3320
3801
  "aria-label": t("plan.execution"),
@@ -3346,15 +3827,16 @@ window.__ModuleLoader__.load({
3346
3827
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3347
3828
  className: PlanReviewCard_module_css_default.body,
3348
3829
  "data-plan-review-scroll": true,
3349
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
3350
- text: review.plan,
3351
- labels: {
3352
- code: {
3353
- copyLabel: t("markdown.copy"),
3354
- copiedLabel: t("markdown.copied")
3355
- },
3356
- footnotes: t("markdown.footnotes")
3357
- }
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
+ })]
3358
3840
  })
3359
3841
  }),
3360
3842
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3413,6 +3895,14 @@ window.__ModuleLoader__.load({
3413
3895
  },
3414
3896
  children: t("plan.keep")
3415
3897
  }),
3898
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3899
+ variant: "outline",
3900
+ className: PlanReviewCard_module_css_default.goalRun,
3901
+ disabled: action.approveDisabled || goalReason !== void 0,
3902
+ ...goalReason === void 0 ? {} : { title: goalReason },
3903
+ onClick: onRunAsGoal,
3904
+ children: t("plan.goalRun")
3905
+ }),
3416
3906
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3417
3907
  variant: "primary",
3418
3908
  className: PlanReviewCard_module_css_default.approve,
@@ -3491,6 +3981,7 @@ window.__ModuleLoader__.load({
3491
3981
  "choice.unavailable": "目录没有这个组合",
3492
3982
  "plan.kicker": "Plan",
3493
3983
  "plan.header": "计划待审",
3984
+ "plan.openFull": "查看完整计划",
3494
3985
  "plan.execution": "执行模型",
3495
3986
  "plan.approve": "确认执行",
3496
3987
  "plan.keep": "拒绝",
@@ -3501,8 +3992,16 @@ window.__ModuleLoader__.load({
3501
3992
  "plan.feedbackPlaceholder": "说明要改什么(可留空)",
3502
3993
  "plan.notRestored": "执行模型已切换,但主模型默认值未能还原({model});请在「设置 → 模型」中确认默认模型。",
3503
3994
  "plan.dismissUnsupported": "当前 DSH 版本既未提供 PendingQuestion.dismiss() 也未提供 cancel(),无法回到聊天;请直接在输入框里发送你的意见。",
3995
+ "plan.goalRun": "设置为目标并执行",
3996
+ "plan.goalFailed": "设置目标失败:{message}",
3997
+ "plan.goalReplaced": "已替换本会话原有的目标",
3998
+ "plan.goalUnsupported": "当前部署未挂载 goal 服务,无法把计划设为目标",
3504
3999
  "plan.responseRejected": "计划答复已被另一客户端处理;已提交的模型切换无法由插件回滚。",
3505
4000
  "plan.cancelRejected": "计划审查已被另一客户端处理,无法返回讨论。",
4001
+ "plan.compacting": "正在压缩上下文,将使用 {model}…",
4002
+ "plan.compacted": "上下文已压缩,开始执行。",
4003
+ "plan.compactEmpty": "没有可压缩的历史,继续执行。",
4004
+ "plan.compactFailed": "压缩上下文失败:{message}(计划仍待批准,可重试)",
3506
4005
  "markdown.copy": "复制代码",
3507
4006
  "markdown.copied": "已复制",
3508
4007
  "markdown.footnotes": "脚注"
@@ -3542,6 +4041,7 @@ window.__ModuleLoader__.load({
3542
4041
  "choice.unavailable": "This combination is not in the catalog",
3543
4042
  "plan.kicker": "Plan",
3544
4043
  "plan.header": "Plan review",
4044
+ "plan.openFull": "View full plan",
3545
4045
  "plan.execution": "Execution model",
3546
4046
  "plan.approve": "Approve",
3547
4047
  "plan.keep": "Keep planning",
@@ -3552,8 +4052,16 @@ window.__ModuleLoader__.load({
3552
4052
  "plan.feedbackPlaceholder": "Say what to change (optional)",
3553
4053
  "plan.notRestored": "The execution model switched, but the Main default could not be restored ({model}); confirm the default in Settings → Models.",
3554
4054
  "plan.dismissUnsupported": "This DSH Host exposes neither PendingQuestion.dismiss() nor cancel(), so the review cannot return to chat; send your feedback as a message instead.",
4055
+ "plan.goalRun": "Set as goal and run",
4056
+ "plan.goalFailed": "Could not set the goal: {message}",
4057
+ "plan.goalReplaced": "Replaced this session's previous goal",
4058
+ "plan.goalUnsupported": "This deployment mounts no goal service, so the Plan cannot be set as a goal",
3555
4059
  "plan.responseRejected": "Another client already handled this Plan response. A committed model change cannot be rolled back by this plugin.",
3556
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)",
3557
4065
  "markdown.copy": "Copy code",
3558
4066
  "markdown.copied": "Copied",
3559
4067
  "markdown.footnotes": "Footnotes"
@@ -3576,6 +4084,68 @@ window.__ModuleLoader__.load({
3576
4084
  }
3577
4085
  const EMPTY_ORDER = [];
3578
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
+ /**
4126
+ * Resolve the released goal remote without hard-depending on it.
4127
+ *
4128
+ * `remote.goals` exists only when the deployment mounts `@deepseek-ai/dsh-goal`,
4129
+ * so the capability must be looked up per render rather than injected: an
4130
+ * `inject` on a dotted service name would otherwise block this whole client
4131
+ * plugin on a deployment that does not mount the goal package. The dotted name
4132
+ * is read first, with the namespace object as the fallback shape.
4133
+ * @param ctx - client root context holding the remote namespace.
4134
+ * @returns The goal remote, or undefined when no goal service is mounted.
4135
+ */
4136
+ function goalRemoteFrom(ctx) {
4137
+ const read = (name) => {
4138
+ try {
4139
+ return ctx.get(name, false);
4140
+ } catch {
4141
+ return;
4142
+ }
4143
+ };
4144
+ const nested = read("remote.goals");
4145
+ if (nested !== void 0 && nested !== null) return nested;
4146
+ return read("remote")?.goals;
4147
+ }
4148
+ /**
3579
4149
  * Bind the optional Providers entry order as a React external store.
3580
4150
  * @returns A subscribable order snapshot with an invalidation hook for directory changes.
3581
4151
  */
@@ -3684,6 +4254,7 @@ window.__ModuleLoader__.load({
3684
4254
  });
3685
4255
  });
3686
4256
  const resolveInteractionOperations = () => interactionOperationsFrom(scope);
4257
+ const planPreview = createPlanPreviewBinding(scope.slots);
3687
4258
  const rpcOf = () => {
3688
4259
  return scope.get("connection", false)?.rpc;
3689
4260
  };
@@ -3709,6 +4280,9 @@ window.__ModuleLoader__.load({
3709
4280
  },
3710
4281
  subscribeMainDefaults: (listener) => mainDefaults.subscribe(listener),
3711
4282
  getMainDefaultsSnapshot: () => mainDefaults.getSnapshot(),
4283
+ goalRemote: () => goalRemoteFrom(scope),
4284
+ planPreview,
4285
+ planCompact: planCompactFrom(rpcOf),
3712
4286
  hooks: {
3713
4287
  directory: directory.store,
3714
4288
  providerOrder: orderStore
@@ -3850,6 +4424,7 @@ window.__ModuleLoader__.load({
3850
4424
  const subagent = deriveConfigForm(owned, deriveSubagentSettings, SUBAGENT_SETTINGS_FIELDS);
3851
4425
  const search = deriveConfigForm(owned, deriveSearchSettings, SEARCH_SETTINGS_FIELDS);
3852
4426
  const image = deriveConfigForm(owned, deriveImageSettings, IMAGE_SETTINGS_FIELDS);
4427
+ const autoReview = deriveConfigForm(owned, deriveAutoReviewSettings, AUTO_REVIEW_SETTINGS_FIELDS);
3853
4428
  const saveMain = async (next, expectedRevision) => {
3854
4429
  if (!await mainForm.mutate([
3855
4430
  {
@@ -3914,18 +4489,23 @@ window.__ModuleLoader__.load({
3914
4489
  subagentSettings: subagent,
3915
4490
  searchSettings: search,
3916
4491
  imageSettings: image,
4492
+ autoReviewSettings: autoReview,
3917
4493
  switchSettings: owned
3918
4494
  },
3919
4495
  capabilities: RUNTIME_CAPABILITIES,
3920
4496
  saveMain,
3921
4497
  setSubagent: (field, value) => value === void 0 ? subagent.unset(field) : subagent.set(field, value),
3922
4498
  setCapability: (route, field, value) => {
3923
- const scope = route === "search" ? search : image;
4499
+ const scope = route === "search" ? search : route === "image" ? image : autoReview;
3924
4500
  return value === void 0 ? scope.unset(field) : scope.set(field, value);
3925
4501
  },
3926
4502
  setCompactOnSwitch: async (value) => {
3927
4503
  if (!await owned.set("compactOnSwitch", value)) throw new Error("settings-rejected");
3928
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
+ },
3929
4509
  providerRoleOf,
3930
4510
  loadCatalog: async () => {
3931
4511
  const response = await remote.session.modelCatalog();