@klarkxy/dsh-safe-auto 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -33,57 +33,69 @@ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-
33
33
  let react_dom = require("react-dom");
34
34
  let react_jsx_runtime = require("react/jsx-runtime");
35
35
  //#region ../dsh-model-route/lib/menu.js
36
- /** Localize standard effort ids only when the provider has not supplied a display name. */
37
- function modelMenuEffortLabel(effort, locale) {
38
- if (effort.name !== effort.id) return effort.name;
39
- return {
40
- off: ["关闭", "Off"],
41
- none: ["无", "None"],
42
- minimal: ["最低", "Minimal"],
43
- low: ["低", "Low"],
44
- medium: ["中", "Medium"],
45
- high: ["高", "High"],
46
- xhigh: ["极高", "Extra high"],
47
- max: ["最高", "Maximum"],
48
- ultra: ["超高", "Ultra"]
49
- }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
50
- }
51
- /** The short model label shown inside a provider group. */
52
- function modelMenuShortName(choice) {
53
- const sep = choice.label.indexOf(" / ");
54
- return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
55
- }
56
- /** Group catalog choices by provider and filter them by a case-insensitive query. */
57
- function groupModelMenuChoices(choices, query) {
58
- const needle = query.trim().toLowerCase();
59
- const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
60
- const groups = [];
61
- for (const choice of matched) {
62
- const sep = choice.label.indexOf(" / ");
63
- const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
64
- const last = groups.at(-1);
65
- if (last && last.provider === choice.provider) last.items.push(choice);
66
- else groups.push({
67
- provider: choice.provider,
68
- name,
69
- items: [choice]
70
- });
71
- }
72
- return groups;
73
- }
74
- //#endregion
75
- //#region ../dsh-model-route/lib/index.js
76
36
  /**
77
37
  * Host model-catalog parsing shared by plugin settings UIs and model hubs.
78
38
  * Pure and browser-safe: no React, no host imports. The catalog envelope is
79
39
  * structural so hosts may add fields without breaking consumers.
80
40
  */
41
+ /** Both services are required: Cordis checks the nested namespace separately. */
42
+ const MODEL_CATALOG_INJECT = ["remote", "remote.session"];
81
43
  function asRecord(value) {
82
44
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
83
45
  }
84
46
  function asText(value) {
85
47
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
86
48
  }
