dsh-model-switch 0.4.37 → 0.4.39

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
@@ -8,6 +8,317 @@ window.__ModuleLoader__.load({
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
9
  let react_dom = require("react-dom");
10
10
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
+ /** Catalog lookup by id. */
12
+ const SOURCE_BY_ID = new Map([
13
+ {
14
+ id: "core",
15
+ kind: "core",
16
+ label: "DSH 核心",
17
+ repo: "https://github.com/deepseek-ai/deepseek-harness"
18
+ },
19
+ {
20
+ id: "dsh-ops",
21
+ kind: "plugin",
22
+ label: "插件 · dsh-ops",
23
+ repo: "https://github.com/T-Auto/dsh-ops"
24
+ },
25
+ {
26
+ id: "dsh-browser-cdp",
27
+ kind: "plugin",
28
+ label: "插件 · dsh-browser-cdp",
29
+ repo: "https://github.com/drscrewdriver/dsh-browser-cdp"
30
+ },
31
+ {
32
+ id: "dsh-auto-memory",
33
+ kind: "plugin",
34
+ label: "插件 · dsh-auto-memory",
35
+ repo: "https://github.com/AskTheWay/dsh-auto-memory"
36
+ },
37
+ {
38
+ id: "dsh-model-switch",
39
+ kind: "plugin",
40
+ label: "插件 · dsh-model-switch",
41
+ repo: "https://github.com/vandoor7-droid/dsh-model-switch"
42
+ },
43
+ {
44
+ id: "mcp:wincu",
45
+ kind: "mcp",
46
+ label: "MCP · wincu(dsh-computer-use-win)",
47
+ repo: "https://github.com/Yu-tao-Li/dsh-computer-use-win"
48
+ },
49
+ {
50
+ id: "qqbot-extension",
51
+ kind: "extension",
52
+ label: "QQ 机器人扩展(框架 @zaofan/dsh-qqbot)",
53
+ repo: "https://github.com/zaofan-make/dsh-qqbot"
54
+ }
55
+ ].map((entry) => [entry.id, entry]));
56
+ /**
57
+ * Tool names the released Host owns, calibrated against dsh 0.2.1-rc.2.
58
+ *
59
+ * A name outside the catalog would otherwise be reported as core, which is the
60
+ * one guess a reader could act on wrongly; it resolves to `unknown` instead.
61
+ */
62
+ const CORE_TOOLS = /* @__PURE__ */ new Set([
63
+ "ask_user_question",
64
+ "create_goal",
65
+ "create_skill",
66
+ "create_worktree",
67
+ "edit",
68
+ "exit_plan_mode",
69
+ "get_goal",
70
+ "glob",
71
+ "grep",
72
+ "interrupt_agent",
73
+ "job_kill",
74
+ "job_list",
75
+ "job_output",
76
+ "list_agents",
77
+ "list_mcp_resource_templates",
78
+ "list_mcp_resources",
79
+ "load_workspace_dependencies",
80
+ "present",
81
+ "pwsh",
82
+ "ralph",
83
+ "read",
84
+ "read_image",
85
+ "read_mcp_resource",
86
+ "schedule_create",
87
+ "schedule_delete",
88
+ "schedule_list",
89
+ "schedule_update",
90
+ "send_message",
91
+ "session_event_read",
92
+ "session_event_search",
93
+ "session_event_trace",
94
+ "session_search",
95
+ "session_trace",
96
+ "sidebar_open",
97
+ "skill",
98
+ "spawn_teammate",
99
+ "subagent",
100
+ "subagent_fork",
101
+ "team_task_create",
102
+ "team_task_get",
103
+ "team_task_list",
104
+ "team_task_update",
105
+ "terminal_close",
106
+ "terminal_list",
107
+ "terminal_open",
108
+ "terminal_read",
109
+ "terminal_send",
110
+ "terminal_signal",
111
+ "todo_write",
112
+ "update_goal",
113
+ "wait_agent",
114
+ "web_fetch",
115
+ "web_search",
116
+ "web_search_pro",
117
+ "workflow",
118
+ "working_directory",
119
+ "write"
120
+ ]);
121
+ /** MCP server name → catalog id, for the servers this allowlist knows. */
122
+ const MCP_SERVERS = /* @__PURE__ */ new Map([["wincu", "mcp:wincu"]]);
123
+ /** Plugin identity by tool-name prefix (checked before the core catalog). */
124
+ const PLUGIN_PREFIXES = [
125
+ ["bcdp_", "dsh-browser-cdp"],
126
+ ["browser_window_", "dsh-browser-cdp"],
127
+ ["ops_", "dsh-ops"],
128
+ ["memory_", "dsh-auto-memory"]
129
+ ];
130
+ /** Single plugin tools that carry no prefix. */
131
+ const PLUGIN_TOOLS = /* @__PURE__ */ new Map([["generate_image", "dsh-model-switch"]]);
132
+ /** `<base>__<owner>` — the extension-tool naming the QQ bot framework uses. */
133
+ const EXTENSION_TOOL = /^([A-Za-z0-9_-]+)__(.+)$/;
134
+ /**
135
+ * Default allowlist: every pure-read tool this deployment exposes, calibrated on
136
+ * a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
137
+ *
138
+ * Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
139
+ * Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
140
+ * the bot's own name, so `sessions_list__*` keeps working when the persona is
141
+ * renamed.
142
+ */
143
+ const DEFAULT_READ_ONLY_TOOLS = [
144
+ "read",
145
+ "grep",
146
+ "glob",
147
+ "read_image",
148
+ "skill",
149
+ "web_search",
150
+ "web_search_pro",
151
+ "web_fetch",
152
+ "job_list",
153
+ "job_output",
154
+ "terminal_list",
155
+ "terminal_read",
156
+ "list_agents",
157
+ "team_task_list",
158
+ "team_task_get",
159
+ "get_goal",
160
+ "schedule_list",
161
+ "list_mcp_resources",
162
+ "list_mcp_resource_templates",
163
+ "read_mcp_resource",
164
+ "session_search",
165
+ "session_event_search",
166
+ "session_event_read",
167
+ "session_event_trace",
168
+ "session_trace",
169
+ "ops_read_files",
170
+ "ops_discover_tools",
171
+ "bcdp_status",
172
+ "bcdp_help",
173
+ "bcdp_doctor",
174
+ "bcdp_snapshot",
175
+ "browser_window_status",
176
+ "memory_list",
177
+ "memory_read",
178
+ "mcp__wincu__windows_computer_use_accessibility_tree",
179
+ "mcp__wincu__windows_computer_use_element_info",
180
+ "mcp__wincu__windows_computer_use_find",
181
+ "mcp__wincu__windows_computer_use_health",
182
+ "mcp__wincu__windows_computer_use_list_windows",
183
+ "mcp__wincu__windows_computer_use_ocr",
184
+ "mcp__wincu__windows_computer_use_screenshot",
185
+ "mcp__wincu__windows_computer_use_snapshot",
186
+ "mcp__wincu__windows_computer_use_tools",
187
+ "mcp__wincu__windows_computer_use_wait",
188
+ "mcp__wincu__windows_computer_use_wait_for",
189
+ "sessions_list__*",
190
+ "bing_search__*"
191
+ ];
192
+ /**
193
+ * Validate one allowlist entry.
194
+ *
195
+ * `*` is a suffix wildcard: the entry must keep at least one literal leading
196
+ * character, so a stray `*` can never turn the list into "allow everything".
197
+ *
198
+ * @param entry - the raw entry as typed in settings.
199
+ * @returns `ok`, or the specific problem.
200
+ */
201
+ function validateAllowEntry(entry) {
202
+ const trimmed = entry.trim();
203
+ if (trimmed === "") return "empty";
204
+ if (/^\*+$/.test(trimmed)) return "bare-wildcard";
205
+ if (trimmed.startsWith("*")) return "leading-wildcard";
206
+ return "ok";
207
+ }
208
+ /**
209
+ * Whether a tool name matches one allowlist entry.
210
+ *
211
+ * Exact names match exactly; an entry may end in `*`, which matches any
212
+ * remaining characters (including none). Invalid entries never match.
213
+ *
214
+ * @param name - the tool name from `exec.name`.
215
+ * @param entry - one allowlist entry.
216
+ * @returns true when the call may skip review.
217
+ */
218
+ function matchesAllowPattern(name, entry) {
219
+ if (name === "" || validateAllowEntry(entry) !== "ok") return false;
220
+ const trimmed = entry.trim();
221
+ const star = trimmed.indexOf("*");
222
+ if (star === -1) return name === trimmed;
223
+ const prefix = trimmed.slice(0, star);
224
+ const suffix = trimmed.slice(star + 1);
225
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
226
+ return name.length >= prefix.length + suffix.length;
227
+ }
228
+ /**
229
+ * Whether a tool name is covered by an allowlist.
230
+ * @param name - the tool name under review.
231
+ * @param allowlist - configured entries, patterns included.
232
+ * @returns true when at least one entry matches.
233
+ */
234
+ function isAllowedByList(name, allowlist) {
235
+ return allowlist.some((entry) => matchesAllowPattern(name, entry));
236
+ }
237
+ /**
238
+ * Resolve the origin of one tool name for display.
239
+ *
240
+ * Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
241
+ * plugin table → core catalog → unknown. Only the first two are structural; the
242
+ * rest are calibrated names, so an unlisted tool is reported as unknown rather
243
+ * than guessed into a package.
244
+ *
245
+ * @param name - the tool name.
246
+ * @returns The origin, with its repository when known.
247
+ */
248
+ function resolveToolSource(name) {
249
+ const trimmed = name.trim().replace(/\*+$/, "");
250
+ const mcp = /^mcp__([A-Za-z0-9_-]+)__/.exec(trimmed);
251
+ if (mcp !== null) {
252
+ const server = mcp[1];
253
+ const entry = SOURCE_BY_ID.get(MCP_SERVERS.get(server) ?? "");
254
+ if (entry !== void 0) return {
255
+ kind: "mcp",
256
+ label: entry.label,
257
+ repo: entry.repo,
258
+ owner: server
259
+ };
260
+ return {
261
+ kind: "mcp",
262
+ label: `MCP · ${server}`,
263
+ owner: server
264
+ };
265
+ }
266
+ const extension = EXTENSION_TOOL.exec(trimmed);
267
+ if (extension !== null && !trimmed.startsWith("mcp__")) {
268
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
269
+ const base = extension[1];
270
+ const owner = extension[2];
271
+ return {
272
+ kind: "extension",
273
+ label: `扩展 · ${base}__${owner}`,
274
+ ...entry === void 0 ? {} : { repo: entry.repo },
275
+ owner
276
+ };
277
+ }
278
+ if (trimmed.endsWith("__")) {
279
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
280
+ return {
281
+ kind: "extension",
282
+ label: `扩展 · ${trimmed.slice(0, -2)}__*`,
283
+ ...entry === void 0 ? {} : { repo: entry.repo },
284
+ owner: "*"
285
+ };
286
+ }
287
+ for (const [prefix, id] of PLUGIN_PREFIXES) {
288
+ if (!trimmed.startsWith(prefix)) continue;
289
+ const entry = SOURCE_BY_ID.get(id);
290
+ if (entry !== void 0) return {
291
+ kind: "plugin",
292
+ label: entry.label,
293
+ repo: entry.repo,
294
+ owner: id
295
+ };
296
+ }
297
+ const plugin = PLUGIN_TOOLS.get(trimmed);
298
+ if (plugin !== void 0) {
299
+ const entry = SOURCE_BY_ID.get(plugin);
300
+ if (entry !== void 0) return {
301
+ kind: "plugin",
302
+ label: entry.label,
303
+ repo: entry.repo,
304
+ owner: plugin
305
+ };
306
+ }
307
+ if (CORE_TOOLS.has(trimmed)) {
308
+ const entry = SOURCE_BY_ID.get("core");
309
+ return {
310
+ kind: "core",
311
+ label: entry?.label ?? "DSH 核心",
312
+ ...entry === void 0 ? {} : { repo: entry.repo },
313
+ owner: "core"
314
+ };
315
+ }
316
+ return {
317
+ kind: "unknown",
318
+ label: "未知来源"
319
+ };
320
+ }
321
+ //#endregion
11
322
  //#region src/client-contract.ts
12
323
  const MODEL_SWITCH_CONFIG_ID = "model-switch";
13
324
  const PROVIDERS_CONFIG_ID = "llm-providers-ui";
@@ -34,6 +345,22 @@ window.__ModuleLoader__.load({
34
345
  provider: "autoReviewProvider",
35
346
  model: "autoReviewModel"
36
347
  });
348
+ Object.freeze({
349
+ enabled: "autoReviewSkipReadOnly",
350
+ tools: "autoReviewAllowlist"
351
+ });
352
+ /**
353
+ * Derive the allowlist editor's value.
354
+ *
355
+ * An absent list means "never configured" and shows the shipped defaults; an
356
+ * explicitly emptied list stays empty, so clearing it is not undone by a reload.
357
+ */
358
+ function deriveAutoReviewAllowlist(settings) {
359
+ return {
360
+ enabled: settings.autoReviewSkipReadOnly === true,
361
+ tools: Array.isArray(settings.autoReviewAllowlist) ? [...settings.autoReviewAllowlist] : [...DEFAULT_READ_ONLY_TOOLS]
362
+ };
363
+ }
37
364
  /** Compaction summarization route for `/compact` and automatic compaction; both unset means the official default (the conversation target). */
38
365
  const COMPACTION_SETTINGS_FIELDS = Object.freeze({
39
366
  provider: "compactProvider",
@@ -86,6 +413,52 @@ window.__ModuleLoader__.load({
86
413
  function routePairConfigured(route) {
87
414
  return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
88
415
  }
416
+ /**
417
+ * Decode the reviewer counters from a `tools` RPC payload.
418
+ * @param value - the RPC result value.
419
+ * @returns The counters, or undefined when the Host does not report them.
420
+ */
421
+ function decodeReviewStatus(value) {
422
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
423
+ const raw = value.status;
424
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
425
+ const status = raw;
426
+ const count = (input) => typeof input === "number" && Number.isFinite(input) && input >= 0 ? input : 0;
427
+ return {
428
+ held: status.held === true,
429
+ allowedCalls: count(status.allowedCalls),
430
+ skippedReadOnly: count(status.skippedReadOnly)
431
+ };
432
+ }
433
+ /**
434
+ * Decode the `tools` RPC payload.
435
+ *
436
+ * Malformed entries are skipped rather than failing the page: the catalog is a
437
+ * convenience view over the Host's tools, and a partial list is still useful.
438
+ *
439
+ * @param value - the RPC result value.
440
+ * @returns The rows, or undefined when the payload is not a catalog at all.
441
+ */
442
+ function decodeToolCatalog(value) {
443
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
444
+ const listed = value.tools;
445
+ if (!Array.isArray(listed)) return void 0;
446
+ const rows = [];
447
+ for (const entry of listed) {
448
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) continue;
449
+ const raw = entry;
450
+ if (typeof raw.name !== "string" || raw.name === "") continue;
451
+ const source = raw.source !== null && typeof raw.source === "object" && !Array.isArray(raw.source) ? raw.source : {};
452
+ rows.push({
453
+ name: raw.name,
454
+ description: typeof raw.description === "string" ? raw.description : "",
455
+ sourceLabel: typeof source.label === "string" ? source.label : "未知来源",
456
+ ...typeof source.repo === "string" ? { repo: source.repo } : {},
457
+ recommended: isAllowedByList(raw.name, DEFAULT_READ_ONLY_TOOLS)
458
+ });
459
+ }
460
+ return rows;
461
+ }
89
462
  //#endregion
90
463
  //#region src/client/derived-config-form.ts
91
464
  function deriveConfigForm(source, project, fields) {
@@ -666,7 +1039,7 @@ window.__ModuleLoader__.load({
666
1039
  }
667
1040
  //#endregion
668
1041
  //#region \0dsh-css:src/client/ModelSwitchSettings.module.css.mjs
669
- const css$2 = ".djvrPG_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex;container-type:inline-size}.djvrPG_title{margin:0;font-size:16px;font-weight:500;line-height:24px}.djvrPG_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.djvrPG_saved{color:var(--dsw-alias-state-success-primary);align-items:center;gap:6px;margin:0;font-size:12px;line-height:18px;display:flex}.djvrPG_savedDot{background:currentColor;border-radius:50%;width:7px;height:7px}.djvrPG_group{flex-direction:column;gap:8px;margin-top:8px;display:flex}.djvrPG_groupLabel{color:var(--dsw-alias-label-tertiary);margin:0 2px;font-size:12px;font-weight:500;line-height:18px}.djvrPG_routeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);border-radius:10px;list-style:none;overflow:hidden}.djvrPG_routeCardUnavailable{opacity:.72}.djvrPG_routeHeader{width:100%;min-height:68px;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;justify-content:space-between;align-items:center;gap:16px;padding:12px 14px;display:flex}.djvrPG_routeHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.djvrPG_routeHeader:disabled{cursor:default}.djvrPG_routeIcon{width:18px;height:18px;color:var(--dsw-alias-label-primary);flex:none;display:block}.djvrPG_routeCopy{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.djvrPG_routeName{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:14px;font-weight:600;line-height:20px;display:flex}.djvrPG_routeSummary{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:18px;overflow:hidden}.djvrPG_badge{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.djvrPG_badgeWarn{color:var(--dsw-alias-state-warn-label)}.djvrPG_chevron{width:18px;height:18px;color:var(--dsw-alias-label-primary);flex:none;transition:transform .16s}.djvrPG_routeCardOpen .djvrPG_chevron{transform:rotate(180deg)}.djvrPG_cardBody{border-top:1px solid var(--dsw-alias-border-l2);padding:16px 14px 18px}.djvrPG_formGrid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;display:grid}.djvrPG_field{flex-direction:column;gap:6px;min-width:0;display:flex}.djvrPG_fieldFull{grid-column:1/-1}.djvrPG_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.djvrPG_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);appearance:none;background-color:var(--dsw-alias-bg-layer-1);width:100%;height:32px;color:var(--dsw-alias-label-primary);font:inherit;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 10px center;background-repeat:no-repeat;border-radius:8px;padding:0 32px 0 10px;font-size:14px;line-height:22px}.djvrPG_textInput{background-image:none;padding-right:10px}.djvrPG_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.djvrPG_input:disabled{opacity:.6;cursor:default}.djvrPG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.djvrPG_warning{color:var(--dsw-alias-state-warn-label)}.djvrPG_message{flex:1;min-width:0}.djvrPG_cardFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;margin-top:14px;padding:12px 0 0;display:flex}.djvrPG_button{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.djvrPG_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.djvrPG_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.djvrPG_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border:0}.djvrPG_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.djvrPG_button:disabled{opacity:.4;cursor:default}.djvrPG_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@container (width<=420px){.djvrPG_formGrid{grid-template-columns:1fr}.djvrPG_cardFooter .djvrPG_button{flex:1}}.djvrPG_routeHeaderRow{align-items:center;gap:8px;padding-right:14px;display:flex}.djvrPG_routeHeaderRow .djvrPG_routeHeader{flex:1;min-width:0}.djvrPG_rowCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);border-radius:10px;align-items:flex-start;gap:16px;padding:14px 16px;display:flex}.djvrPG_rowCopy{flex:1;gap:4px;min-width:0;display:grid}.djvrPG_rowTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:20px}.djvrPG_switch{background:var(--dsw-alias-bg-layer-1);width:40px;height:24px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2);cursor:pointer;border:0;border-radius:999px;flex:none;padding:0;position:relative}.djvrPG_switchOn{background:var(--dsw-alias-button-primary-fill);box-shadow:none}.djvrPG_switch i{background:var(--dsw-alias-label-primary-foreground);pointer-events:none;border-radius:50%;width:18px;height:18px;transition:left .16s;position:absolute;top:3px;left:3px}.djvrPG_switchOn i{left:19px}.djvrPG_switch:disabled{opacity:.4;cursor:default}.djvrPG_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@media (prefers-reduced-motion:reduce){.djvrPG_chevron,.djvrPG_switch i{transition:none}}";
1042
+ const css$2 = ".djvrPG_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex;container-type:inline-size}.djvrPG_title{margin:0;font-size:16px;font-weight:500;line-height:24px}.djvrPG_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.djvrPG_saved{color:var(--dsw-alias-state-success-primary);align-items:center;gap:6px;margin:0;font-size:12px;line-height:18px;display:flex}.djvrPG_savedDot{background:currentColor;border-radius:50%;width:7px;height:7px}.djvrPG_group{flex-direction:column;gap:8px;margin-top:8px;display:flex}.djvrPG_groupLabel{color:var(--dsw-alias-label-tertiary);margin:0 2px;font-size:12px;font-weight:500;line-height:18px}.djvrPG_routeCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);border-radius:10px;list-style:none;overflow:hidden}.djvrPG_routeCardUnavailable{opacity:.72}.djvrPG_routeHeader{width:100%;min-height:68px;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;justify-content:space-between;align-items:center;gap:16px;padding:12px 14px;display:flex}.djvrPG_routeHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.djvrPG_routeHeader:disabled{cursor:default}.djvrPG_routeIcon{width:18px;height:18px;color:var(--dsw-alias-label-primary);flex:none;display:block}.djvrPG_routeCopy{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.djvrPG_routeName{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:14px;font-weight:600;line-height:20px;display:flex}.djvrPG_routeSummary{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:18px;overflow:hidden}.djvrPG_badge{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.djvrPG_badgeWarn{color:var(--dsw-alias-state-warn-label)}.djvrPG_chevron{width:18px;height:18px;color:var(--dsw-alias-label-primary);flex:none;transition:transform .16s}.djvrPG_routeCardOpen .djvrPG_chevron{transform:rotate(180deg)}.djvrPG_cardBody{border-top:1px solid var(--dsw-alias-border-l2);padding:16px 14px 18px}.djvrPG_formGrid{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;display:grid}.djvrPG_field{flex-direction:column;gap:6px;min-width:0;display:flex}.djvrPG_fieldFull{grid-column:1/-1}.djvrPG_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.djvrPG_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);appearance:none;background-color:var(--dsw-alias-bg-layer-1);width:100%;height:32px;color:var(--dsw-alias-label-primary);font:inherit;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 10px center;background-repeat:no-repeat;border-radius:8px;padding:0 32px 0 10px;font-size:14px;line-height:22px}.djvrPG_textInput{background-image:none;padding-right:10px}.djvrPG_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.djvrPG_input:disabled{opacity:.6;cursor:default}.djvrPG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.djvrPG_warning{color:var(--dsw-alias-state-warn-label)}.djvrPG_message{flex:1;min-width:0}.djvrPG_cardFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;margin-top:14px;padding:12px 0 0;display:flex}.djvrPG_button{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.djvrPG_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.djvrPG_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.djvrPG_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border:0}.djvrPG_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.djvrPG_button:disabled{opacity:.4;cursor:default}.djvrPG_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@container (width<=420px){.djvrPG_formGrid{grid-template-columns:1fr}.djvrPG_cardFooter .djvrPG_button{flex:1}}.djvrPG_routeHeaderRow{align-items:center;gap:8px;padding-right:14px;display:flex}.djvrPG_routeHeaderRow .djvrPG_routeHeader{flex:1;min-width:0}.djvrPG_rowCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);border-radius:10px;align-items:flex-start;gap:16px;padding:14px 16px;display:flex}.djvrPG_rowCopy{flex:1;gap:4px;min-width:0;display:grid}.djvrPG_rowTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:20px}.djvrPG_switch{background:var(--dsw-alias-bg-layer-1);width:40px;height:24px;box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2);cursor:pointer;border:0;border-radius:999px;flex:none;padding:0;position:relative}.djvrPG_switchOn{background:var(--dsw-alias-button-primary-fill);box-shadow:none}.djvrPG_switch i{background:var(--dsw-alias-label-primary-foreground);pointer-events:none;border-radius:50%;width:18px;height:18px;transition:left .16s;position:absolute;top:3px;left:3px}.djvrPG_switchOn i{left:19px}.djvrPG_switch:disabled{opacity:.4;cursor:default}.djvrPG_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}@media (prefers-reduced-motion:reduce){.djvrPG_chevron,.djvrPG_switch i{transition:none}}.djvrPG_allowToolbar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px;display:flex}.djvrPG_allowToolbar .djvrPG_input{flex:220px;width:auto;padding-right:10px}.djvrPG_allowList{flex-direction:column;gap:6px;margin:0 0 12px;padding:0;list-style:none;display:flex}.djvrPG_allowRow{border:1px solid var(--dsw-alias-border-l2);background-color:var(--dsw-alias-bg-layer-1);border-radius:8px;align-items:center;gap:10px;padding:6px 10px;display:flex}.djvrPG_allowLabel{cursor:default;flex:auto;align-items:center;gap:8px;min-width:0;display:flex}.djvrPG_allowLabel input{width:14px;height:14px;accent-color:var(--dsw-alias-brand-primary);flex:none}.djvrPG_allowName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.djvrPG_allowSource{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;gap:4px;font-size:12px;line-height:18px;display:flex}.djvrPG_allowDot{opacity:.6}.djvrPG_allowRepo{color:var(--dsw-alias-brand-primary);text-decoration:none}.djvrPG_allowRepo:hover{text-decoration:underline}";
670
1043
  const tagId$2 = "dsh-model-switch/ModelSwitchSettings.module.css";
671
1044
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
672
1045
  const tag = document.createElement("style");
@@ -676,6 +1049,14 @@ window.__ModuleLoader__.load({
676
1049
  document.head.appendChild(tag);
677
1050
  }
678
1051
  var ModelSwitchSettings_module_css_default = {
1052
+ "allowDot": "djvrPG_allowDot",
1053
+ "allowLabel": "djvrPG_allowLabel",
1054
+ "allowList": "djvrPG_allowList",
1055
+ "allowName": "djvrPG_allowName",
1056
+ "allowRepo": "djvrPG_allowRepo",
1057
+ "allowRow": "djvrPG_allowRow",
1058
+ "allowSource": "djvrPG_allowSource",
1059
+ "allowToolbar": "djvrPG_allowToolbar",
679
1060
  "badge": "djvrPG_badge",
680
1061
  "badgeWarn": "djvrPG_badgeWarn",
681
1062
  "button": "djvrPG_button",
@@ -1114,6 +1495,10 @@ window.__ModuleLoader__.load({
1114
1495
  const [message, setMessage] = (0, react.useState)();
1115
1496
  const [groupProvider, setGroupProvider] = (0, react.useState)("");
1116
1497
  const [groupDraft, setGroupDraft] = (0, react.useState)(void 0);
1498
+ const [allowlistQuery, setAllowlistQuery] = (0, react.useState)("");
1499
+ const [catalogRows, setCatalogRows] = (0, react.useState)(void 0);
1500
+ const [catalogState, setCatalogState] = (0, react.useState)("idle");
1501
+ const [reviewStatus, setReviewStatus] = (0, react.useState)(void 0);
1117
1502
  const loadSearchCapabilities = props.loadCapabilities;
1118
1503
  const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
1119
1504
  const [searchError, setSearchError] = (0, react.useState)(void 0);
@@ -1336,6 +1721,58 @@ window.__ModuleLoader__.load({
1336
1721
  setBusy(void 0);
1337
1722
  }
1338
1723
  };
1724
+ /**
1725
+ * The Auto reviewer's read-only allowlist.
1726
+ *
1727
+ * An absent stored list means "never configured" and renders the shipped
1728
+ * defaults; an explicitly emptied list stays empty. Every write carries both
1729
+ * the switch and the entries, so the two can never drift apart on disk.
1730
+ */
1731
+ const allowlistView = switchSettings.value === void 0 ? void 0 : deriveAutoReviewAllowlist(switchSettings.value);
1732
+ const allowlistTools = allowlistView?.tools ?? DEFAULT_READ_ONLY_TOOLS;
1733
+ const allowlistEnabled = allowlistView?.enabled === true;
1734
+ const allowlistWritable = switchSettings.status === "ready" && switchSettings.writable;
1735
+ const persistAllowlist = (next) => {
1736
+ if (busy !== void 0 || !allowlistWritable) return;
1737
+ const tools = next.tools.filter((entry) => validateAllowEntry(entry) === "ok");
1738
+ run("allowlist", () => props.saveReviewAllowlist({
1739
+ enabled: next.enabled,
1740
+ tools
1741
+ }));
1742
+ };
1743
+ const setAllowTool = (name, allowed) => {
1744
+ if (allowed) {
1745
+ if (isAllowedByList(name, allowlistTools)) return;
1746
+ persistAllowlist({
1747
+ enabled: true,
1748
+ tools: [...allowlistTools, name]
1749
+ });
1750
+ return;
1751
+ }
1752
+ persistAllowlist({
1753
+ enabled: allowlistEnabled,
1754
+ tools: allowlistTools.filter((entry) => entry !== name)
1755
+ });
1756
+ };
1757
+ const scanAllowlistTools = () => {
1758
+ const scan = props.scanTools;
1759
+ if (scan === void 0 || busy !== void 0) return;
1760
+ setCatalogState("loading");
1761
+ run("allowlist", async () => {
1762
+ const scanned = await scan();
1763
+ setCatalogRows(scanned.rows);
1764
+ setReviewStatus(scanned.status);
1765
+ }).then((accepted) => {
1766
+ setCatalogState(accepted ? "ready" : "failed");
1767
+ });
1768
+ };
1769
+ const allowlistQueryText = allowlistQuery.trim().toLowerCase();
1770
+ const allowlistIncluded = allowlistTools.map((entry) => ({
1771
+ entry,
1772
+ source: resolveToolSource(entry)
1773
+ })).filter((row) => allowlistQueryText === "" || row.entry.toLowerCase().includes(allowlistQueryText));
1774
+ const allowlistAvailable = (catalogRows ?? []).filter((row) => !isAllowedByList(row.name, allowlistTools)).filter((row) => allowlistQueryText === "" || row.name.toLowerCase().includes(allowlistQueryText) || row.sourceLabel.toLowerCase().includes(allowlistQueryText));
1775
+ const allowlistSummaryText = allowlistView === void 0 ? props.t("loading") : allowlistEnabled ? props.t("reviewAllowlistOnSummary").replace("{count}", String(allowlistTools.length)) : props.t("reviewAllowlistOffSummary");
1339
1776
  const saveSubagent = () => {
1340
1777
  if (subagentDraft === void 0 || subagentDraft.mode !== "fixed") return;
1341
1778
  const matched = subagentCatalogModel;
@@ -2074,6 +2511,174 @@ window.__ModuleLoader__.load({
2074
2511
  })
2075
2512
  ]
2076
2513
  }),
2514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2515
+ className: ModelSwitchSettings_module_css_default.group,
2516
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2517
+ className: ModelSwitchSettings_module_css_default.groupLabel,
2518
+ children: props.t("reviewAllowlistGroup")
2519
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RouteCard, {
2520
+ title: props.t("reviewAllowlist"),
2521
+ summary: allowlistSummaryText,
2522
+ hint: props.t("reviewAllowlistHelp"),
2523
+ icon: "autoReview",
2524
+ open: open === "allowlist",
2525
+ onToggle: () => {
2526
+ toggle("allowlist");
2527
+ },
2528
+ ...allowlistEnabled ? { badge: props.t("reviewAllowlistBadge") } : {},
2529
+ trailing: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2530
+ checked: allowlistEnabled,
2531
+ disabled: busy !== void 0 || !allowlistWritable,
2532
+ label: props.t("reviewAllowlist"),
2533
+ onChange: (value) => {
2534
+ persistAllowlist({
2535
+ enabled: value,
2536
+ tools: allowlistTools
2537
+ });
2538
+ }
2539
+ }),
2540
+ children: [
2541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2542
+ className: ModelSwitchSettings_module_css_default.hint,
2543
+ children: props.t("reviewAllowlistNote")
2544
+ }),
2545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2546
+ className: ModelSwitchSettings_module_css_default.allowToolbar,
2547
+ children: [
2548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2549
+ className: ModelSwitchSettings_module_css_default.input,
2550
+ type: "search",
2551
+ value: allowlistQuery,
2552
+ placeholder: props.t("reviewAllowlistSearch"),
2553
+ onChange: (event) => {
2554
+ setAllowlistQuery(event.target.value);
2555
+ }
2556
+ }),
2557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2558
+ type: "button",
2559
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2560
+ disabled: busy !== void 0 || props.scanTools === void 0 || catalogState === "loading",
2561
+ onClick: scanAllowlistTools,
2562
+ children: catalogState === "loading" ? props.t("reviewAllowlistScanning") : props.t("reviewAllowlistScan")
2563
+ }),
2564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2565
+ type: "button",
2566
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2567
+ disabled: busy !== void 0 || !allowlistWritable,
2568
+ onClick: () => {
2569
+ persistAllowlist({
2570
+ enabled: allowlistEnabled,
2571
+ tools: [...DEFAULT_READ_ONLY_TOOLS]
2572
+ });
2573
+ },
2574
+ children: props.t("reviewAllowlistReset")
2575
+ })
2576
+ ]
2577
+ }),
2578
+ message?.route === "allowlist" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2579
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
2580
+ children: message.text
2581
+ }) : null,
2582
+ reviewStatus === void 0 || reviewStatus.skippedReadOnly === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2583
+ className: ModelSwitchSettings_module_css_default.hint,
2584
+ children: props.t("reviewAllowlistSkipped").replace("{count}", String(reviewStatus.skippedReadOnly))
2585
+ }),
2586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2587
+ className: ModelSwitchSettings_module_css_default.allowList,
2588
+ children: allowlistIncluded.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2589
+ className: ModelSwitchSettings_module_css_default.allowRow,
2590
+ children: [
2591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2592
+ className: ModelSwitchSettings_module_css_default.allowLabel,
2593
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2594
+ type: "checkbox",
2595
+ checked: true,
2596
+ readOnly: true
2597
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2598
+ className: ModelSwitchSettings_module_css_default.allowName,
2599
+ children: row.entry
2600
+ })]
2601
+ }),
2602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2603
+ className: ModelSwitchSettings_module_css_default.allowSource,
2604
+ children: [row.source.label, row.source.repo === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2605
+ className: ModelSwitchSettings_module_css_default.allowDot,
2606
+ children: "·"
2607
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2608
+ className: ModelSwitchSettings_module_css_default.allowRepo,
2609
+ href: row.source.repo,
2610
+ target: "_blank",
2611
+ rel: "noreferrer",
2612
+ children: props.t("reviewAllowlistRepo")
2613
+ })] })]
2614
+ }),
2615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2616
+ type: "button",
2617
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2618
+ disabled: busy !== void 0 || !allowlistWritable,
2619
+ onClick: () => {
2620
+ setAllowTool(row.entry, false);
2621
+ },
2622
+ children: props.t("reviewAllowlistRemove")
2623
+ })
2624
+ ]
2625
+ }, row.entry))
2626
+ }),
2627
+ allowlistIncluded.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2628
+ className: ModelSwitchSettings_module_css_default.hint,
2629
+ children: props.t("reviewAllowlistNone")
2630
+ }) : null,
2631
+ allowlistAvailable.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2632
+ className: ModelSwitchSettings_module_css_default.hint,
2633
+ children: props.t("reviewAllowlistAvailable")
2634
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2635
+ className: ModelSwitchSettings_module_css_default.allowList,
2636
+ children: allowlistAvailable.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2637
+ className: ModelSwitchSettings_module_css_default.allowRow,
2638
+ children: [
2639
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2640
+ className: ModelSwitchSettings_module_css_default.allowLabel,
2641
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2642
+ type: "checkbox",
2643
+ checked: false,
2644
+ readOnly: true
2645
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2646
+ className: ModelSwitchSettings_module_css_default.allowName,
2647
+ children: row.name
2648
+ })]
2649
+ }),
2650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2651
+ className: ModelSwitchSettings_module_css_default.allowSource,
2652
+ children: [row.sourceLabel, row.repo === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2653
+ className: ModelSwitchSettings_module_css_default.allowDot,
2654
+ children: "·"
2655
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2656
+ className: ModelSwitchSettings_module_css_default.allowRepo,
2657
+ href: row.repo,
2658
+ target: "_blank",
2659
+ rel: "noreferrer",
2660
+ children: props.t("reviewAllowlistRepo")
2661
+ })] })]
2662
+ }),
2663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2664
+ type: "button",
2665
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2666
+ disabled: busy !== void 0 || !allowlistWritable,
2667
+ onClick: () => {
2668
+ setAllowTool(row.name, true);
2669
+ },
2670
+ children: props.t("reviewAllowlistAdd")
2671
+ })
2672
+ ]
2673
+ }, row.name))
2674
+ })] }),
2675
+ catalogState === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2676
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
2677
+ children: props.t("reviewAllowlistScanFailed")
2678
+ }) : null
2679
+ ]
2680
+ })]
2681
+ }),
2077
2682
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2078
2683
  className: ModelSwitchSettings_module_css_default.group,
