dsh-model-switch 0.4.38 → 0.4.40

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,337 @@ 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 plus the
136
+ * no-side-effect and autonomy entries the user explicitly released, calibrated
137
+ * on a live desktop profile (105 tools) at dsh 0.2.1-rc.2.
138
+ *
139
+ * Grouped by origin so a reader can trace each entry through {@link SOURCE_CATALOG}.
140
+ * Entries ending in `*` are patterns: the QQ bot extension suffixes its tools with
141
+ * the bot's own name, so `sessions_list__*` keeps working when the persona is
142
+ * renamed.
143
+ *
144
+ * Everything here follows one rule: releasing it must not let a side effect escape
145
+ * the reviewer. Calls that merely dispatch, ask, present, wait or navigate qualify
146
+ * because the work they trigger still runs under the same auto preset. Delegation
147
+ * tools (`subagent`, `subagent_fork`, `spawn_teammate`) deliberately stay out: a
148
+ * child session seeds its preset from `permissionPresets.current(parent.session)`,
149
+ * and this bypass answers `'custom'` for an allowlisted call in flight — releasing
150
+ * them would hand the child no `auto` preset at all, i.e. no review.
151
+ */
152
+ const DEFAULT_READ_ONLY_TOOLS = [
153
+ "read",
154
+ "grep",
155
+ "glob",
156
+ "read_image",
157
+ "skill",
158
+ "web_search",
159
+ "web_search_pro",
160
+ "web_fetch",
161
+ "load_workspace_dependencies",
162
+ "ask_user_question",
163
+ "present",
164
+ "sidebar_open",
165
+ "exit_plan_mode",
166
+ "job_list",
167
+ "job_output",
168
+ "terminal_list",
169
+ "terminal_read",
170
+ "list_agents",
171
+ "team_task_list",
172
+ "team_task_get",
173
+ "get_goal",
174
+ "schedule_list",
175
+ "wait_agent",
176
+ "send_message",
177
+ "team_task_create",
178
+ "list_mcp_resources",
179
+ "list_mcp_resource_templates",
180
+ "read_mcp_resource",
181
+ "session_search",
182
+ "session_event_search",
183
+ "session_event_read",
184
+ "session_event_trace",
185
+ "session_trace",
186
+ "create_goal",
187
+ "schedule_create",
188
+ "terminal_open",
189
+ "ops_read_files",
190
+ "ops_discover_tools",
191
+ "bcdp_status",
192
+ "bcdp_help",
193
+ "bcdp_doctor",
194
+ "bcdp_snapshot",
195
+ "browser_window_status",
196
+ "memory_list",
197
+ "memory_read",
198
+ "mcp__wincu__windows_computer_use_accessibility_tree",
199
+ "mcp__wincu__windows_computer_use_element_info",
200
+ "mcp__wincu__windows_computer_use_find",
201
+ "mcp__wincu__windows_computer_use_health",
202
+ "mcp__wincu__windows_computer_use_list_windows",
203
+ "mcp__wincu__windows_computer_use_ocr",
204
+ "mcp__wincu__windows_computer_use_screenshot",
205
+ "mcp__wincu__windows_computer_use_snapshot",
206
+ "mcp__wincu__windows_computer_use_tools",
207
+ "mcp__wincu__windows_computer_use_wait",
208
+ "mcp__wincu__windows_computer_use_wait_for",
209
+ "sessions_list__*",
210
+ "bing_search__*"
211
+ ];
212
+ /**
213
+ * Validate one allowlist entry.
214
+ *
215
+ * `*` is a suffix wildcard: the entry must keep at least one literal leading
216
+ * character, so a stray `*` can never turn the list into "allow everything".
217
+ *
218
+ * @param entry - the raw entry as typed in settings.
219
+ * @returns `ok`, or the specific problem.
220
+ */
221
+ function validateAllowEntry(entry) {
222
+ const trimmed = entry.trim();
223
+ if (trimmed === "") return "empty";
224
+ if (/^\*+$/.test(trimmed)) return "bare-wildcard";
225
+ if (trimmed.startsWith("*")) return "leading-wildcard";
226
+ return "ok";
227
+ }
228
+ /**
229
+ * Whether a tool name matches one allowlist entry.
230
+ *
231
+ * Exact names match exactly; an entry may end in `*`, which matches any
232
+ * remaining characters (including none). Invalid entries never match.
233
+ *
234
+ * @param name - the tool name from `exec.name`.
235
+ * @param entry - one allowlist entry.
236
+ * @returns true when the call may skip review.
237
+ */
238
+ function matchesAllowPattern(name, entry) {
239
+ if (name === "" || validateAllowEntry(entry) !== "ok") return false;
240
+ const trimmed = entry.trim();
241
+ const star = trimmed.indexOf("*");
242
+ if (star === -1) return name === trimmed;
243
+ const prefix = trimmed.slice(0, star);
244
+ const suffix = trimmed.slice(star + 1);
245
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) return false;
246
+ return name.length >= prefix.length + suffix.length;
247
+ }
248
+ /**
249
+ * Whether a tool name is covered by an allowlist.
250
+ * @param name - the tool name under review.
251
+ * @param allowlist - configured entries, patterns included.
252
+ * @returns true when at least one entry matches.
253
+ */
254
+ function isAllowedByList(name, allowlist) {
255
+ return allowlist.some((entry) => matchesAllowPattern(name, entry));
256
+ }
257
+ /**
258
+ * Resolve the origin of one tool name for display.
259
+ *
260
+ * Resolution order: MCP bridge → `<tool>__<owner>` extension → plugin prefix →
261
+ * plugin table → core catalog → unknown. Only the first two are structural; the
262
+ * rest are calibrated names, so an unlisted tool is reported as unknown rather
263
+ * than guessed into a package.
264
+ *
265
+ * @param name - the tool name.
266
+ * @returns The origin, with its repository when known.
267
+ */
268
+ function resolveToolSource(name) {
269
+ const trimmed = name.trim().replace(/\*+$/, "");
270
+ const mcp = /^mcp__([A-Za-z0-9_-]+)__/.exec(trimmed);
271
+ if (mcp !== null) {
272
+ const server = mcp[1];
273
+ const entry = SOURCE_BY_ID.get(MCP_SERVERS.get(server) ?? "");
274
+ if (entry !== void 0) return {
275
+ kind: "mcp",
276
+ label: entry.label,
277
+ repo: entry.repo,
278
+ owner: server
279
+ };
280
+ return {
281
+ kind: "mcp",
282
+ label: `MCP · ${server}`,
283
+ owner: server
284
+ };
285
+ }
286
+ const extension = EXTENSION_TOOL.exec(trimmed);
287
+ if (extension !== null && !trimmed.startsWith("mcp__")) {
288
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
289
+ const base = extension[1];
290
+ const owner = extension[2];
291
+ return {
292
+ kind: "extension",
293
+ label: `扩展 · ${base}__${owner}`,
294
+ ...entry === void 0 ? {} : { repo: entry.repo },
295
+ owner
296
+ };
297
+ }
298
+ if (trimmed.endsWith("__")) {
299
+ const entry = SOURCE_BY_ID.get("qqbot-extension");
300
+ return {
301
+ kind: "extension",
302
+ label: `扩展 · ${trimmed.slice(0, -2)}__*`,
303
+ ...entry === void 0 ? {} : { repo: entry.repo },
304
+ owner: "*"
305
+ };
306
+ }
307
+ for (const [prefix, id] of PLUGIN_PREFIXES) {
308
+ if (!trimmed.startsWith(prefix)) continue;
309
+ const entry = SOURCE_BY_ID.get(id);
310
+ if (entry !== void 0) return {
311
+ kind: "plugin",
312
+ label: entry.label,
313
+ repo: entry.repo,
314
+ owner: id
315
+ };
316
+ }
317
+ const plugin = PLUGIN_TOOLS.get(trimmed);
318
+ if (plugin !== void 0) {
319
+ const entry = SOURCE_BY_ID.get(plugin);
320
+ if (entry !== void 0) return {
321
+ kind: "plugin",
322
+ label: entry.label,
323
+ repo: entry.repo,
324
+ owner: plugin
325
+ };
326
+ }
327
+ if (CORE_TOOLS.has(trimmed)) {
328
+ const entry = SOURCE_BY_ID.get("core");
329
+ return {
330
+ kind: "core",
331
+ label: entry?.label ?? "DSH 核心",
332
+ ...entry === void 0 ? {} : { repo: entry.repo },
333
+ owner: "core"
334
+ };
335
+ }
336
+ return {
337
+ kind: "unknown",
338
+ label: "未知来源"
339
+ };
340
+ }
341
+ //#endregion
11
342
  //#region src/client-contract.ts