49
+ /** A useful diagnostic even when a transport rejects with a plain object. */
50
+ function modelCatalogErrorMessage(value) {
51
+ if (value instanceof Error && value.message) return value.message;
52
+ return asText(asRecord(value)?.message) ?? asText(value) ?? "Unable to load the model catalog.";
53
+ }
54
+ /**
55
+ * Validate and unwrap a successful catalog without discarding provider failures
56
+ * or newer host metadata. Names and metadata remain optional for older hosts.
57
+ * A broken response is never presented as a successfully loaded empty menu.
58
+ */
59
+ function checkedModelCatalog(value) {
60
+ const envelope = asRecord(value);
61
+ if (envelope?.ok === false) throw new Error(modelCatalogErrorMessage(envelope.error ?? envelope));
62
+ const root = envelope?.ok === true ? asRecord(envelope.value) : envelope;
63
+ const malformed = (field) => {
64
+ throw new Error(`Invalid model catalog: ${field}.`);
65
+ };
66
+ if (!root || !Array.isArray(root.groups)) return malformed("groups must be an array");
67
+ for (const value of root.groups) {
68
+ const group = asRecord(value);
69
+ if (!group || !asText(group.id) || !Array.isArray(group.models)) return malformed("each group needs an id and models array");
70
+ for (const value of group.models) {
71
+ const model = asRecord(value);
72
+ if (!model || !asText(model.id)) return malformed("each model needs an id");
73
+ if (model.reasoning !== void 0) {
74
+ const reasoning = asRecord(model.reasoning);
75
+ if (!reasoning || !Array.isArray(reasoning.efforts) || reasoning.efforts.some((value) => !parseEffort(value))) return malformed("reasoning efforts must be an array of identified efforts");
76
+ }
77
+ }
78
+ }
79
+ if (root.failures !== void 0 && (!Array.isArray(root.failures) || root.failures.some((value) => {
80
+ const failure = asRecord(value);
81
+ return !failure || !asText(failure.id) || typeof failure.name !== "string" || typeof failure.message !== "string";
82
+ }))) return malformed("failures must be an array of provider failures");
83
+ return root;
84
+ }
85
+ /** Read through the injected session namespace; absence and rejection are errors. */
86
+ async function readModelCatalog(client) {
87
+ const session = client.remote?.session;
88
+ if (typeof session?.modelCatalog !== "function") throw new Error("Model catalog unavailable: inject both remote and remote.session.");
89
+ return checkedModelCatalog(await session.modelCatalog());
90
+ }
91
+ /** Provider failures are partial results, not a transport failure. */
92
+ function modelCatalogFailures(value) {
93
+ try {
94
+ return checkedModelCatalog(value).failures ?? [];
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
87
99
  function parseEffort(value) {
88
100
  const row = asRecord(value);
89
101
  const id = row ? asText(row.id) : void 0;
@@ -139,9 +151,201 @@ function modelMenuEffortOptions(choice, current) {
139
151
  });
140
152
  return listed;
141
153
  }
154
+ /** Localize standard effort ids only when the provider has not supplied a display name. */
155
+ function modelMenuEffortLabel(effort, locale) {
156
+ if (effort.name !== effort.id) return effort.name;
157
+ return {
158
+ off: ["关闭", "Off"],
159
+ none: ["无", "None"],
160
+ minimal: ["最低", "Minimal"],
161
+ low: ["低", "Low"],
162
+ medium: ["中", "Medium"],
163
+ high: ["高", "High"],
164
+ xhigh: ["极高", "Extra high"],
165
+ max: ["最高", "Maximum"],
166
+ ultra: ["超高", "Ultra"]
167
+ }[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
168
+ }
169
+ /** The short model label shown inside a provider group. */
170
+ function modelMenuShortName(choice) {
171
+ const sep = choice.label.indexOf(" / ");
172
+ return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
173
+ }
174
+ /** Group catalog choices by provider and filter them by a case-insensitive query. */
175
+ function groupModelMenuChoices(choices, query) {
176
+ const needle = query.trim().toLowerCase();
177
+ const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
178
+ const groups = [];
179
+ for (const choice of matched) {
180
+ const sep = choice.label.indexOf(" / ");
181
+ const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
182
+ const last = groups.at(-1);
183
+ if (last && last.provider === choice.provider) last.items.push(choice);
184
+ else groups.push({
185
+ provider: choice.provider,
186
+ name,
187
+ items: [choice]
188
+ });
189
+ }
190
+ return groups;
191
+ }
142
192
  //#endregion
143
193
  //#region ../dsh-model-route/lib/ui.js
144
194
  /**
195
+ * Latest refresh wins, including retries while a previous request is pending.
196
+ * Keep load stable (for example with useCallback); changing it invalidates the
197
+ * previous source and fetches again, so switching clients cannot publish stale data.
198
+ */
199
+ function useModelCatalog(load, options = {}) {
200
+ const enabled = options.enabled !== false;
201
+ const enabledRef = (0, react.useRef)(enabled);
202
+ enabledRef.current = enabled;
203
+ const live = (0, react.useRef)(false);
204
+ const source = (0, react.useRef)(load);
205
+ const revision = (0, react.useRef)(0);
206
+ const [state, setState] = (0, react.useState)(() => {
207
+ try {
208
+ return {
209
+ catalog: options.initialCatalog === void 0 ? void 0 : checkedModelCatalog(options.initialCatalog),
210
+ loading: enabled,
211
+ error: ""
212
+ };
213
+ } catch (error) {
214
+ return {
215
+ catalog: void 0,
216
+ loading: enabled,
217
+ error: modelCatalogErrorMessage(error)
218
+ };
219
+ }
220
+ });
221
+ const refresh = (0, react.useCallback)(async () => {
222
+ if (!live.current || !enabledRef.current) return;
223
+ const request = ++revision.current;
224
+ setState((previous) => ({
225
+ ...previous,
226
+ loading: true,
227
+ error: ""
228
+ }));
229
+ try {
230
+ const catalog = checkedModelCatalog(await load());
231
+ if (live.current && request === revision.current) setState({
232
+ catalog,
233
+ loading: false,
234
+ error: ""
235
+ });
236
+ } catch (error) {
237
+ if (live.current && request === revision.current) setState((previous) => ({
238
+ ...previous,
239
+ loading: false,
240
+ error: modelCatalogErrorMessage(error)
241
+ }));
242
+ }
243
+ }, [load, enabled]);
244
+ (0, react.useEffect)(() => {
245
+ live.current = true;
246
+ if (source.current !== load) {
247
+ source.current = load;
248
+ setState({
249
+ catalog: void 0,
250
+ loading: enabled,
251
+ error: ""
252
+ });
253
+ }
254
+ if (enabled) refresh();
255
+ else setState((previous) => ({
256
+ ...previous,
257
+ loading: false
258
+ }));
259
+ return () => {
260
+ live.current = false;
261
+ ++revision.current;
262
+ };
263
+ }, [enabled, refresh]);
264
+ (0, react.useEffect)(() => {
265
+ if (!enabled) return;
266
+ const reload = () => {
267
+ refresh();
268
+ };
269
+ if (typeof window !== "undefined") window.addEventListener("focus", reload);
270
+ let unsubscribe;
271
+ try {
272
+ unsubscribe = options.subscribe?.(reload);
273
+ } catch (error) {
274
+ setState((previous) => ({
275
+ ...previous,
276
+ error: modelCatalogErrorMessage(error)
277
+ }));
278
+ }
279
+ return () => {
280
+ if (typeof window !== "undefined") window.removeEventListener("focus", reload);
281
+ unsubscribe?.();
282
+ };
283
+ }, [
284
+ enabled,
285
+ refresh,
286
+ options.subscribe
287
+ ]);
288
+ const choices = (0, react.useMemo)(() => parseModelMenuChoices(state.catalog), [state.catalog]);
289
+ const failures = (0, react.useMemo)(() => state.catalog === void 0 ? [] : modelCatalogFailures(state.catalog), [state.catalog]);
290
+ return {
291
+ ...state,
292
+ choices,
293
+ failures,
294
+ refresh
295
+ };
296
+ }
297
+ /** Also accepts a mapped state from a caller that owns broader async orchestration. */
298
+ function ModelCatalogStatus({ state, locale }) {
299
+ const zh = locale?.toLowerCase().startsWith("zh") === true;
300
+ const copy = zh ? {
301
+ loading: "正在加载模型…",
302
+ error: "模型列表加载失败",
303
+ partial: "部分提供商的模型未能加载",
304
+ retry: "重试"
305
+ } : {
306
+ loading: "Loading models…",
307
+ error: "Unable to load models",
308
+ partial: "Some providers could not load their models",
309
+ retry: "Retry"
310
+ };
311
+ if (!state.loading && !state.error && state.failures.length === 0) return null;
312
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
313
+ className: "dsh-model-catalog-status",
314
+ lang: zh ? "zh" : "en",
315
+ children: [
316
+ state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
317
+ role: "status",
318
+ children: copy.loading
319
+ }) : null,
320
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
321
+ role: "alert",
322
+ children: [
323
+ copy.error,
324
+ ": ",
325
+ state.error
326
+ ]
327
+ }) : null,
328
+ state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
329
+ role: "status",
330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
331
+ failure.name || failure.id,
332
+ ": ",
333
+ failure.message
334
+ ] }, `${failure.id}:${index}`)) })]
335
+ }) : null,
336
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
337
+ type: "button",
338
+ size: "sm",
339
+ disabled: state.loading,
340
+ onClick: () => {
341
+ state.refresh();
342
+ },
343
+ children: copy.retry
344
+ }) : null
345
+ ]
346
+ });
347
+ }
348
+ /**
145
349
  * ModelMenu: the standard editor of one model-route field — the visible half
146
350
  * of the x-model-route contract. A feature plugin's settings page renders it
147
351
  * for its own marked field; the model hub renders one per detected field.
@@ -1149,6 +1353,29 @@ ${scope} {
1149
1353
  & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1150
1354
  & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1151
1355
 
1356
+ /* --- Native textarea. The primitives ship no textarea either, so a settings
1357
+ * prompt keeps the platform element and the same field shell as the select:
1358
+ * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of
1359
+ * locking to the select's 34px row. --- */
1360
+ & .dsh-ui-textarea {
1361
+ box-sizing: border-box;
1362
+ width: 100%;
1363
+ min-width: 0;
1364
+ resize: vertical;
1365
+ padding: 8px 12px;
1366
+ border: 0.5px solid var(--dsw-alias-border-l4);
1367
+ border-radius: var(--dsw-radius-md);
1368
+ background: var(--dsw-alias-bg-layer-3);
1369
+ font: inherit;
1370
+ font-size: 13px;
1371
+ line-height: 1.5;
1372
+ color: var(--dsw-alias-label-primary);
1373
+ }
1374
+ & .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
1375
+ & .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1376
+ & .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }
1377
+ & .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
1378
+
1152
1379
  /* Long panels scroll inside themselves instead of growing the host page. */
