@chaoset/adaptive-perf 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/winliyou/dsh-plugins) (2026-08-25)
4
+
5
+ ### Features
6
+
7
+ * 设置卡片按语义分组折叠(首轮锚定 / 极简提示词层 / 高级 JSON 默认收起):20+ 字段不再一屏平铺,渐进披露;分组头带 `aria-expanded`
8
+ * `bootstrap.maxTokens` 数字输入改草稿态:编辑期保留原始输入(清空/删改中间态不立即跳回 0),失焦才归一化显示,消除输入被强制改写的突兀感
9
+
10
+ ### Bug Fixes
11
+
12
+ * `session/event` 处理器对 `agents.get(session.id)` 的 `agent.id === session.id` 隐含契约加防御性兜底(按 `agent.session.id` 匹配)并注释说明:未来二者分离时晋升信号不再静默丢配导致 bootstrap restrict 不释放
13
+ * 修复 `applyFamilies` 的 JSDoc 注释与函数签名挤在同一行的格式瑕疵
14
+
15
+ ## [0.8.0](https://github.com/winliyou/dsh-plugins) (2026-08-22)
16
+
17
+ ### Features
18
+
19
+ * **`keywordMatchMode` config** (`smart`(default) | `substring` | `word`): escalation keywords containing CJK keep substring matching (CJK has no word boundaries — `\b` would never match a pure-CJK keyword), pure-ASCII words match on word boundaries via lookarounds (`(?<![a-z0-9_])kw(?![a-z0-9_])`, more predictable than `\b`). Statement text like "the goal is to refactor" or "goalish" no longer falsely escalates the goal family on the substring "goal"; `substring` restores pre-0.8.0 behavior; validated in `remote.set` and normalized with fallback
20
+ * explicit empty arrays are now legal values for all string-list config fields (`presets`, `bootstrap.tools`, family tools/keywords…): previously an emptied list was silently reverted to the default on save, so "apply no preset" was impossible to express
21
+
22
+ ### Bug Fixes
23
+
24
+ * pre-step injected-context stripping now requires bootstrap enabled: with `bootstrap.enabled=false` injections were stripped while no discovery tools were registered as compensation — unrecoverable context loss
25
+ * hot-updating `bootstrap.promoteOn` now takes effect for already-tracked sessions: the phase cache is invalidated and rebuilt from the persistent journal (persistent promotion signals survive the rebuild)
26
+ * per-message overhead reduced: composed preset resolved once per agent instead of once per message; suppressed-context-sources Set cached by content key instead of rebuilt per message; preset lookup failure warning capped at 20 occurrences
27
+ * settings card: `promoteOn` is a select, `maxTokens` a number input (invalid input can no longer be saved); loading placeholder for `suppressInjectedContext` matches the real default; error notices render in the header badge so they are visible while collapsed; dirty detection compares field-by-field instead of order-sensitive JSON.stringify; error color referenced the non-existent `state-danger-primary` token so errors never rendered red (now `state-error-primary`)
28
+ * removed `filterBootstrapMessages` (exact duplicate of `stripSuppressedMessages`); fixed malformed nested JSDoc on dev_tool_search; description truncation operator-precedence bug (`split('\n')[0].slice(...)` could throw under `noUncheckedIndexedAccess` semantics)
29
+
30
+ ## [0.7.3](https://github.com/winliyou/dsh-plugins) (2026-08-22)
31
+
32
+ ### Bug Fixes
33
+
34
+ * **P0 dev_tool_search used the post-restrict catalog**: tools hidden by restrict layers were undiscoverable/unlockable; it now searches a pre-restrict snapshot captured at agent setup
35
+ * **P0 escalate() did not recompute the bootstrap deny set**: after an escalation signal, family tools stayed hidden behind the second restrict layer; escalate now resyncs the keep-set so the release takes effect on the next request
36
+ * real Minimal tool pair remounts idempotently via a state key: unrelated hot-updates no longer destroy/recreate persistent PTY bashes (cwd/env/background jobs survive)
37
+ * first-round maxTokens cap: listener always registered with in-callback branching (0→N hot-update works; N→0 strips the field instead of injecting 0); promotion strips any cap injected this session including stale values from before a hot-update change
38
+ * custom tool families defined in settings survive normalizeConfig (previously silently dropped)
39
+ * test mocks made host-faithful (schemas reflect active restrict layers); regression tests added for both P0s
40
+
41
+ ## [0.7.2](https://github.com/winliyou/dsh-plugins) (2026-08-22)
42
+
43
+ * align with dsh 0.1.0-rc.8 dependency set
44
+
3
45
  ## [0.7.0](https://github.com/winliyou/dsh-plugins) (2026-08-18)