12
343
  const MODEL_SWITCH_CONFIG_ID = "model-switch";
13
344
  const PROVIDERS_CONFIG_ID = "llm-providers-ui";
@@ -34,6 +365,22 @@ window.__ModuleLoader__.load({
34
365
  provider: "autoReviewProvider",
35
366
  model: "autoReviewModel"
36
367
  });
368
+ Object.freeze({
369
+ enabled: "autoReviewSkipReadOnly",
370
+ tools: "autoReviewAllowlist"
371
+ });
372
+ /**
373
+ * Derive the allowlist editor's value.
374
+ *
375
+ * An absent list means "never configured" and shows the shipped defaults; an
376
+ * explicitly emptied list stays empty, so clearing it is not undone by a reload.
377
+ */
378
+ function deriveAutoReviewAllowlist(settings) {
379
+ return {
380
+ enabled: settings.autoReviewSkipReadOnly === true,
381
+ tools: Array.isArray(settings.autoReviewAllowlist) ? [...settings.autoReviewAllowlist] : [...DEFAULT_READ_ONLY_TOOLS]
382
+ };
383
+ }
37
384
  /** Compaction summarization route for `/compact` and automatic compaction; both unset means the official default (the conversation target). */
38
385
  const COMPACTION_SETTINGS_FIELDS = Object.freeze({
39
386
  provider: "compactProvider",
@@ -86,6 +433,52 @@ window.__ModuleLoader__.load({
86
433
  function routePairConfigured(route) {
87
434
  return route !== void 0 && (route.provider ?? "").trim() !== "" && (route.model ?? "").trim() !== "";
88
435
  }
436
+ /**
437
+ * Decode the reviewer counters from a `tools` RPC payload.
438
+ * @param value - the RPC result value.
439
+ * @returns The counters, or undefined when the Host does not report them.
440
+ */
441
+ function decodeReviewStatus(value) {
442
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
443
+ const raw = value.status;
444
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
445
+ const status = raw;
446
+ const count = (input) => typeof input === "number" && Number.isFinite(input) && input >= 0 ? input : 0;
447
+ return {
448
+ held: status.held === true,
449
+ allowedCalls: count(status.allowedCalls),
450
+ skippedReadOnly: count(status.skippedReadOnly)
451
+ };
452
+ }
453
+ /**
454
+ * Decode the `tools` RPC payload.
455
+ *
456
+ * Malformed entries are skipped rather than failing the page: the catalog is a
457
+ * convenience view over the Host's tools, and a partial list is still useful.
458
+ *
459
+ * @param value - the RPC result value.
460
+ * @returns The rows, or undefined when the payload is not a catalog at all.
461
+ */
462
+ function decodeToolCatalog(value) {
463
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
464
+ const listed = value.tools;
465
+ if (!Array.isArray(listed)) return void 0;
466
+ const rows = [];
467
+ for (const entry of listed) {
468
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) continue;
469
+ const raw = entry;
470
+ if (typeof raw.name !== "string" || raw.name === "") continue;
471
+ const source = raw.source !== null && typeof raw.source === "object" && !Array.isArray(raw.source) ? raw.source : {};
472
+ rows.push({
473
+ name: raw.name,
474
+ description: typeof raw.description === "string" ? raw.description : "",
475
+ sourceLabel: typeof source.label === "string" ? source.label : "未知来源",
476
+ ...typeof source.repo === "string" ? { repo: source.repo } : {},
477
+ recommended: isAllowedByList(raw.name, DEFAULT_READ_ONLY_TOOLS)
478
+ });
479
+ }
480
+ return rows;
481
+ }
89
482
  //#endregion
90
483
  //#region src/client/derived-config-form.ts
91
484
  function deriveConfigForm(source, project, fields) {
@@ -666,7 +1059,7 @@ window.__ModuleLoader__.load({
666
1059
  }
667
1060
  //#endregion
668
1061
  //#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}}";
1062
+ 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
1063
  const tagId$2 = "dsh-model-switch/ModelSwitchSettings.module.css";
671
1064
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
672
1065
  const tag = document.createElement("style");
@@ -676,6 +1069,14 @@ window.__ModuleLoader__.load({
676
1069
  document.head.appendChild(tag);
677
1070
  }
678
1071
  var ModelSwitchSettings_module_css_default = {
1072
+ "allowDot": "djvrPG_allowDot",
1073
+ "allowLabel": "djvrPG_allowLabel",
1074
+ "allowList": "djvrPG_allowList",
1075
+ "allowName": "djvrPG_allowName",
1076
+ "allowRepo": "djvrPG_allowRepo",
1077
+ "allowRow": "djvrPG_allowRow",
1078
+ "allowSource": "djvrPG_allowSource",
1079
+ "allowToolbar": "djvrPG_allowToolbar",
679
1080
  "badge": "djvrPG_badge",
680
1081
  "badgeWarn": "djvrPG_badgeWarn",
681
1082
  "button": "djvrPG_button",
@@ -1114,6 +1515,10 @@ window.__ModuleLoader__.load({
1114
1515
  const [message, setMessage] = (0, react.useState)();
1115
1516
  const [groupProvider, setGroupProvider] = (0, react.useState)("");
1116
1517
  const [groupDraft, setGroupDraft] = (0, react.useState)(void 0);
1518
+ const [allowlistQuery, setAllowlistQuery] = (0, react.useState)("");
1519
+ const [catalogRows, setCatalogRows] = (0, react.useState)(void 0);
1520
+ const [catalogState, setCatalogState] = (0, react.useState)("idle");
1521
+ const [reviewStatus, setReviewStatus] = (0, react.useState)(void 0);
1117
1522
  const loadSearchCapabilities = props.loadCapabilities;
1118
1523
  const [searchSnapshot, setSearchSnapshot] = (0, react.useState)(void 0);
1119
1524
  const [searchError, setSearchError] = (0, react.useState)(void 0);
@@ -1336,6 +1741,58 @@ window.__ModuleLoader__.load({
1336
1741
  setBusy(void 0);
1337
1742
  }
1338
1743
  };
1744
+ /**
1745
+ * The Auto reviewer's read-only allowlist.
1746
+ *
1747
+ * An absent stored list means "never configured" and renders the shipped
1748
+ * defaults; an explicitly emptied list stays empty. Every write carries both
1749
+ * the switch and the entries, so the two can never drift apart on disk.
1750
+ */
1751
+ const allowlistView = switchSettings.value === void 0 ? void 0 : deriveAutoReviewAllowlist(switchSettings.value);
1752
+ const allowlistTools = allowlistView?.tools ?? DEFAULT_READ_ONLY_TOOLS;
1753
+ const allowlistEnabled = allowlistView?.enabled === true;
1754
+ const allowlistWritable = switchSettings.status === "ready" && switchSettings.writable;
1755
+ const persistAllowlist = (next) => {
1756
+ if (busy !== void 0 || !allowlistWritable) return;
1757
+ const tools = next.tools.filter((entry) => validateAllowEntry(entry) === "ok");
1758
+ run("allowlist", () => props.saveReviewAllowlist({
1759
+ enabled: next.enabled,
1760
+ tools
1761
+ }));
1762
+ };
1763
+ const setAllowTool = (name, allowed) => {
1764
+ if (allowed) {
1765
+ if (isAllowedByList(name, allowlistTools)) return;
1766
+ persistAllowlist({
1767
+ enabled: true,
1768
+ tools: [...allowlistTools, name]
1769
+ });
1770
+ return;
1771
+ }
1772
+ persistAllowlist({
1773
+ enabled: allowlistEnabled,
1774
+ tools: allowlistTools.filter((entry) => entry !== name)
1775
+ });
1776
+ };
1777
+ const scanAllowlistTools = () => {
1778
+ const scan = props.scanTools;
1779
+ if (scan === void 0 || busy !== void 0) return;
1780
+ setCatalogState("loading");
1781
+ run("allowlist", async () => {
1782
+ const scanned = await scan();
1783
+ setCatalogRows(scanned.rows);
1784
+ setReviewStatus(scanned.status);
1785
+ }).then((accepted) => {
1786
+ setCatalogState(accepted ? "ready" : "failed");
1787
+ });
1788
+ };
1789
+ const allowlistQueryText = allowlistQuery.trim().toLowerCase();
1790
+ const allowlistIncluded = allowlistTools.map((entry) => ({
1791
+ entry,
1792
+ source: resolveToolSource(entry)
1793
+ })).filter((row) => allowlistQueryText === "" || row.entry.toLowerCase().includes(allowlistQueryText));
1794
+ const allowlistAvailable = (catalogRows ?? []).filter((row) => !isAllowedByList(row.name, allowlistTools)).filter((row) => allowlistQueryText === "" || row.name.toLowerCase().includes(allowlistQueryText) || row.sourceLabel.toLowerCase().includes(allowlistQueryText));
1795
+ const allowlistSummaryText = allowlistView === void 0 ? props.t("loading") : allowlistEnabled ? props.t("reviewAllowlistOnSummary").replace("{count}", String(allowlistTools.length)) : props.t("reviewAllowlistOffSummary");
1339
1796
  const saveSubagent = () => {
1340
1797
  if (subagentDraft === void 0 || subagentDraft.mode !== "fixed") return;
1341
1798
  const matched = subagentCatalogModel;
@@ -2074,6 +2531,174 @@ window.__ModuleLoader__.load({
2074
2531
  })
2075
2532
  ]
2076
2533
  }),
2534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2535
+ className: ModelSwitchSettings_module_css_default.group,
2536
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2537
+ className: ModelSwitchSettings_module_css_default.groupLabel,
2538
+ children: props.t("reviewAllowlistGroup")
2539
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(RouteCard, {
2540
+ title: props.t("reviewAllowlist"),
2541
+ summary: allowlistSummaryText,
2542
+ hint: props.t("reviewAllowlistHelp"),
2543
+ icon: "autoReview",
2544
+ open: open === "allowlist",
2545
+ onToggle: () => {
2546
+ toggle("allowlist");
2547
+ },
2548
+ ...allowlistEnabled ? { badge: props.t("reviewAllowlistBadge") } : {},
2549
+ trailing: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstantSwitch, {
2550
+ checked: allowlistEnabled,
2551
+ disabled: busy !== void 0 || !allowlistWritable,
2552
+ label: props.t("reviewAllowlist"),
2553
+ onChange: (value) => {
2554
+ persistAllowlist({
2555
+ enabled: value,
2556
+ tools: allowlistTools
2557
+ });
2558
+ }
2559
+ }),
2560
+ children: [
2561
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2562
+ className: ModelSwitchSettings_module_css_default.hint,
2563
+ children: props.t("reviewAllowlistNote")
2564
+ }),
2565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2566
+ className: ModelSwitchSettings_module_css_default.allowToolbar,
2567
+ children: [
2568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2569
+ className: ModelSwitchSettings_module_css_default.input,
2570
+ type: "search",
2571
+ value: allowlistQuery,
2572
+ placeholder: props.t("reviewAllowlistSearch"),
2573
+ onChange: (event) => {
2574
+ setAllowlistQuery(event.target.value);
2575
+ }
2576
+ }),
2577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2578
+ type: "button",
2579
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2580
+ disabled: busy !== void 0 || props.scanTools === void 0 || catalogState === "loading",
2581
+ onClick: scanAllowlistTools,
2582
+ children: catalogState === "loading" ? props.t("reviewAllowlistScanning") : props.t("reviewAllowlistScan")
2583
+ }),
2584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2585
+ type: "button",
2586
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2587
+ disabled: busy !== void 0 || !allowlistWritable,
2588
+ onClick: () => {
2589
+ persistAllowlist({
2590
+ enabled: allowlistEnabled,
2591
+ tools: [...DEFAULT_READ_ONLY_TOOLS]
2592
+ });
2593
+ },
2594
+ children: props.t("reviewAllowlistReset")
2595
+ })
2596
+ ]
2597
+ }),
2598
+ message?.route === "allowlist" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2599
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
2600
+ children: message.text
2601
+ }) : null,
2602
+ reviewStatus === void 0 || reviewStatus.skippedReadOnly === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2603
+ className: ModelSwitchSettings_module_css_default.hint,
2604
+ children: props.t("reviewAllowlistSkipped").replace("{count}", String(reviewStatus.skippedReadOnly))
2605
+ }),
2606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2607
+ className: ModelSwitchSettings_module_css_default.allowList,
2608
+ children: allowlistIncluded.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2609
+ className: ModelSwitchSettings_module_css_default.allowRow,
2610
+ children: [
2611
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2612
+ className: ModelSwitchSettings_module_css_default.allowLabel,
2613
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2614
+ type: "checkbox",
2615
+ checked: true,
2616
+ readOnly: true
2617
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2618
+ className: ModelSwitchSettings_module_css_default.allowName,
2619
+ children: row.entry
2620
+ })]
2621
+ }),
2622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2623
+ className: ModelSwitchSettings_module_css_default.allowSource,
2624
+ 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", {
2625
+ className: ModelSwitchSettings_module_css_default.allowDot,
2626
+ children: "·"
2627
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2628
+ className: ModelSwitchSettings_module_css_default.allowRepo,
2629
+ href: row.source.repo,
2630
+ target: "_blank",
2631
+ rel: "noreferrer",
2632
+ children: props.t("reviewAllowlistRepo")
2633
+ })] })]
2634
+ }),
2635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2636
+ type: "button",
2637
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2638
+ disabled: busy !== void 0 || !allowlistWritable,
2639
+ onClick: () => {
2640
+ setAllowTool(row.entry, false);
2641
+ },
2642
+ children: props.t("reviewAllowlistRemove")
2643
+ })
2644
+ ]
2645
+ }, row.entry))
2646
+ }),
2647
+ allowlistIncluded.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2648
+ className: ModelSwitchSettings_module_css_default.hint,
2649
+ children: props.t("reviewAllowlistNone")
2650
+ }) : null,
2651
+ allowlistAvailable.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2652
+ className: ModelSwitchSettings_module_css_default.hint,
2653
+ children: props.t("reviewAllowlistAvailable")
2654
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2655
+ className: ModelSwitchSettings_module_css_default.allowList,
2656
+ children: allowlistAvailable.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2657
+ className: ModelSwitchSettings_module_css_default.allowRow,
2658
+ children: [
2659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2660
+ className: ModelSwitchSettings_module_css_default.allowLabel,
2661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2662
+ type: "checkbox",
2663
+ checked: false,
2664
+ readOnly: true
2665
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2666
+ className: ModelSwitchSettings_module_css_default.allowName,
2667
+ children: row.name
2668
+ })]
2669
+ }),
2670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2671
+ className: ModelSwitchSettings_module_css_default.allowSource,
2672
+ 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", {
2673
+ className: ModelSwitchSettings_module_css_default.allowDot,
2674
+ children: "·"
2675
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2676
+ className: ModelSwitchSettings_module_css_default.allowRepo,
2677
+ href: row.repo,
2678
+ target: "_blank",
2679
+ rel: "noreferrer",
2680
+ children: props.t("reviewAllowlistRepo")
2681
+ })] })]
2682
+ }),
2683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2684
+ type: "button",
2685
+ className: cx(ModelSwitchSettings_module_css_default.button, ModelSwitchSettings_module_css_default.secondaryButton),
2686
+ disabled: busy !== void 0 || !allowlistWritable,
2687
+ onClick: () => {
2688
+ setAllowTool(row.name, true);
2689
+ },
2690
+ children: props.t("reviewAllowlistAdd")
2691
+ })
2692
+ ]
2693
+ }, row.name))
2694
+ })] }),
2695
+ catalogState === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2696
+ className: cx(ModelSwitchSettings_module_css_default.hint, ModelSwitchSettings_module_css_default.message),
2697
+ children: props.t("reviewAllowlistScanFailed")
2698
+ }) : null
2699
+ ]
2700
+ })]
2701
+ }),
2077
2702
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2078
2703
  className: ModelSwitchSettings_module_css_default.group,