1153
1380
  & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
1154
1381
  & .dsh-ui-panel-wide { max-width: 100%; }
@@ -1187,25 +1414,18 @@ function createPluginReadGate(now = Date.now) {
1187
1414
  }
1188
1415
  //#endregion
1189
1416
  //#region ../dsh-plugin-kit/lib/contracts.js
1190
- /**
1191
- * Remote contribution for model-settings change notifications.
1192
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
1193
- * subscribers degrade silently when that package is not installed.
1194
- * The stream carries invalidations only — `ready` is never a settings snapshot.
1195
- */
1196
- const MODEL_SETTINGS_CHANGES_REMOTE = {
1197
- package: "@klarkxy/dsh-model-hub",
1198
- descriptors: [{
1199
- id: "modelSettingsChanges/watch",
1200
- service: "modelSettingsChangesController",
1201
- namespace: "modelSettingsChanges",
1202
- method: "watch",
1203
- mode: "stream",
1204
- invocation: { kind: "direct" },
1205
- parameters: [],
1206
- cancellation: { parameter: "signal" }
1207
- }]
1208
- };
1417
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
1418
+ function modelSettingsChangesIdentity(packageName) {
1419
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
1420
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
1421
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
1422
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
1423
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
1424
+ return {
1425
+ service: segment,
1426
+ namespace: segment
1427
+ };
1428
+ }
1209
1429
  //#endregion
1210
1430
  //#region ../dsh-plugin-kit/lib/model-settings-client.js
1211
1431
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -1220,17 +1440,44 @@ function parseModelSettingsChangeFrame(value) {
1220
1440
  };
1221
1441
  throw new Error("Invalid model settings change frame");
1222
1442
  }
1223
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
1224
- ...descriptor,
1225
- result: {
1226
- mode: "strict",
1227
- typeSymbol: "ModelSettings.ChangeFrame",
1228
- create: () => ({ parse: parseModelSettingsChangeFrame })
1229
- }
1230
- }));
1443
+ /** One owner's contribution. Each package mounts only its own endpoint. */
1444
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
1445
+ return {
1446
+ package: packageName,
1447
+ descriptors: [{
1448
+ id: `${identity.namespace}/watch`,
1449
+ service: identity.service,
1450
+ namespace: identity.namespace,
1451
+ method: "watch",
1452
+ mode: "stream",
1453
+ invocation: { kind: "direct" },
1454
+ parameters: [],
1455
+ cancellation: { parameter: "signal" },
1456
+ result: {
1457
+ mode: "strict",
1458
+ typeSymbol: "ModelSettings.ChangeFrame",
1459
+ create: () => ({ parse: parseModelSettingsChangeFrame })
1460
+ }
1461
+ }]
1462
+ };
1463
+ }
1464
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
1465
+ * then withdraws it, so the contribution cannot outlive the plugin. */
1466
+ function mountModelSettingsChanges(ctx, packageName) {
1467
+ const contribution = modelSettingsChangesRemote(packageName);
1468
+ ctx.inject(["remote"], (injected) => {
1469
+ const scope = injected;
1470
+ const mounting = scope.remote.$mount(contribution);
1471
+ mounting.catch(() => {});
1472
+ scope.effect(() => async () => {
1473
+ await (await mounting.catch(() => void 0))?.();
1474
+ });
1475
+ });
1476
+ }
1231
1477
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1232
1478
  * A factory shares one logical stream while at least one subscriber remains. */
1233
- function createModelSettingsSubscriber(ctx, namespace) {
1479
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
1480
+ const endpoint = identity;
1234
1481
  const listeners = /* @__PURE__ */ new Set();
1235
1482
  let injection;
1236
1483
  let current;
@@ -1256,12 +1503,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
1256
1503
  };
1257
1504
  const start = () => {
1258
1505
  const life = ++lifecycle;
1259
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1506
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
1260
1507
  if (life !== lifecycle || !listeners.size) return;
1261
1508
  const scope = injected;
1509
+ const service = scope.remote[endpoint.namespace];
1510
+ if (!service || !("watch" in service) || !service.watch) {
1511
+ report("Model settings sync service unavailable");
1512
+ return;
1513
+ }
1514
+ const watch = service.watch;
1262
1515
  const stream = scope.remote.$stream({
1263
1516
  name: "Model settings sync",
1264
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1517
+ open: (signal) => watch(signal),
1265
1518
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1266
1519
  });
1267
1520
  const session = {
@@ -1296,7 +1549,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
1296
1549
  invalidate();
1297
1550
  } else {
1298
1551
  if (!ready) throw new Error("Model settings sync change arrived before ready");
1299
- if (namespace === "*" || frame.ns === namespace) invalidate();
1552
+ if (accept(frame.ns)) invalidate();
1300
1553
  }
1301
1554
  }