4
46
 
5
47
  ### Features
package/README.md CHANGED
@@ -129,7 +129,8 @@ dsh plugin --profile web remove @chaoset/adaptive-perf
129
129
  | `suppressRuntimeContext` | `true` | 抑制运行时上下文快照(零功能损失) |
130
130
  | `suppressInjectedContext` | `true` | 常驻上下文抑制(默认开启):整个会话剥离技能目录/AGENTS.md 注入(发现工具承担可见性);`false` = 只在请求 #1 剥离,晋升后恢复 |
131
131
  | `leanByDefault` | `false` | 工具目录精简(opt-in):`true` = 会话启动即隐藏编排类工具族;`false`(默认)= 零裁剪 |
132
- | `escalateOnKeyword` | `true` | 用户消息命中触发词 → 放行该族 |
132
+ | `escalateOnKeyword` | `true` | 用户消息命中触发词 → 放行该族(匹配方式见 `keywordMatchMode`) |
133
+ | `keywordMatchMode` | `"smart"` | 触发词匹配模式:`smart` = 含 CJK 的触发词子串匹配、纯 ASCII 词词边界匹配(`"goalish"` 不命中 `"goal"`,整词 `"the goal is"` 命中);`substring` = 一律子串;`word` = 一律词边界 |
133
134
  | `escalateOnUnknownTool` | `true` | 工具调用失败(UNKNOWN_TOOL)→ 放行该族 |
134
135
  | `coreTools` | 核心编码工具 | 展示用:不进入任何限制族 |
135
136
  | `families` | 见下 | 工具族:`{ 族名: { enabled, tools, keywords } }` |
package/client/client.cjs CHANGED
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
40
40
  });
41
41
  module.exports = __toCommonJS(index_exports);
42
42
  var React = __toESM(require("react"), 1);
43
- var css = ".ap_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;overflow:hidden}.ap_card[data-open=true]{border-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-shadow-lv1)}.ap_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:10px;align-items:center;gap:12px;padding:12px 14px;display:flex}.ap_header:hover,.ap_card[data-open=true]>.ap_header{background:var(--dsw-alias-interactive-bg-hover)}.ap_title{flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px}.ap_badge{white-space:nowrap;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:16px}.ap_body{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);padding:10px 14px 12px}.ap_field{flex-direction:column;gap:4px;padding:8px 0;display:flex}.ap_label{font-size:12px;font-weight:500;line-height:18px}.ap_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;line-height:20px}.ap_textarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 10px;font-size:12px;line-height:18px;resize:vertical;min-height:120px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ap_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}.ap_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:8px 0 2px;display:flex}.ap_save{font:inherit;cursor:pointer;border:1px solid transparent;border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.ap_discard{font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:0 0;color:inherit}.ap_status{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);flex:1}.ap_status.ap_error{color:var(--dsw-alias-state-danger-primary)}";
43
+ var css = ".ap_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;overflow:hidden}.ap_card[data-open=true]{border-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-shadow-lv1)}.ap_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:10px;align-items:center;gap:12px;padding:12px 14px;display:flex}.ap_header:hover,.ap_card[data-open=true]>.ap_header{background:var(--dsw-alias-interactive-bg-hover)}.ap_title{flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px}.ap_badge{white-space:nowrap;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:16px}.ap_body{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);padding:10px 14px 12px}.ap_field{flex-direction:column;gap:4px;padding:8px 0;display:flex}.ap_label{font-size:12px;font-weight:500;line-height:18px}.ap_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;line-height:20px}.ap_textarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 10px;font-size:12px;line-height:18px;resize:vertical;min-height:120px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ap_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}.ap_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:8px 0 2px;display:flex}.ap_save{font:inherit;cursor:pointer;border:1px solid transparent;border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.ap_discard{font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:0 0;color:inherit}.ap_status{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);flex:1}.ap_section{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:8px;padding-top:2px}.ap_sectionHead{appearance:none;width:100%;font:inherit;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;justify-content:space-between;gap:8px;padding:6px 2px;display:flex}.ap_sectionHead:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.ap_sectionTitle{font-size:12px;font-weight:600;line-height:18px}.ap_sectionChevron{flex:none;font-size:10px}.ap_sectionBody{flex-direction:column;display:flex}.ap_status.ap_error{color:var(--dsw-alias-state-error-primary)}.ap_badge_error{white-space:nowrap;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:16px;max-width:260px;overflow:hidden;text-overflow:ellipsis}";
44
44
  var tagId = "@chaoset/adaptive-perf/client.css";