2079
2704
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -2406,6 +3031,24 @@ window.__ModuleLoader__.load({
2406
3031
  groupOverridesMore: "组覆盖:{listed} 等 {count} 个组;无组时用上方默认值",
2407
3032
  saveTimeout: "保存超时,请重试",
2408
3033
  saveRejected: "保存被拒绝,请重试",
3034
+ reviewAllowlistGroup: "审查豁免",
3035
+ reviewAllowlist: "纯读工具免审查",
3036
+ reviewAllowlistBadge: "已开启",
3037
+ reviewAllowlistOnSummary: "名单内 {count} 条免审查",
3038
+ reviewAllowlistOffSummary: "未开启:每次调用都经审查",
3039
+ reviewAllowlistHelp: "开启后,名单内的纯读工具直接执行,不再调用审查模型;名单外的写操作、命令执行与删除仍照常审查。",
3040
+ reviewAllowlistNote: "来源按工具名解析(MCP 前缀 / 插件前缀 / 扩展后缀);同名工具可以冒充来源,徽章只是提示,不是安全边界。",
3041
+ reviewAllowlistSearch: "搜索工具名或来源",
3042
+ reviewAllowlistScan: "扫描本机工具",
3043
+ reviewAllowlistScanning: "扫描中…",
3044
+ reviewAllowlistScanFailed: "扫描失败:Host 没有提供工具目录接口。",
3045
+ reviewAllowlistReset: "恢复默认名单",
3046
+ reviewAllowlistRemove: "移除",
3047
+ reviewAllowlistAdd: "加入",
3048
+ reviewAllowlistNone: "名单为空:没有任何工具免审查。",
3049
+ reviewAllowlistAvailable: "本机存在但未放行的工具:",
3050
+ reviewAllowlistRepo: "仓库",
3051
+ reviewAllowlistSkipped: "本 Host 进程已为纯读调用跳过 {count} 次审查。",
2409
3052
  "reason.central-subagent-routing": "Alpha.4 没有全局子代理启动路由接口。",
2410
3053
  "reason.packaged-preset-roots": "Alpha.4 不支持插件提供额外 preset root。",
2411
3054
  "reason.tool-owner-suppression": "Alpha.4 没有工具所有者或来源抑制接口。",
@@ -2468,6 +3111,24 @@ window.__ModuleLoader__.load({
2468
3111
  groupOverridesMore: "Group overrides: {listed} and {count} groups; without a group the default above applies",
2469
3112
  saveTimeout: "The save timed out; try again",
2470
3113
  saveRejected: "The save was rejected; try again",
3114
+ reviewAllowlistGroup: "Review bypass",
3115
+ reviewAllowlist: "Skip review for pure reads",
3116
+ reviewAllowlistBadge: "On",
3117
+ reviewAllowlistOnSummary: "{count} entries skip review",
3118
+ reviewAllowlistOffSummary: "Off: every call is reviewed",
3119
+ reviewAllowlistHelp: "When on, allowlisted pure-read tools run immediately instead of calling the reviewer. Writes, command execution and deletions stay reviewed.",
3120
+ 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.",
3121
+ reviewAllowlistSearch: "Search tool name or source",
3122
+ reviewAllowlistScan: "Scan local tools",
3123
+ reviewAllowlistScanning: "Scanning…",
3124
+ reviewAllowlistScanFailed: "Scan failed: the Host exposes no tool catalog endpoint.",
3125
+ reviewAllowlistReset: "Restore defaults",
3126
+ reviewAllowlistRemove: "Remove",
3127
+ reviewAllowlistAdd: "Add",
3128
+ reviewAllowlistNone: "Empty list: no tool skips review.",
3129
+ reviewAllowlistAvailable: "Present on this machine but not allowed:",
3130
+ reviewAllowlistRepo: "repo",
3131
+ reviewAllowlistSkipped: "This Host process has skipped the reviewer {count} times.",
2471
3132
  "reason.central-subagent-routing": "Alpha.4 exposes no global Subagent start-routing seam.",
2472
3133
  "reason.packaged-preset-roots": "Alpha.4 exposes no plugin-owned preset root.",
2473
3134
  "reason.tool-owner-suppression": "Alpha.4 exposes no tool owner or provenance suppression seam.",
@@ -3982,6 +4643,29 @@ window.__ModuleLoader__.load({
3982
4643
  throw new PlanDismissalUnsupportedError("this DSH Host exposes neither PendingQuestion.dismiss() (0.2.x) nor PendingQuestion.cancel() (0.1.x)");
3983
4644
  }
3984
4645
  //#endregion
4646
+ //#region src/picker/discuss-plan.ts
4647
+ /**
4648
+ * Withdraw the review by interrupting the turn, falling back to dismissal.
4649
+ *
4650
+ * Order matters: cancellation is what keeps the work details expanded, so it is
4651
+ * tried first and only genuinely unavailable/refused cancellation falls through
4652
+ * to the released withdrawal (which still gives the human the composer back, just
4653
+ * without the expansion guarantee).
4654
+ *
4655
+ * @param cancelTurn - the session cancel seam; absent on a Host without one.
4656
+ * @param sessionId - the session whose turn is being withdrawn.
4657
+ * @param dismiss - the released `PendingQuestion` withdrawal.
4658
+ * @returns Which route withdrew the review.
4659
+ */
4660
+ async function discussPlan(cancelTurn, sessionId, dismiss) {
4661
+ if (cancelTurn !== void 0 && sessionId !== "") try {
4662
+ await cancelTurn(sessionId);
4663
+ return "interrupted";
4664
+ } catch {}
4665
+ await dismiss();
4666
+ return "dismissed";
4667
+ }
4668
+ //#endregion
3985
4669
  //#region src/picker/plan-goal.ts
3986
4670
  /**
3987
4671
  * Strip the gateway's transport prefix from a Host message.
@@ -4277,10 +4961,11 @@ window.__ModuleLoader__.load({
4277
4961
  goalRemote: props.goalRemote,
4278
4962
  planPreview: props.planPreview,
4279
4963
  ...props.resolveInteractionOperations === void 0 ? {} : { resolveInteractionOperations: props.resolveInteractionOperations },
4280
- ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf }
4964
+ ...props.roleOf === void 0 ? {} : { roleOf: props.roleOf },
4965
+ ...props.cancelTurn === void 0 ? {} : { cancelTurn: props.cancelTurn }
4281
4966
  }, props.matched.key);
4282
4967
  }
4283
- function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf }) {
4968
+ function PlanReviewState({ matched, review, available, providerLock, agentLocked, lockFailed, directory, t, goalRemote, planPreview, resolveInteractionOperations, roleOf, cancelTurn }) {
4284
4969
  const { snapshot, getDirectorySnapshot, load, select } = directory;
4285
4970
  const [execution, setExecution] = (0, react.useState)(snapshot.current ?? void 0);
4286
4971
  const [feedback, setFeedback] = (0, react.useState)("");
@@ -4501,7 +5186,9 @@ window.__ModuleLoader__.load({
4501
5186
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, { size: 14 }),
4502
5187
  disabled: busy,
4503
5188
  onClick: () => {
4504
- settle(() => respondDismiss(matched, t("plan.cancelRejected"), t("plan.dismissUnsupported")), true);
5189
+ settle(async () => {
5190
+ await discussPlan(cancelTurn, matched.sessionId, () => respondDismiss(matched, t("plan.cancelRejected"), t("plan.dismissUnsupported")));
5191
+ }, true);
4505
5192
  },
4506
5193
  children: t("plan.discuss")
4507
5194
  }),
@@ -4820,6 +5507,20 @@ window.__ModuleLoader__.load({
4820
5507
  const rpcOf = () => {
4821
5508
  return scope.get("connection", false)?.rpc;
4822
5509
  };
5510
+ /**
5511
+ * Cancel the session's running turn through the official Stop seam.
5512
+ *
5513
+ * The discuss action interrupts instead of merely withdrawing the pending
5514
+ * question: an aborted turn is kept expanded by the transcript, so the work
5515
+ * details the human was reading never fold. The conversation service is
5516
+ * addressed through the session scope exactly like the official composer's
5517
+ * Stop button does.
5518
+ */
5519
+ const cancelTurn = async (sessionId) => {
5520
+ const conversation = (sessions?.scope?.(sessionId))?.get?.("conversation");
5521
+ if (typeof conversation?.cancel !== "function") throw new Error("model-switch: the conversation service exposes no cancel() through the session scope");
5522
+ await conversation.cancel();
5523
+ };
4823
5524
  const directoryFace = (sessionId) => {
4824
5525
  const directory = models.directoryFor(sessionId);
4825
5526
  const available = sessions?.subagentAddress?.(sessionId) === void 0;
@@ -4842,6 +5543,7 @@ window.__ModuleLoader__.load({
4842
5543
  },
4843
5544
  goalRemote: () => goalRemoteFrom(scope),
4844
5545
  planPreview,
5546
+ cancelTurn,
4845
5547
  hooks: {
4846
5548
  directory: directory.store,
4847
5549
  providerOrder: orderStore
@@ -5050,6 +5752,28 @@ window.__ModuleLoader__.load({
5050
5752
  saveCapability: (route, ops) => {
5051
5753
  return (route === "search" ? search : route === "image" ? image : route === "compaction" ? compaction : route === "title" ? sessionTitle : autoReview).mutate(ops);
5052
5754
  },
5755
+ saveReviewAllowlist: async ({ enabled, tools }) => {
5756
+ if (!await owned.mutate([{
5757
+ op: "set",
5758
+ path: ["autoReviewSkipReadOnly"],
5759
+ value: enabled
5760
+ }, {
5761
+ op: "set",
5762
+ path: ["autoReviewAllowlist"],
5763
+ value: [...tools]
5764
+ }])) throw new Error("settings-rejected");
5765
+ },
5766
+ ...connectionRpc === void 0 ? {} : { scanTools: async () => {
5767
+ const response = await callPluginRpc(connectionRpc, "plugin-rpc/model-switch", "tools", {});
5768
+ if (!response.ok || response.value === void 0) throw new Error(t("reviewAllowlistScanFailed"));
5769
+ const rows = decodeToolCatalog(response.value);
5770
+ if (rows === void 0) throw new Error(t("reviewAllowlistScanFailed"));
5771
+ const status = decodeReviewStatus(response.value);
5772
+ return status === void 0 ? { rows } : {
5773
+ rows,
5774
+ status
5775
+ };
5776
+ } },
5053
5777
  saveProviderGroup: async (provider, group) => {
5054
5778
  await saveProviderGroupFenced(owned, provider, group);
5055
5779
  },