2079
2684
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -2406,6 +3011,24 @@ window.__ModuleLoader__.load({
2406
3011
  groupOverridesMore: "组覆盖:{listed} 等 {count} 个组;无组时用上方默认值",
2407
3012
  saveTimeout: "保存超时,请重试",
2408
3013
  saveRejected: "保存被拒绝,请重试",
3014
+ reviewAllowlistGroup: "审查豁免",
3015
+ reviewAllowlist: "纯读工具免审查",
3016
+ reviewAllowlistBadge: "已开启",
3017
+ reviewAllowlistOnSummary: "名单内 {count} 条免审查",
3018
+ reviewAllowlistOffSummary: "未开启:每次调用都经审查",
3019
+ reviewAllowlistHelp: "开启后,名单内的纯读工具直接执行,不再调用审查模型;名单外的写操作、命令执行与删除仍照常审查。",
3020
+ reviewAllowlistNote: "来源按工具名解析(MCP 前缀 / 插件前缀 / 扩展后缀);同名工具可以冒充来源,徽章只是提示,不是安全边界。",
3021
+ reviewAllowlistSearch: "搜索工具名或来源",
3022
+ reviewAllowlistScan: "扫描本机工具",
3023
+ reviewAllowlistScanning: "扫描中…",
3024
+ reviewAllowlistScanFailed: "扫描失败:Host 没有提供工具目录接口。",
3025
+ reviewAllowlistReset: "恢复默认名单",
3026
+ reviewAllowlistRemove: "移除",
3027
+ reviewAllowlistAdd: "加入",
3028
+ reviewAllowlistNone: "名单为空:没有任何工具免审查。",
3029
+ reviewAllowlistAvailable: "本机存在但未放行的工具:",
3030
+ reviewAllowlistRepo: "仓库",
3031
+ reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。",
2409
3032
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
2410
3033
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
2411
3034
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -2468,6 +3091,24 @@ window.__ModuleLoader__.load({
2468
3091
  groupOverridesMore: "Group overrides: {listed} and {count} groups; without a group the default above applies",
2469
3092
  saveTimeout: "The save timed out; try again",
2470
3093
  saveRejected: "The save was rejected; try again",
3094
+ reviewAllowlistGroup: "Review bypass",
3095
+ reviewAllowlist: "Skip review for pure reads",
3096
+ reviewAllowlistBadge: "On",
3097
+ reviewAllowlistOnSummary: "{count} entries skip review",
3098
+ reviewAllowlistOffSummary: "Off: every call is reviewed",
3099
+ reviewAllowlistHelp: "When on, allowlisted pure-read tools run immediately instead of calling the reviewer. Writes, command execution and deletions stay reviewed.",
3100
+ reviewAllowlistNote: "Sources are resolved from the tool name (MCP prefix, plugin prefix, extension suffix); a lookalike tool can claim any origin, so the badge is a hint, not a security boundary.",
3101
+ reviewAllowlistSearch: "Search tool name or source",
3102
+ reviewAllowlistScan: "Scan local tools",
3103
+ reviewAllowlistScanning: "Scanning…",
3104
+ reviewAllowlistScanFailed: "Scan failed: the Host exposes no tool catalog endpoint.",
3105
+ reviewAllowlistReset: "Restore defaults",
3106
+ reviewAllowlistRemove: "Remove",
3107
+ reviewAllowlistAdd: "Add",
3108
+ reviewAllowlistNone: "Empty list: no tool skips review.",
3109
+ reviewAllowlistAvailable: "Present on this machine but not allowed:",
3110
+ reviewAllowlistRepo: "repo",
3111
+ reviewAllowlistSkipped: "This Host process has skipped the reviewer {count} times.",
2471
3112
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
2472
3113
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
2473
3114
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -3982,16 +4623,92 @@ window.__ModuleLoader__.load({
3982
4623
  throw new PlanDismissalUnsupportedError("this DSH Host exposes neither PendingQuestion.dismiss() (0.2.x) nor PendingQuestion.cancel() (0.1.x)");
3983
4624
  }
3984
4625
  //#endregion
4626
+ //#region src/picker/discuss-plan.ts
4627
+ /**
4628
+ * Withdraw the review by interrupting the turn, falling back to dismissal.
4629
+ *
4630
+ * Order matters: cancellation is what keeps the work details expanded, so it is
4631
+ * tried first and only genuinely unavailable/refused cancellation falls through
4632
+ * to the released withdrawal (which still gives the human the composer back, just
4633
+ * without the expansion guarantee).
4634
+ *
4635
+ * @param cancelTurn - the session cancel seam; absent on a Host without one.
4636
+ * @param sessionId - the session whose turn is being withdrawn.
4637
+ * @param dismiss - the released `PendingQuestion` withdrawal.
4638
+ * @returns Which route withdrew the review.
4639
+ */
4640
+ async function discussPlan(cancelTurn, sessionId, dismiss) {
4641
+ if (cancelTurn !== void 0 && sessionId !== "") try {
4642
+ await cancelTurn(sessionId);
4643
+ return "interrupted";
4644
+ } catch {}
4645
+ await dismiss();
4646
+ return "dismissed";
4647
+ }
4648
+ //#endregion
3985
4649
  //#region src/picker/plan-goal.ts
4650
+ /**
4651
+ * Strip the gateway's transport prefix from a Host message.
4652
+ *
4653
+ * The goal service throws `GoalError`, and the RPC gateway relays a thrown
4654
+ * error as `code: "gateway/internal"` with the original text still in
4655
+ * `message`. Showing `gateway/internal: goal "…" already exists…` to a human
4656
+ * buries the only sentence that matters, so the prefix is removed here.
4657
+ */
4658
+ function normalizeHostMessage(message) {
4659
+ return message.replace(/^\s*gateway\/[a-z0-9-]+\s*:\s*/i, "").trim();
4660
+ }
4661
+ /**
4662
+ * Whether a refused goal call means "this session already has a goal".
4663
+ *
4664
+ * Two shapes reach here. A Host that returns the domain code says
4665
+ * `GOAL_ALREADY_EXISTS`; the gateway instead relays the thrown `GoalError` as
4666
+ * `gateway/internal` and keeps only the text, so the message itself has to be
4667
+ * recognised. The domain code is checked first and the text only decides the
4668
+ * message shape the Host is known to produce — a refusal for any other reason
4669
+ * must never clear the user's current goal.
4670
+ */
4671
+ function isAlreadyExistsError(error) {
4672
+ if (error.code === "GOAL_ALREADY_EXISTS") return true;
4673
+ return /already exists with phase/i.test(error.message);
4674
+ }
4675
+ /** Codes that describe the transport, not the goal domain. */
4676
+ function isTransportCode(code) {
4677
+ return code === "GOAL_TRANSPORT" || code.startsWith("gateway/");
4678
+ }
3986
4679
  function failure(result, fallback) {
3987
4680
  if (result.ok) return { message: fallback };
3988
4681
  const { code, message } = result.error;
3989
- return {
4682
+ const text = normalizeHostMessage(message);
4683
+ const wording = text === "" ? fallback : text;
4684
+ return isTransportCode(code) ? { message: wording } : {
3990
4685
  code,
3991
- message: message.trim() === "" ? fallback : message
4686
+ message: wording
3992
4687
  };
3993
4688
  }
3994
4689
  /**
4690
+ * Run one remote call, turning a rejected promise into a refused envelope.
4691
+ *
4692
+ * Generated remotes normally resolve `{ ok: false }`, but a transport failure
4693
+ * (or an older Host that rejects instead of answering) must not escape as an
4694
+ * unhandled rejection: the card would lose its error state and the plan would
4695
+ * stay pending with no explanation.
4696
+ */
4697
+ async function guarded(call, fallback) {
4698
+ try {
4699
+ return await call();
4700
+ } catch (error) {
4701
+ const text = normalizeHostMessage(error instanceof Error ? error.message : String(error));
4702
+ return {
4703
+ ok: false,
4704
+ error: {
4705
+ code: "GOAL_TRANSPORT",
4706
+ message: text === "" ? fallback : text
4707
+ }
4708
+ };
4709
+ }
4710
+ }
4711
+ /**
3995
4712
  * Address the plan document exactly as the official opener does.
3996
4713
  *
3997
4714
  * A logged plan (`callId` present) resolves from `dsh-resource://plan/...`
@@ -4034,26 +4751,26 @@ window.__ModuleLoader__.load({
4034
4751
  message: "goal objective must be a non-empty string"
4035
4752
  };
4036
4753
  const objective = `持续推进当前plan直到完成(plan 文档:${planDocumentAddress(sessionId, ref)})`;
4037
- const created = await remote.create(sessionId, { objective });
4754
+ const created = await guarded(() => remote.create(sessionId, { objective }), "the goal could not be created");
4038
4755
  if (created.ok) return { kind: "created" };
4039
- if (created.error.code !== "GOAL_ALREADY_EXISTS") return {
4756
+ if (!isAlreadyExistsError(created.error)) return {
4040
4757
  kind: "failed",
4041
4758
  ...failure(created, "the goal could not be created")
4042
4759
  };
4043
- const current = await remote.get(sessionId);
4760
+ const current = await guarded(() => remote.get(sessionId), "the current goal could not be read for replacement");
4044
4761
  if (!current.ok || current.value === void 0 || current.value === null) return {
4045
4762
  kind: "failed",
4046
4763
  ...failure(current, "the current goal could not be read for replacement")
4047
4764
  };
4048
- const cleared = await remote.clear(sessionId, {
4765
+ const cleared = await guarded(() => remote.clear(sessionId, {
4049
4766
  id: current.value.id,
4050
4767
  revision: current.value.revision
4051
- });
4768
+ }), "the current goal could not be cleared");
4052
4769
  if (!cleared.ok) return {
4053
4770
  kind: "failed",
4054
4771
  ...failure(cleared, "the current goal could not be cleared")
4055
4772
  };
4056
- const retry = await remote.create(sessionId, { objective });
4773
+ const retry = await guarded(() => remote.create(sessionId, { objective }), "the goal could not be created");
4057
4774
  if (retry.ok) return { kind: "replaced" };
4058
4775
  return {
4059
4776
  kind: "failed",
@@ -4224,10 +4941,11 @@ window.__ModuleLoader__.load({
4224
4941
  goalRemote: props.goalRemote,
4225
4942
  planPreview: props.planPreview,
4226
4943
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
4227
- ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
4944
+ ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf },
4945
+ ...props.cancelTurn === void 0 ? {} : { cancelTurn: props.cancelTurn }
4228
4946
  }, props.matched.key);
4229
4947
  }
4230
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf }) {
4948
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf, cancelTurn }) {
4231
4949
  const { snapshot, getDirectorySnapshot, load, select } = directory;
4232
4950
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
4233
4951
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -4448,7 +5166,9 @@ window.__ModuleLoader__.load({
4448
5166
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, { size: 14 }),
4449
5167
  disabled: busy,
4450
5168
  onClick: () => {
4451
- settle(() => respondDismiss(matched, t("plan.cancelRejected"), t("plan.dismissUnsupported")), true);
5169
+ settle(async () => {
5170
+ await discussPlan(cancelTurn, matched.sessionId, () => respondDismiss(matched, t("plan.cancelRejected"), t("plan.dismissUnsupported")));
5171
+ }, true);
4452
5172
  },
4453
5173
  children: t("plan.discuss")
4454
5174
  }),
@@ -4767,6 +5487,20 @@ window.__ModuleLoader__.load({
4767
5487
  const rpcOf = () => {
4768
5488
  return scope.get("connection", false)?.rpc;
4769
5489
  };
5490
+ /**
5491
+ * Cancel the session's running turn through the official Stop seam.
5492
+ *
5493
+ * The discuss action interrupts instead of merely withdrawing the pending
5494
+ * question: an aborted turn is kept expanded by the transcript, so the work
5495
+ * details the human was reading never fold. The conversation service is
5496
+ * addressed through the session scope exactly like the official composer's
5497
+ * Stop button does.
5498
+ */
5499
+ const cancelTurn = async (sessionId) => {
5500
+ const conversation = (sessions?.scope?.(sessionId))?.get?.("conversation");
5501
+ if (typeof conversation?.cancel !== "function") throw new Error("model-switch: the conversation service exposes no cancel() through the session scope");
5502
+ await conversation.cancel();
5503
+ };
4770
5504
  const directoryFace = (sessionId) => {
4771
5505
  const directory = models.directoryFor(sessionId);
4772
5506
  const available = sessions?.subagentAddress?.(sessionId) === void 0;
@@ -4789,6 +5523,7 @@ window.__ModuleLoader__.load({
4789
5523
  },
4790
5524
  goalRemote: () => goalRemoteFrom(scope),
4791
5525
  planPreview,
5526
+ cancelTurn,
4792
5527
  hooks: {
4793
5528
  directory: directory.store,
4794
5529
  providerOrder: orderStore
@@ -4997,6 +5732,28 @@ window.__ModuleLoader__.load({
4997
5732
  saveCapability: (route, ops) => {
4998
5733
  return (route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : route === "title" ? sessionTitle : autoReview).mutate(ops);
4999
5734
  },
5735
+ saveReviewAllowlist: async ({ enabled, tools }) => {
5736
+ if (!await owned.mutate([{
5737
+ op: "set",
5738
+ path: ["autoReviewSkipReadOnly"],
5739
+ value: enabled
5740
+ }, {
5741
+ op: "set",
5742
+ path: ["autoReviewAllowlist"],
5743
+ value: [...tools]
5744
+ }])) throw new Error("settings-rejected");
5745
+ },
5746
+ ...connectionRpc === void 0 ? {} : { scanTools: async () => {
5747
+ const response = await callPluginRpc(connectionRpc, "plugin-rpc/model-switch", "tools", {});
5748
+ if (!response.ok || response.value === void 0) throw new Error(t("reviewAllowlistScanFailed"));
5749
+ const rows = decodeToolCatalog(response.value);
5750
+ if (rows === void 0) throw new Error(t("reviewAllowlistScanFailed"));
5751
+ const status = decodeReviewStatus(response.value);
5752
+ return status === void 0 ? { rows } : {
5753
+ rows,
5754
+ status
5755
+ };
5756
+ } },
5000
5757
  saveProviderGroup: async (provider, group) => {
5001
5758
  await saveProviderGroupFenced(owned, provider, group);
5002
5759
  },