45
45
  if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
46
46
  tag = document.createElement("style");
@@ -64,7 +64,7 @@ window.__ModuleLoader__.load({
64
64
  invalidFamilies: "families \u5FC5\u987B\u662F\u5408\u6CD5 JSON \u5BF9\u8C61",
65
65
  enabled: "\u542F\u7528\u63D2\u4EF6",
66
66
  presets: "\u76EE\u6807 preset",
67
- fieldPresets: "\u9017\u53F7\u5206\u9694\u7684 preset id\uFF08\u5982 standard, code\uFF09",
67
+ fieldPresets: "\u9017\u53F7\u5206\u9694\u7684 preset id\uFF08\u5982 standard, code\uFF09\u3002\u7559\u7A7A = \u4E0D\u5E94\u7528\u4EFB\u4F55 preset",
68
68
  suppressRuntimeContext: "\u6291\u5236\u8FD0\u884C\u65F6\u4E0A\u4E0B\u6587\u5FEB\u7167",
69
69
  fieldSuppressRuntimeContext: "\u7B49\u540C\u6781\u7B80\u6A21\u5F0F\u7684 includeRuntimeContext: false\uFF0C\u6BCF\u6B21\u8BF7\u6C42\u7701\u6389\u5FEB\u7167\u6587\u672C\uFF08\u96F6\u529F\u80FD\u635F\u5931\uFF09",
70
70
  leanByDefault: "\u542F\u52A8\u5373\u7CBE\u7B80\u5DE5\u5177\u76EE\u5F55",
@@ -98,7 +98,10 @@ window.__ModuleLoader__.load({
98
98
  minimalPromptPersona: "persona \u6587\u672C",
99
99
  fieldMinimalPromptPersona: "\u6309\u540D\u66FF\u6362 deployment:persona\uFF1B\u9ED8\u8BA4\u4E0E\u6781\u7B80\u6A21\u5F0F\u9010\u5B57\u76F8\u540C\u3002\u7559\u7A7A = \u4E0D\u66FF\u6362\uFF08\u4FDD\u7559\u539F persona\uFF09",
100
100
  minimalPromptSuppressSections: "\u5C4F\u853D\u5168\u5C40\u5F15\u5BFC\u6BB5",
101
- fieldMinimalPromptSuppressSections: "\u5C4F\u853D harness:identity / harness:source / app:web-surface \u4E09\u4E2A\u5168\u5C40\u6BB5\uFF08\u7B49\u540C\u6781\u7B80 complete persona \u7684\u6548\u679C\uFF1Bplan-mode \u4E0E PTC \u7684 SDK \u6BB5\u4E0D\u53D7\u5F71\u54CD\uFF09"
101
+ fieldMinimalPromptSuppressSections: "\u5C4F\u853D harness:identity / harness:source / app:web-surface \u4E09\u4E2A\u5168\u5C40\u6BB5\uFF08\u7B49\u540C\u6781\u7B80 complete persona \u7684\u6548\u679C\uFF1Bplan-mode \u4E0E PTC \u7684 SDK \u6BB5\u4E0D\u53D7\u5F71\u54CD\uFF09",
102
+ sectionBootstrap: "\u9996\u8F6E\u951A\u5B9A\uFF08bootstrap\uFF09",
103
+ sectionPrompt: "\u6781\u7B80\u63D0\u793A\u8BCD\u5C42\uFF08\u8BED\u57DF\u951A\u5B9A\uFF09",
104
+ sectionAdvanced: "\u9AD8\u7EA7\uFF1A\u5DE5\u5177\u65CF\u4E0E\u6838\u5FC3\u5DE5\u5177"
102
105
  };
103
106
  var en = {
104
107
  title: "Adaptive performance (adaptive-perf)",
@@ -113,7 +116,7 @@ window.__ModuleLoader__.load({
113
116
  invalidFamilies: "families must be valid JSON object",
114
117
  enabled: "Enable plugin",
115
118
  presets: "Target presets",
116
- fieldPresets: "Comma-separated preset ids (e.g. standard, code)",
119
+ fieldPresets: "Comma-separated preset ids (e.g. standard, code). Empty = apply to no preset.",
117
120
  suppressRuntimeContext: "Suppress runtime-context snapshot",
118
121
  fieldSuppressRuntimeContext: "Same as minimal's includeRuntimeContext: false; drops the snapshot text from every request (zero feature loss)",
119
122
  leanByDefault: "Lean tool catalog at session start",
@@ -147,7 +150,10 @@ window.__ModuleLoader__.load({
147
150
  minimalPromptPersona: "Persona text",
148
151
  fieldMinimalPromptPersona: "Shadows deployment:persona by name; defaults to the exact minimal-mode text. Empty = keep the original persona",
149
152
  minimalPromptSuppressSections: "Suppress global orientation sections",
150
- fieldMinimalPromptSuppressSections: "Shadow harness:identity / harness:source / app:web-surface to empty (same effect as minimal's complete persona; plan-mode and the PTC SDK sections are unaffected)"
153
+ fieldMinimalPromptSuppressSections: "Shadow harness:identity / harness:source / app:web-surface to empty (same effect as minimal's complete persona; plan-mode and the PTC SDK sections are unaffected)",
154
+ sectionBootstrap: "First-request bootstrap",
155
+ sectionPrompt: "Minimal prompt layer (register anchor)",
156
+ sectionAdvanced: "Advanced: tool families & core tools"
151
157
  };
152
158
  function Field(props) {
153
159
  return React.createElement(
@@ -182,6 +188,20 @@ window.__ModuleLoader__.load({
182
188
  })
183
189
  );
184
190
  }
191
+ function Section(props) {
192
+ const [expanded, setExpanded] = React.useState(props.defaultOpen !== false);
193
+ return React.createElement(
194
+ "div",
195
+ { className: "ap_section" },
196
+ React.createElement(
197
+ "button",
198
+ { type: "button", className: "ap_sectionHead", onClick: () => setExpanded(!expanded), "aria-expanded": expanded },
199
+ React.createElement("span", { className: "ap_sectionTitle" }, props.title),
200
+ React.createElement("span", { className: "ap_sectionChevron", "aria-hidden": true }, expanded ? "\u25B2" : "\u25BC")
201
+ ),
202
+ expanded ? React.createElement("div", { className: "ap_sectionBody" }, props.children) : null
203
+ );
204
+ }
185
205
  function familiesToText(families) {
186
206
  return JSON.stringify(families, null, 2);
187
207
  }
@@ -205,12 +225,25 @@ window.__ModuleLoader__.load({
205
225
  }
206
226
  return parsed;
207
227
  }
228
+ function valuesEqual(a, b) {
229
+ if (a === b) return true;
230
+ if (typeof a !== typeof b || a === null || b === null) return false;
231
+ if (typeof a !== "object") return false;
232
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
233
+ if (Array.isArray(a)) {
234
+ return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
235
+ }
236
+ const ka = Object.keys(a);
237
+ const kb = Object.keys(b);
238
+ return ka.length === kb.length && ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && valuesEqual(a[k], b[k]));
239
+ }
208
240
  function AdaptivePerfCard(props) {
209
241
  const t = props.t;
210
242
  const [open, setOpen] = React.useState(false);
211
243
  const [cfg, setCfg] = React.useState(null);
212
244
  const [draft, setDraft] = React.useState(null);
213
245
  const [familiesText, setFamiliesText] = React.useState("");
246
+ const [maxTokensText, setMaxTokensText] = React.useState(null);
214
247
  const [saving, setSaving] = React.useState(false);
215
248
  const [status, setStatus] = React.useState(null);
216
249
  React.useEffect(() => {
@@ -220,6 +253,7 @@ window.__ModuleLoader__.load({
220
253
  setCfg(value);
221
254
  setDraft(value);
222
255
  setFamiliesText(familiesToText(value.families));
256
+ setMaxTokensText(null);
223
257
  }).catch((error) => {
224
258
  if (!cancelled) setStatus({ kind: "error", text: t("loadFailed") + ": " + (error && error.message || error) });
225
259
  });
@@ -237,11 +271,12 @@ window.__ModuleLoader__.load({
237
271
  familiesError = error?.message ?? String(error);
238
272
  }
239
273
  }
240
- const dirty = draft !== null && (JSON.stringify(draft) !== JSON.stringify(cfg) || familiesInvalid === false && familiesToText(draft.families) !== familiesText);
274
+ const dirty = draft !== null && (!valuesEqual(draft, cfg) || familiesInvalid === false && familiesToText(draft.families) !== familiesText);
241
275
  const invalid = draft !== null && familiesInvalid;
242
276
  const discard = () => {
243
277
  setDraft(cfg);
244
278
  setFamiliesText(cfg === null ? "" : familiesToText(cfg.families));
279
+ setMaxTokensText(null);
245
280
  setStatus(null);
246
281
  };
247
282
  const save = () => {
@@ -259,6 +294,7 @@ window.__ModuleLoader__.load({
259
294
  setCfg(next);
260
295
  setDraft(next);
261
296
  setFamiliesText(familiesToText(next.families));
297
+ setMaxTokensText(null);
262
298
  setStatus({ kind: "ok", text: t("saved") });
263
299
  }).catch((error) => {
264
300
  setStatus({ kind: "error", text: t("saveFailed") + ": " + (error && error.message || error) });
@@ -276,6 +312,7 @@ window.__ModuleLoader__.load({
276
312
  "aria-expanded": open
277
313
  },
278
314
  React.createElement("span", { className: "ap_title" }, t("title")),
315
+ status !== null && status.kind === "error" ? React.createElement("span", { className: "ap_badge ap_badge_error", title: status.text }, status.text) : null,
279
316
  dirty ? React.createElement("span", { className: "ap_badge" }, t("unsaved")) : null,
280
317
  React.createElement("span", null, open ? "\u25B2" : "\u25BC")
281
318
  ),
@@ -327,104 +364,142 @@ window.__ModuleLoader__.load({
327
364
  React.createElement(BoolField, {
328
365
  label: t("suppressInjectedContext"),
329
366
  hint: t("fieldSuppressInjectedContext"),
330
- value: draft === null ? false : draft.suppressInjectedContext,
367
+ value: draft === null ? true : draft.suppressInjectedContext,
331
368
  disabled: draft === null,
332
369
  onChange: (v) => setDraft({ ...draft, suppressInjectedContext: v })
333
370
  }),
334
- React.createElement(BoolField, {
335
- label: t("bootstrapEnabled"),
336
- hint: t("fieldBootstrapEnabled"),
337
- value: draft === null ? true : !!draft.bootstrap && draft.bootstrap.enabled,
338
- disabled: draft === null,
339
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, enabled: v } })
340
- }),
341
- React.createElement(BoolField, {
342
- label: t("bootstrapRealPair"),
343
- hint: t("fieldBootstrapRealPair"),
344
- value: draft === null ? true : !!draft.bootstrap && draft.bootstrap.realPair,
345
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
346
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, realPair: v } })
347
- }),
348
- React.createElement(TextField, {
349
- label: t("bootstrapTools"),
350
- hint: t("fieldBootstrapTools"),
351
- value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.tools || []).join(", "),
352
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
353
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, tools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
354
- }),
355
- React.createElement(TextField, {
356
- label: t("bootstrapPromoteOn"),
357
- hint: t("fieldBootstrapPromoteOn"),
358
- value: draft === null ? "" : draft.bootstrap && draft.bootstrap.promoteOn || "either",
359
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
360
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, promoteOn: v.trim() || "either" } })
361
- }),
362
- React.createElement(TextField, {
363
- label: t("bootstrapSuppressed"),
364
- hint: t("fieldBootstrapSuppressed"),
365
- value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.suppressedContextSources || []).join(", "),
366
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
367
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, suppressedContextSources: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
368
- }),
369
- React.createElement(TextField, {
370
- label: t("bootstrapDiscovery"),
371
- hint: t("fieldBootstrapDiscovery"),
372
- value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.discoveryTools || []).join(", "),
373
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
374
- onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, discoveryTools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
375
- }),
376
- React.createElement(TextField, {
377
- label: t("bootstrapMaxTokens"),
378
- hint: t("fieldBootstrapMaxTokens"),
379
- value: draft === null ? "" : String(draft.bootstrap && draft.bootstrap.maxTokens || 0),
380
- disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
381
- onChange: (v) => {
382
- const n = Number(v.trim());
383
- setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, maxTokens: Number.isSafeInteger(n) && n > 0 ? n : 0 } });
384
- }
385
- }),
386
- React.createElement(BoolField, {
387
- label: t("minimalPromptEnabled"),
388
- hint: t("fieldMinimalPromptEnabled"),
389
- value: draft === null ? true : draft.minimalPrompt && draft.minimalPrompt.enabled === true,
390
- disabled: draft === null,
391
- onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, enabled: v } })
392
- }),
393
- React.createElement(BoolField, {
394
- label: t("minimalPromptSuppressSections"),
395
- hint: t("fieldMinimalPromptSuppressSections"),
396
- value: draft === null ? true : draft.minimalPrompt && draft.minimalPrompt.suppressSections === true,
397
- disabled: draft === null || draft.minimalPrompt && draft.minimalPrompt.enabled === false,
398
- onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, suppressSections: v } })
399
- }),
400
- React.createElement(TextField, {
401
- label: t("minimalPromptPersona"),
402
- hint: t("fieldMinimalPromptPersona"),
403
- value: draft === null ? "" : draft.minimalPrompt && draft.minimalPrompt.persona || "",
404
- disabled: draft === null || draft.minimalPrompt && draft.minimalPrompt.enabled === false,
405
- onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, persona: v } })
406
- }),
371
+ // ── 首轮锚定(bootstrap)分组 ──
372
+ React.createElement(
373
+ Section,
374
+ { title: t("sectionBootstrap") },
375
+ React.createElement(BoolField, {
376
+ label: t("bootstrapEnabled"),
377
+ hint: t("fieldBootstrapEnabled"),
378
+ value: draft === null ? true : !!draft.bootstrap && draft.bootstrap.enabled,
379
+ disabled: draft === null,
380
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, enabled: v } })
381
+ }),
382
+ React.createElement(BoolField, {
383
+ label: t("bootstrapRealPair"),
384
+ hint: t("fieldBootstrapRealPair"),
385
+ value: draft === null ? true : !!draft.bootstrap && draft.bootstrap.realPair,
386
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
387
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, realPair: v } })
388
+ }),
389
+ React.createElement(TextField, {
390
+ label: t("bootstrapTools"),
391
+ hint: t("fieldBootstrapTools"),
392
+ value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.tools || []).join(", "),
393
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
394
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, tools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
395
+ }),
396
+ React.createElement(
397
+ Field,
398
+ {
399
+ label: t("bootstrapPromoteOn"),
400
+ hint: t("fieldBootstrapPromoteOn")
401
+ },
402
+ React.createElement(
403
+ "select",
404
+ {
405
+ className: "ap_input",
406
+ value: draft === null ? "either" : draft.bootstrap && draft.bootstrap.promoteOn || "either",
407
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
408
+ onChange: (e) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, promoteOn: e.target.value } })
409
+ },
410
+ ["either", "tool-call", "assistant-message"].map((v) => React.createElement("option", { key: v, value: v }, v))
411
+ )
412
+ ),
413
+ React.createElement(TextField, {
414
+ label: t("bootstrapSuppressed"),
415
+ hint: t("fieldBootstrapSuppressed"),
416
+ value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.suppressedContextSources || []).join(", "),
417
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
418
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, suppressedContextSources: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
419
+ }),
420
+ React.createElement(TextField, {
421
+ label: t("bootstrapDiscovery"),
422
+ hint: t("fieldBootstrapDiscovery"),
423
+ value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.discoveryTools || []).join(", "),
424
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
425
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, discoveryTools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
426
+ }),
427
+ React.createElement(
428
+ Field,
429
+ {
430
+ label: t("bootstrapMaxTokens"),
431
+ hint: t("fieldBootstrapMaxTokens")
432
+ },
433
+ React.createElement("input", {
434
+ className: "ap_input",
435
+ type: "number",
436
+ min: 0,
437
+ step: 1,
438
+ // 草稿态:编辑期间显示原始输入(清空/删改中间态不立即跳回 0),
439
+ // 失焦后才把显示归一化为生效数字,避免输入被强制改写的突兀感。
440
+ value: maxTokensText !== null ? maxTokensText : String(draft === null ? 0 : draft.bootstrap && draft.bootstrap.maxTokens || 0),
441
+ disabled: draft === null || draft.bootstrap && draft.bootstrap.enabled === false,
442
+ onChange: (e) => {
443
+ const n = Number(e.target.value);
444
+ setMaxTokensText(e.target.value);
445
+ setDraft({ ...draft, bootstrap: { ...draft.bootstrap || {}, maxTokens: Number.isSafeInteger(n) && n > 0 ? n : 0 } });
446
+ },
447
+ onBlur: () => setMaxTokensText(null)
448
+ })
449
+ )
450
+ ),
451
+ // ── 极简提示词层分组 ──
407
452
  React.createElement(
408
- Field,
409
- {
410
- label: t("families"),
411
- hint: familiesInvalid ? t("invalidFamilies") + (familiesError !== null ? " \u2014 " + familiesError : "") : t("fieldFamilies")
412
- },
413
- React.createElement("textarea", {
414
- className: "ap_textarea",
415
- value: familiesText,
453
+ Section,
454
+ { title: t("sectionPrompt") },
455
+ React.createElement(BoolField, {
456
+ label: t("minimalPromptEnabled"),
457
+ hint: t("fieldMinimalPromptEnabled"),
458
+ value: draft === null ? true : draft.minimalPrompt && draft.minimalPrompt.enabled === true,
416
459
  disabled: draft === null,
417
- spellCheck: false,
418
- onChange: (e) => setFamiliesText(e.target.value)
460
+ onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, enabled: v } })
461
+ }),
462
+ React.createElement(BoolField, {
463
+ label: t("minimalPromptSuppressSections"),
464
+ hint: t("fieldMinimalPromptSuppressSections"),
465
+ value: draft === null ? true : draft.minimalPrompt && draft.minimalPrompt.suppressSections === true,
466
+ disabled: draft === null || draft.minimalPrompt && draft.minimalPrompt.enabled === false,
467
+ onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, suppressSections: v } })
468
+ }),
469
+ React.createElement(TextField, {
470
+ label: t("minimalPromptPersona"),
471
+ hint: t("fieldMinimalPromptPersona"),
472
+ value: draft === null ? "" : draft.minimalPrompt && draft.minimalPrompt.persona || "",
473
+ disabled: draft === null || draft.minimalPrompt && draft.minimalPrompt.enabled === false,
474
+ onChange: (v) => setDraft({ ...draft, minimalPrompt: { ...draft.minimalPrompt || {}, persona: v } })
419
475
  })