1302
1555
  } catch (error) {
@@ -1348,8 +1601,7 @@ const name = "dsh-safe-auto-client";
1348
1601
  const inject = [
1349
1602
  "slots",
1350
1603
  "connection",
1351
- "remote",
1352
- "remote.session",
1604
+ ...MODEL_CATALOG_INJECT,
1353
1605
  "locale"
1354
1606
  ];
1355
1607
  const CHANNEL = "/dsh-safe-auto";
@@ -1357,11 +1609,7 @@ const BUNDLE = "@klarkxy/dsh-safe-auto";
1357
1609
  const TEXT = {
1358
1610
  zh: {
1359
1611
  requestFailed: "请求失败",
1360
- confirmationUnavailable: "确认已不可用",
1361
- approvalInvalid: "确认请求无效",
1362
- approvalExpiredError: "确认已超时或不再等待处理",
1363
1612
  invalidSession: "会话无效",
1364
- sessionInactive: "会话未激活,请打开后重试",
1365
1613
  childSession: "子会话不能启用安全自动",
1366
1614
  invalidRequest: "请求无效",
1367
1615
  settingsChanged: "配置已更新,请刷新后重试",
@@ -1371,7 +1619,6 @@ const TEXT = {
1371
1619
  workspaceUnavailable: "当前无法使用需原生审批的工作区写入权限",
1372
1620
  unknownPreset: "权限预设无效",
1373
1621
  controlUnloaded: "安全自动已停用",
1374
- browserRequired: "请在浏览器中确认这次调用",
1375
1622
  unknownSetting: "审核模型设置无效",
1376
1623
  invalidModel: "审核模型或提供方标识无效",
1377
1624
  modelPair: "提供方和模型必须一起配置",
@@ -1408,37 +1655,22 @@ const TEXT = {
1408
1655
  tabReviewer: "审核配置",
1409
1656
  details: "安全说明",
1410
1657
  tabLimits: "预算边界",
1411
- safetyDetail: "只处理原生 write/edit/bash/pwsh 的单次沙箱提权。审核会保留真人任务授权及后续限制,并按需读取工作区内的包配置和脚本;模型通过时自动放行。其余有效请求请你在 60 秒内确认,超时拒绝。确认只适用于这一次调用。",
1412
- unableLabel: "无法自动判断时:",
1413
- unableValue: "请你在 60 秒内确认;超时拒绝",
1414
- approvalTitle: "确认这一次调用",
1415
- approvalPending: (count) => `${count} 个请求待确认`,
1416
- approvalCountdown: (seconds) => `剩余 ${seconds} 秒 · 超时拒绝`,
1417
- approvalExpired: "确认已超时,已拒绝。",
1418
- approvalAllow: "仅允许这一次",
1419
- approvalDeny: "拒绝",
1420
- approvalReview: "模型审核意见",
1421
- approvalRefresh: (message) => `${message}。正在刷新请求;不会自动重试决定。`,
1422
- approvalEffect: "完整调用内容",
1658
+ safetyDetail: "普通调用照常在工作区沙箱内运行,插件不拦截。需要审批的请求(沙箱提权、少量危险命令,以及其他工具的审批)交给审核模型:通过就只放行这一次,拒绝或审核失败就把原因告诉智能体。",
1659
+ reviewInput: "发给审核模型的内容:对话记录(你的消息标为可信,其余只作参考,超长时截断)、工作目录和这次操作的完整参数。",
1660
+ breakerDetail: "同一轮对话里连续拒绝 3 次,或最近 50 次审核拒绝 10 次,本轮暂停自动审批,等你发新消息。",
1423
1661
  prompt: "额外审核提示词",
1424
1662
  promptHint: (count) => `${count}/4096 · 只可补充审核约束,不可扩大授权或覆盖安全规则。`,
1425
- limitsLine: (l) => `单次审查超时 ${l.timeoutMs} ms · 输入上限 ${l.maxInputBytes} bytes · 输出上限 ${l.outputTokens} tokens`,
1426
- limitsFuses: (l) => `每个任务最多审查 ${l.maxReviewsPerTask} 次 · 连续拒绝 ${l.consecutiveDenials} 次后熔断`,
1427
- limitsHelp: "预算在 profile 中配置,保存设置或重新启用不会补充;动作、真人指令和有界的本地脚本证据会发送给审核模型。",
1663
+ limitsLine: (l) => `单次审核超时 ${Math.round(l.timeoutMs / 1e3)} 秒 · 输出上限 ${l.outputTokens} tokens · 对话记录上限 ${l.maxTranscriptBytes} bytes`,
1664
+ limitsHelp: "这些上限在 profile 中配置。",
1428
1665
  save: "保存",
1429
1666
  refreshDiscard: "刷新(丢弃草稿)",
1430
1667
  saveFailed: (message) => `${message}。草稿已保留,刷新后重新编辑,不会自动重试。`,
1431
- catalogFailed: (message) => `模型目录读取失败,已保存值保持不变:${message}`,
1432
1668
  saved: "已保存",
1433
- summary: "安全自动:沙箱提权时由独立审核模型把关,通过才自动放行。"
1669
+ summary: "安全自动:需要审批的请求交给审核模型判断,通过才放行这一次。"
1434
1670
  },
1435
1671
  en: {
1436
1672
  requestFailed: "Request failed",
1437
- confirmationUnavailable: "Confirmation is no longer available",
1438
- approvalInvalid: "Invalid approval request",
1439
- approvalExpiredError: "Approval expired or is no longer pending",
1440
1673
  invalidSession: "Invalid session",
1441
- sessionInactive: "Session is not active; open it and retry",
1442
1674
  childSession: "Child sessions cannot select Safe Auto",
1443
1675
  invalidRequest: "Invalid request",
1444
1676
  settingsChanged: "Settings changed; reload and retry",
@@ -1448,7 +1680,6 @@ const TEXT = {
1448
1680
  workspaceUnavailable: "Workspace Write with native approval is unavailable",
1449
1681
  unknownPreset: "Unknown permission preset",
1450
1682
  controlUnloaded: "Safe Auto control unloaded",
1451
- browserRequired: "Browser confirmation required",
1452
1683
  unknownSetting: "Invalid reviewer model setting",
1453
1684
  invalidModel: "Invalid reviewer provider/model identifier",
1454
1685
  modelPair: "Provider and model must be configured together",
@@ -1485,29 +1716,18 @@ const TEXT = {
1485
1716
  tabReviewer: "Reviewer",
1486
1717
  details: "Safety notes",
1487
1718
  tabLimits: "Limits",
1488
- safetyDetail: "Reviews one native write/edit/bash/pwsh sandbox escalation at a time. Earlier human task authorization and later restrictions remain visible; workspace package definitions and scripts may supply evidence. A model pass auto-approves. Other valid requests ask you to confirm within 60 seconds and are denied on timeout. Confirmation grants this call only.",
1489
- unableLabel: "When it cannot judge automatically: ",
1490
- unableValue: "Ask you to confirm within 60 seconds; deny on timeout",
1491
- approvalTitle: "Confirm this call",
1492
- approvalPending: (count) => `${count} pending confirmation${count === 1 ? "" : "s"}`,
1493
- approvalCountdown: (seconds) => `${seconds} seconds left · Deny on timeout`,
1494
- approvalExpired: "Confirmation expired and was denied.",
1495
- approvalAllow: "Allow this call only",
1496
- approvalDeny: "Reject",
1497
- approvalReview: "Model review opinion",
1498
- approvalRefresh: (message) => `${message}. Refreshing requests; your decision will not be retried automatically.`,
1499
- approvalEffect: "Complete call content",
1719
+ safetyDetail: "Ordinary calls run inside the workspace sandbox as usual; the plugin does not intercept them. Requests that need approval (sandbox escalations, a short list of dangerous commands, and approval prompts from other tools) go to the reviewer model: a pass approves that one call; a denial or review failure is explained to the agent.",
1720
+ reviewInput: "Sent to the reviewer: the conversation (your messages marked trusted, everything else as evidence, truncated when long), the working directory, and the complete arguments of the action.",
1721
+ breakerDetail: "After 3 consecutive denials, or 10 denials among the last 50 reviews in one turn, automatic approval pauses until your next message.",
1500
1722
  prompt: "Additional reviewer prompt",
1501
1723
  promptHint: (count) => `${count}/4096 · May add review constraints, never expand authorization or override safety rules.`,
1502
- limitsLine: (l) => `Review timeout ${l.timeoutMs} ms · Input cap ${l.maxInputBytes} bytes · Output cap ${l.outputTokens} tokens`,
1503
- limitsFuses: (l) => `At most ${l.maxReviewsPerTask} reviews per task · Breaker opens after ${l.consecutiveDenials} consecutive denials`,
1504
- limitsHelp: "Budgets live in the profile and are not refilled by saving or re-enabling. Actions, human instructions and bounded local script evidence are sent to the reviewer model.",
1724
+ limitsLine: (l) => `Review timeout ${Math.round(l.timeoutMs / 1e3)} s · Output cap ${l.outputTokens} tokens · Transcript cap ${l.maxTranscriptBytes} bytes`,
1725
+ limitsHelp: "These limits are configured in the profile.",
1505
1726
  save: "Save",
1506
1727
  refreshDiscard: "Refresh (discard draft)",
1507
1728
  saveFailed: (message) => `${message}. Draft retained; refresh before editing again. No automatic retry.`,
1508
- catalogFailed: (message) => `Model catalog failed; saved values retained: ${message}`,
1509
1729
  saved: "Saved",
1510
- summary: "Safe Auto: an independent reviewer vets each sandbox escalation and only a pass auto-approves."
1730
+ summary: "Safe Auto: a reviewer model answers approval requests and approves only the calls that pass."
1511
1731
  }
1512
1732
  };
1513
1733
  /** The host locale may arrive as the injected service or as a plain tag. */
@@ -1529,13 +1749,7 @@ function useText(locale) {
1529
1749
  const LOCAL_ERRORS = {
1530
1750
  "Request failed": "requestFailed",
1531
1751
  "Request rejected": "requestFailed",
1532
- "Confirmation is no longer available": "confirmationUnavailable",
1533
- "Invalid approval response": "approvalInvalid",
1534
- "Invalid approval payload": "approvalInvalid",
1535
- "Approval expired or is no longer pending": "approvalExpiredError",
1536
- "Approval expired or invalidated": "approvalExpiredError",
1537
1752
  "Invalid session": "invalidSession",
1538
- "Session is not active; open it and retry": "sessionInactive",
1539
1753
  "Child sessions cannot select Safe Auto": "childSession",
1540
1754
  "Invalid request": "invalidRequest",
1541
1755
  "Settings changed; reload and retry": "settingsChanged",
@@ -1546,7 +1760,6 @@ const LOCAL_ERRORS = {
1546
1760
  "Workspace Write with native approval is unavailable": "workspaceUnavailable",
1547
1761
  "Unknown permission preset": "unknownPreset",
1548
1762
  "Safe Auto control unloaded": "controlUnloaded",
1549
- "Browser confirmation required": "browserRequired",
1550
1763
  "Unknown Safe Auto model setting": "unknownSetting",
1551
1764
  "invalid reviewer provider/model identifier": "invalidModel",
1552
1765
  "provider and model must be configured together": "modelPair",
@@ -1555,11 +1768,16 @@ const LOCAL_ERRORS = {
1555
1768
  function errorMessage(error) {
1556
1769
  return (typeof error === "string" ? error : error?.message) || TEXT.en.requestFailed;
1557
1770
  }
1771
+ const INACTIVE_SESSION = "Session is not active; open it and retry";
1772
+ function shownPermissionError(error) {
1773
+ const message = errorMessage(error);
1774
+ return message === INACTIVE_SESSION ? "" : message;
1775
+ }
1558
1776
  function errorText(t, error) {
1559
1777
  const message = typeof error === "string" ? error : error?.message;
1560
1778
  const key = Object.hasOwn(LOCAL_ERRORS, message) ? LOCAL_ERRORS[message] : void 0;
1561
1779
  const unknownField = /^unknown config field: (.+)$/.exec(message || "");
1562
- const invalidField = /^(?:(enabled|staticReadonly) must be a boolean|(provider|model|reasoningEffort|reviewerPrompt) must be a string|invalid (timeoutMs|maxInputBytes|outputTokens|maxReviewsPerTask|consecutiveDenials|humanApprovalTimeoutMs))$/.exec(message || "");
1780
+ const invalidField = /^(?:(enabled) must be a boolean|(provider|model|reasoningEffort|reviewerPrompt) must be a string|invalid (timeoutMs|outputTokens|maxTranscriptBytes))$/.exec(message || "");
1563
1781
  const text = key ? t(key) : unknownField ? t("unknownConfigField", unknownField[1]) : invalidField ? t("invalidConfigField", invalidField[1] || invalidField[2] || invalidField[3]) : ["config must be an object", "invalid configuration"].includes(message) ? t("invalidConfig") : message || t("requestFailed");
1564
1782
  return error?.context ? t(error.context, text) : text;
1565
1783
  }
@@ -1603,120 +1821,6 @@ function createRequestScope() {
1603
1821
  }
1604
1822
  };
1605
1823
  }
1606
- const monotonicNow = () => globalThis.performance?.now?.() ?? Date.now();
1607
- function approvalRemaining(request, now = monotonicNow()) {
1608
- return Math.max(0, request.deadline - now);
1609
- }
1610
- /** Per-session serial reads and one-shot writes. A response's server remainder
1611
- * is anchored before the round trip so network delay cannot extend the prompt. */
1612
- function createApprovalController({ api, sessionId, onState, now = monotonicNow, schedule = setTimeout, cancel = clearTimeout, intervalMs = 1e3, readGate = createPluginReadGate() }) {
1613
- let active = true, reading = false, writing = false, generation = 0, readSequence = 0, timer;
1614
- let state = {
1615
- requests: [],
1616
- busy: false,
1617
- error: ""
1618
- };
1619
- const publish = (next) => {
1620
- state = {
1621
- ...state,
1622
- ...next
1623
- };
1624
- if (active) onState(state);
1625
- };
1626
- const arm = () => {
1627
- if (active) {
1628
- cancel(timer);
1629
- timer = schedule(() => {
1630
- refresh();
1631
- }, intervalMs);
1632
- }
1633
- };
1634
- async function refresh(manual = false) {
1635
- if (!active || reading || writing) return false;
1636
- if (manual) {
1637
- generation++;
1638
- readGate.reset();
1639
- }
1640
- if (!readGate.canRead()) {
1641
- arm();
1642
- return false;
1643
- }
1644
- cancel(timer);
1645
- reading = true;
1646
- const ticket = generation, readId = ++readSequence, started = now();
1647
- try {
1648
- const value = await readGate.run(() => api.getApprovals(sessionId));
1649
- if (active && ticket === generation) {
1650
- const previous = new Map(state.requests.map((request) => [request.id, request.deadline]));
1651
- const requests = (value?.requests || []).map((request) => ({
1652
- ...request,
1653
- deadline: Math.min(previous.get(request.id) ?? Infinity, started + Math.max(0, Number(request.remainingMs) || 0))
1654
- })).filter((request) => approvalRemaining(request, now()) > 0);
1655
- publish({
1656
- requests,
1657
- error: ""
1658
- });
1659
- }
1660
- } catch (error) {
1661
- if (active && ticket === generation) publish({ error: errorMessage(error) });
1662
- } finally {
1663
- if (readId === readSequence) {
1664
- reading = false;
1665
- arm();
1666
- }
1667
- }
1668
- return true;
1669
- }
1670
- return {
1671
- refresh,
1672
- reconnect() {
1673
- generation++;
1674
- readSequence++;
1675
- readGate.reset();
1676
- reading = false;
1677
- if (!writing) refresh();
1678
- },
1679
- dispose() {
1680
- active = false;
1681
- generation++;
1682
- readGate.reset();
1683
- cancel(timer);
1684
- },
1685
- async answer(requestId, outcome) {
1686
- const request = state.requests.find((item) => item.id === requestId);
1687
- if (!active || writing || !request || approvalRemaining(request, now()) <= 0) return false;
1688
- writing = true;
1689
- generation++;
1690
- cancel(timer);
1691
- publish({
1692
- busy: true,
1693
- error: ""
1694
- });
1695
- readGate.reset();
1696
- try {
1697
- if ((await api.answerApproval({
1698
- sessionId,
1699
- requestId,
1700
- outcome
1701
- }))?.accepted !== true) throw new Error("Confirmation is no longer available");
1702
- if (active) publish({ requests: state.requests.filter((item) => item.id !== requestId) });
1703
- } catch (error) {
1704
- if (active) publish({
1705
- requests: state.requests.filter((item) => item.id !== requestId),
1706
- error: errorMessage(error)
1707
- });
1708
- } finally {
1709
- writing = false;
1710
- if (active) {
1711
- publish({ busy: false });
1712
- if (!reading) refresh();
1713
- else arm();
1714
- }
1715
- }
1716
- return true;
1717
- }
1718
- };
1719
- }
1720
1824
  function reviewerMenuState(values, catalog) {
1721
1825
  const provider = values.provider || "", model = values.model || "";
1722
1826
  const follow = !provider && !model;
@@ -1765,52 +1869,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1765
1869
  /* A fieldset is the settings group and takes the contract's card surface; its
1766
1870
  * legend keeps the UA inline padding from indenting the group title. */
1767
1871
  .dsh-safe-auto legend { padding: 0; }
1768
- /* The primitives ship no textarea, so it keeps the platform element at the
1769
- * official field geometry, exactly as dsh-ui-select does for a native select. */
1770
- .dsh-safe-auto-input {
1771
- box-sizing: border-box;
1772
- width: 100%;
1773
- min-width: 0;
1774
- padding: 8px 12px;
1775
- border: 0.5px solid var(--dsw-alias-border-l4);
1776
- border-radius: var(--dsw-radius-md);
1777
- background: var(--dsw-alias-bg-layer-3);
1778
- font: inherit;
1779
- font-size: 13px;
1780
- line-height: 20px;
1781
- color: var(--dsw-alias-label-primary);
1782
- resize: vertical;
1783
- }
1784
- .dsh-safe-auto-input:disabled { opacity: 0.4; cursor: not-allowed; }
1785
- .dsh-safe-auto-approval-dialog {
1786
- display: flex;
1787
- flex-direction: column;
1788
- max-height: 100%;
1789
- max-width: 100%;
1790
- }
1791
- /* The host card sizes every modal to a 380px column; an approval shows whole
1792
- * commands and file contents, so it claims a wider one. The doubled class
1793
- * keeps this rule ahead of the card's own width regardless of style order. */
1794
- .dsh-safe-auto.dsh-safe-auto-approval-dialog { width: min(640px, 100%); }
1795
- .dsh-safe-auto-approval-content {
1796
- flex: 1 1 auto;
1797
- min-height: 0;
1798
- min-width: 0;
1799
- overflow: auto;
1800
- }
1801
- .dsh-safe-auto-approval-pre {
1802
- max-height: 35vh;
1803
- overflow: auto;
1804
- white-space: pre-wrap;
1805
- overflow-wrap: anywhere;
1806
- margin: 0;
1807
- padding: 8px 12px;
1808
- border: 0.5px solid var(--dsw-alias-border-l4);
1809
- border-radius: var(--dsw-radius-md);
1810
- background: var(--dsw-alias-bg-layer-3);
1811
- color: var(--dsw-alias-label-primary);
1812
- font-size: 13px;
1813
- }
1814
1872
  `;
1815
1873
  function Style() {
1816
1874
  return h("style", null, css);
@@ -1821,47 +1879,6 @@ function ErrorText({ error, t }) {
1821
1879
  className: "dsh-ui-error dsh-ui-wrap"
1822
1880
  }, errorText(t, error)) : null;
1823
1881
  }
1824
- /** Values are rendered as plain text, with no Markdown or HTML interpretation. */
1825
- function ApprovalDialog({ request, pendingCount = 1, remainingMs, busy = false, error = "", onAnswer, locale }) {
1826
- const t = useText(locale);
1827
- if (!request) return null;
1828
- const expired = remainingMs <= 0;
1829
- const action = request.action || {};
1830
- const fields = Object.entries({
1831
- tool: action.tool,
1832
- cwd: action.cwd,
1833
- ...action.arguments
1834
- });
1835
- return h(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1836
- open: true,
1837
- title: t("approvalTitle"),
1838
- closeLabel: t("approvalDeny"),
1839
- onClose: () => {
1840
- if (!busy && !expired) onAnswer(request.id, "deny");
1841
- },
1842
- className: "dsh-safe-auto dsh-safe-auto-approval-dialog",
1843
- contentClassName: "dsh-safe-auto-approval-content",
1844
- footer: h("div", { className: "dsh-safe-auto dsh-ui-actions" }, h(Style), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1845
- variant: "ghost",
1846
- disabled: busy || expired,
1847
- "data-modal-autofocus": true,
1848
- onClick: () => onAnswer(request.id, "deny")
1849
- }, t("approvalDeny")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1850
- variant: "primary",
1851
- disabled: busy || expired,
1852
- onClick: () => onAnswer(request.id, "allow")
1853
- }, t("approvalAllow")))
1854
- }, h("div", { className: "dsh-safe-auto dsh-ui-stack" }, h(Style), h("p", {
1855
- className: "dsh-ui-meta",
1856
- role: "status"
1857
- }, t("approvalPending", pendingCount), " · ", expired ? t("approvalExpired") : t("approvalCountdown", Math.ceil(remainingMs / 1e3))), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("approvalEffect")), fields.map(([field, value]) => h("div", {
1858
- key: field,
1859
- className: "dsh-ui-field"
1860
- }, h("span", { className: "dsh-ui-meta" }, field), h("pre", { className: "dsh-safe-auto-approval-pre" }, typeof value === "string" ? value : JSON.stringify(value, null, 2))))), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("approvalReview")), h("pre", { className: "dsh-safe-auto-approval-pre" }, JSON.stringify(request.review || {}, null, 2))), h(ErrorText, {
1861
- error,
1862
- t
1863
- })));
1864
- }
1865
1882
  /** Composer permission control via the replacement slot: native presets plus Safe Auto. */
1866
1883
  function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null }) {
1867
1884
  const t = useText(locale);
@@ -1871,20 +1888,15 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1871
1888
  const [busy, setBusy] = (0, react.useState)(false);
1872
1889
  const scopeRef = (0, react.useRef)(null);
1873
1890
  const refreshRef = (0, react.useRef)(null);
1874
- const approvalsRef = (0, react.useRef)(null);
1875
- const [approvals, setApprovals] = (0, react.useState)({
1876
- sessionId,
1877
- requests: [],
1878
- busy: false,
1879
- error: ""
1880
- });
1881
- const [approvalNow, setApprovalNow] = (0, react.useState)(monotonicNow);
1882
1891
  const refresh = (automatic = false) => scopeRef.current?.run(() => api.getSession(sessionId), {
1883
1892
  value: (value) => {
1884
1893
  setSnapshot(value);
1885
1894
  setError("");
1886
1895
  },
1887
- error: (e) => setError(errorMessage(e))
1896
+ error: (e) => {
1897
+ const message = shownPermissionError(e);
1898
+ if (message) setError(message);
1899
+ }
1888
1900
  }, false, automatic);
1889
1901
  refreshRef.current = refresh;
1890
1902
  (0, react.useEffect)(() => {
@@ -1905,50 +1917,6 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1905
1917
  globalThis.removeEventListener?.("focus", focus);
1906
1918
  };
1907
1919
  }, [api, sessionId]);
1908
- const approvalsEnabled = Boolean(snapshot?.safeAuto);
1909
- (0, react.useEffect)(() => {
1910
- setApprovals({
1911
- sessionId,
1912
- requests: [],
1913
- busy: false,
1914
- error: ""
1915
- });
1916
- if (!approvalsEnabled) return void 0;
1917
- const controller = createApprovalController({
1918
- api,
1919
- sessionId,
1920
- onState: (value) => {
1921
- setApprovalNow(monotonicNow());
1922
- setApprovals({
1923
- ...value,
1924
- sessionId
1925
- });
1926
- }
1927
- });
1928
- approvalsRef.current = controller;
1929
- controller.refresh();
1930
- const stopConnection = api.subscribeGeneration?.(() => controller.reconnect());
1931
- const focus = () => {
1932
- controller.refresh();
1933
- };
1934
- globalThis.addEventListener?.("focus", focus);
1935
- return () => {
1936
- controller.dispose();
1937
- stopConnection?.();
1938
- approvalsRef.current = null;
1939
- globalThis.removeEventListener?.("focus", focus);
1940
- };
1941
- }, [
1942
- api,
1943
- sessionId,
1944
- approvalsEnabled
1945
- ]);
1946
- const pending = approvalsEnabled && approvals.sessionId === sessionId ? approvals.requests.filter((request) => approvalRemaining(request, approvalNow) > 0) : [];
1947
- (0, react.useEffect)(() => {
1948
- if (!pending.length) return void 0;
1949
- const timer = setInterval(() => setApprovalNow(monotonicNow()), 250);
1950
- return () => clearInterval(timer);
1951
- }, [pending.length]);
1952
1920
  const choose = (value) => {
1953
1921
  const scope = scopeRef.current;
1954
1922
  if (!snapshot || scope?.writing) return;
@@ -1969,8 +1937,9 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1969
1937
  setOpen(false);
1970
1938
  },
1971
1939
  error: (e) => {
1972
- setError({
1973
- message: errorMessage(e),
1940
+ const message = shownPermissionError(e);
1941
+ if (message) setError({
1942
+ message,
1974
1943
  context: "refreshRetry"
1975
1944
  });
1976
1945
  refreshRef.current();
@@ -2021,23 +1990,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
2021
1990
  }), h(ErrorText, {
2022
1991
  error,
2023
1992
  t
2024
- }), h(ErrorText, {
2025
- error: approvals.error && {
2026
- message: approvals.error,
2027
- context: "approvalRefresh"
2028
- },
2029
- t
2030
- }), pending.length ? h(ApprovalDialog, {
2031
- key: pending[0].id,
2032
- request: pending[0],
2033
- pendingCount: pending.length,
2034
- remainingMs: approvalRemaining(pending[0], approvalNow),
2035
- busy: approvals.busy,
2036
- onAnswer: (id, outcome) => {
2037
- approvalsRef.current?.answer(id, outcome);
2038
- },
2039
- locale
2040
- }) : null);
1993
+ }));
2041
1994
  }
2042
1995
  function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
2043
1996
  const t = useText(locale);
@@ -2098,9 +2051,12 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2098
2051
  const t = useText(locale);
2099
2052
  const [settings, setSettings] = (0, react.useState)(initialSettings);
2100
2053
  const [values, setValues] = (0, react.useState)(initialSettings?.values || null);
2101
- const [catalog, setCatalog] = (0, react.useState)(initialCatalog);
2054
+ const catalogState = useModelCatalog((0, react.useCallback)(() => api.modelCatalog(), [api]), {
2055
+ initialCatalog: initialCatalog ?? void 0,
2056
+ subscribe: api.subscribeGeneration
2057
+ });
2058
+ const { catalog, refresh: refreshCatalog } = catalogState;
2102
2059
  const [error, setError] = (0, react.useState)("");
2103
- const [catalogError, setCatalogError] = (0, react.useState)("");
2104
2060
  const [busy, setBusy] = (0, react.useState)(false);
2105
2061
  const [saved, setSaved] = (0, react.useState)(false);
2106
2062
  const [tab, setTab] = (0, react.useState)(initialTab);
@@ -2169,11 +2125,6 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2169
2125
  setSyncError("");
2170
2126
  refreshRef.current(false, true);
2171
2127
  let alive = true;
2172
- Promise.resolve().then(() => api.modelCatalog()).then((value) => {
2173
- if (alive) setCatalog(value);
2174
- }, (e) => {
2175
- if (alive) setCatalogError(errorMessage(e));
2176
- });
2177
2128
  const focus = () => {
2178
2129
  if (!dirty.current) refreshRef.current(false, true);
2179
2130
  };
@@ -2186,6 +2137,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2186
2137
  if (!alive) return;
2187
2138
  setSyncError("");
2188
2139
  external.current = true;
2140
+ refreshCatalog();
2189
2141
  if (dirty.current && !scope.writing) setExternalChange(true);
2190
2142
  refreshRef.current(false, true);
2191
2143
  }, (message) => {
@@ -2199,7 +2151,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2199
2151
  stopSettings?.();
2200
2152
  globalThis.removeEventListener?.("focus", focus);
2201
2153
  };
2202
- }, [api]);
2154
+ }, [api, refreshCatalog]);
2203
2155
  const change = (next) => {
2204
2156
  dirty.current = true;
2205
2157
  setSaved(false);
@@ -2275,7 +2227,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2275
2227
  key: "prompt",
2276
2228
  className: "dsh-ui-field"
2277
2229
  }, h("span", { className: "dsh-ui-label" }, t("prompt")), h("textarea", {
2278
- className: "dsh-safe-auto-input",
2230
+ className: "dsh-ui-textarea",
2279
2231
  rows: 6,
2280
2232
  maxLength: 4096,
2281
2233
  disabled: busy,
@@ -2291,20 +2243,13 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2291
2243
  id: "sa-prompt-hint",
2292
2244
  className: "dsh-ui-help"
2293
2245
  }, t("promptHint", (values.reviewerPrompt || "").length))
2294
- ]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, h("span", { className: "dsh-ui-label" }, t("unableLabel")), t("unableValue")))), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [
2295
- h("p", {
2296
- key: "line",
2297
- className: "dsh-ui-meta dsh-ui-wrap"
2298
- }, t("limitsLine", settings.policyLimits)),
2299
- h("p", {
2300
- key: "fuses",
2301
- className: "dsh-ui-meta dsh-ui-wrap"
2302
- }, t("limitsFuses", settings.policyLimits)),
2303
- h("p", {
2304
- key: "help",
2305
- className: "dsh-ui-help"
2306
- }, t("limitsHelp"))
2307
- ]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2246
+ ]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [h("p", {
2247
+ key: "line",
2248
+ className: "dsh-ui-meta dsh-ui-wrap"
2249
+ }, t("limitsLine", settings.policyLimits)), h("p", {
2250
+ key: "help",
2251
+ className: "dsh-ui-help"
2252
+ }, t("limitsHelp"))]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2308
2253
  variant: "primary",
2309
2254
  disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
2310
2255
  onClick: save
@@ -2315,12 +2260,9 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2315
2260
  }, t("refreshDiscard")))), h(ErrorText, {
2316
2261
  error,
2317
2262
  t
2318
- }), h(ErrorText, {
2319
- error: catalogError && {
2320
- message: catalogError,
2321
- context: "catalogFailed"
2322
- },
2323
- t
2263
+ }), h(ModelCatalogStatus, {
2264
+ state: catalogState,
2265
+ locale: localeLanguage(locale)
2324
2266
  }), externalChange && h("p", {
2325
2267
  role: "alert",
2326
2268
  className: "dsh-ui-error"
@@ -2333,6 +2275,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
2333
2275
  }, t("saved")));
2334
2276
  }
2335
2277
  function apply(ctx) {
2278
+ mountModelSettingsChanges(ctx, BUNDLE);
2336
2279
  const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
2337
2280
  const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
2338
2281
  const api = Object.freeze({
@@ -2340,11 +2283,9 @@ function apply(ctx) {
2340
2283
  subscribeGeneration: (listener) => ctx.connection.generation?.subscribe(listener) ?? (() => {}),
2341
2284
  getSettings: () => call("settings.get", {}),
2342
2285
  saveSettings: (payload) => call("settings.save", payload),
2343
- modelCatalog: () => ctx.remote.session.modelCatalog(),
2286
+ modelCatalog: () => readModelCatalog(ctx),
2344
2287
  getSession: (sessionId) => call("session.get", { sessionId }),
2345
- selectSession: (payload) => call("session.select", payload),
2346
- getApprovals: (sessionId) => call("approval.list", { sessionId }),
2347
- answerApproval: (payload) => call("approval.answer", payload)
2288
+ selectSession: (payload) => call("session.select", payload)
2348
2289
  });
2349
2290
  ctx.slots.inject("conversation.input.permission", () => ctx.slots.register({
2350
2291
  name: "conversation.input.permission",
@@ -2364,15 +2305,12 @@ function apply(ctx) {
2364
2305
  }, SettingsPanel));
2365
2306
  }
2366
2307
  //#endregion
2367
- exports.ApprovalDialog = ApprovalDialog;
2368
2308
  exports.BUNDLE = BUNDLE;
2369
2309
  exports.CHANNEL = CHANNEL;
2370
2310
  exports.PermissionMenu = PermissionMenu;
2371
2311
  exports.ReviewerFields = ReviewerFields;
2372
2312
  exports.SettingsPanel = SettingsPanel;
2373
2313
  exports.apply = apply;
2374
- exports.approvalRemaining = approvalRemaining;
2375
- exports.createApprovalController = createApprovalController;
2376
2314
  exports.createRequestScope = createRequestScope;
2377
2315
  exports.errorText = errorText;
2378
2316
  exports.inject = inject;