420
476
  ),
477
+ // ── 高级分组(默认收起):工具族 JSON 与核心工具展示 ──
421
478
  React.createElement(
422
- Field,
423
- {
424
- label: t("coreTools"),
425
- hint: t("fieldCoreTools")
426
- },
427
- React.createElement("p", { className: "ap_hint" }, draft === null ? "" : (draft.coreTools || []).join(", "))
479
+ Section,
480
+ { title: t("sectionAdvanced"), defaultOpen: false },
481
+ React.createElement(
482
+ Field,
483
+ {
484
+ label: t("families"),
485
+ hint: familiesInvalid ? t("invalidFamilies") + (familiesError !== null ? " \u2014 " + familiesError : "") : t("fieldFamilies")
486
+ },
487
+ React.createElement("textarea", {
488
+ className: "ap_textarea",
489
+ value: familiesText,
490
+ disabled: draft === null,
491
+ spellCheck: false,
492
+ onChange: (e) => setFamiliesText(e.target.value)
493
+ })
494
+ ),
495
+ React.createElement(
496
+ Field,
497
+ {
498
+ label: t("coreTools"),
499
+ hint: t("fieldCoreTools")
500
+ },
501
+ React.createElement("p", { className: "ap_hint" }, draft === null ? "" : (draft.coreTools || []).join(", "))
502
+ )
428
503
  ),
429
504
  React.createElement(
430
505
  "div",