@klarkxy/dsh-safe-auto 0.2.0-rc.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -33,6 +33,21 @@ 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
+ }
36
51
  /** The short model label shown inside a provider group. */
37
52
  function modelMenuShortName(choice) {
38
53
  const sep = choice.label.indexOf(" / ");
@@ -151,7 +166,23 @@ const MEASURE_STYLE = {
151
166
  top: 0
152
167
  };
153
168
  function ModelMenu(props) {
154
- const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = "Model", modelValue = "—", effortLabel = "Reasoning", effortValue, defaultEffortLabel = "Default", searchPlaceholder = "Search models…", emptyLabel = "No matching models", clearSearchLabel = "Clear search", className, listClassName } = props;
169
+ const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
170
+ const copy = language === "zh" ? {
171
+ model: "模型",
172
+ effort: "思考强度",
173
+ default: "默认",
174
+ search: "搜索模型…",
175
+ empty: "没有匹配的模型",
176
+ clear: "清除搜索"
177
+ } : {
178
+ model: "Model",
179
+ effort: "Reasoning",
180
+ default: "Default",
181
+ search: "Search models…",
182
+ empty: "No matching models",
183
+ clear: "Clear search"
184
+ };
185
+ const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
155
186
  const rootRef = (0, react.useRef)(null);
156
187
  const openRef = (0, react.useRef)(open);
157
188
  openRef.current = open;
@@ -172,7 +203,14 @@ function ModelMenu(props) {
172
203
  const effortRows = (0, react.useMemo)(() => [{
173
204
  id: "",
174
205
  name: defaultEffortLabel
175
- }, ...efforts], [defaultEffortLabel, efforts]);
206
+ }, ...efforts.map((effort) => ({
207
+ ...effort,
208
+ name: modelMenuEffortLabel(effort, language)
209
+ }))], [
210
+ defaultEffortLabel,
211
+ efforts,
212
+ language
213
+ ]);
176
214
  const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
177
215
  ...group,
178
216
  items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
@@ -424,6 +462,7 @@ function ModelMenu(props) {
424
462
  style: menuPos ?? MEASURE_STYLE,
425
463
  role: pane === "model" ? "group" : "menu",
426
464
  "aria-label": modelLabel,
465
+ lang: language,
427
466
  children: [
428
467
  pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
429
468
  ref: itemRef(),
@@ -1045,7 +1084,7 @@ ${scope} {
1045
1084
  border: 0.5px solid var(--dsw-alias-border-l3);
1046
1085
  border-radius: var(--dsw-radius-md);
1047
1086
  background: var(--dsw-alias-bg-layer-1);
1048
- font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
1087
+ font-family: var(--ds-font-family-code);
1049
1088
  font-size: 12px;
1050
1089
  line-height: 20px;
1051
1090
  color: var(--dsw-alias-label-secondary);
@@ -1147,6 +1186,162 @@ function createPluginReadGate(now = Date.now) {
1147
1186
  };
1148
1187
  }
1149
1188
  //#endregion
1189
+ //#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
+ };
1209
+ //#endregion
1210
+ //#region ../dsh-plugin-kit/lib/model-settings-client.js
1211
+ /** This stream carries invalidations only; ready is never a settings snapshot. */
1212
+ function parseModelSettingsChangeFrame(value) {
1213
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
1214
+ const frame = value;
1215
+ if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
1216
+ if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
1217
+ kind: "changed",
1218
+ ns: frame.ns,
1219
+ revision: frame.revision
1220
+ };
1221
+ throw new Error("Invalid model settings change frame");
1222
+ }
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
+ }));
1231
+ /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
1232
+ * A factory shares one logical stream while at least one subscriber remains. */
1233
+ function createModelSettingsSubscriber(ctx, namespace) {
1234
+ const listeners = /* @__PURE__ */ new Set();
1235
+ let injection;
1236
+ let current;
1237
+ let lifecycle = 0;
1238
+ let failureMessage;
1239
+ const report = (message) => {
1240
+ failureMessage = message;
1241
+ for (const listener of [...listeners]) try {
1242
+ listener.error?.(message);
1243
+ } catch {}
1244
+ };
1245
+ const invalidate = () => {
1246
+ for (const listener of [...listeners]) {
1247
+ if (!listeners.has(listener)) continue;
1248
+ try {
1249
+ listener.change();
1250
+ } catch (error) {
1251
+ try {
1252
+ listener.error?.(error instanceof Error ? error.message : String(error));
1253
+ } catch {}
1254
+ }
1255
+ }
1256
+ };
1257
+ const start = () => {
1258
+ const life = ++lifecycle;
1259
+ injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
1260
+ if (life !== lifecycle || !listeners.size) return;
1261
+ const scope = injected;
1262
+ const stream = scope.remote.$stream({
1263
+ name: "Model settings sync",
1264
+ open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
1265
+ ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
1266
+ });
1267
+ const session = {
1268
+ stream,
1269
+ active: true
1270
+ };
1271
+ current = session;
1272
+ scope.effect(() => () => {
1273
+ session.active = false;
1274
+ if (current === session) {
1275
+ current = void 0;
1276
+ if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
1277
+ }
1278
+ return stream.dispose();
1279
+ });
1280
+ (async () => {
1281
+ let generation;
1282
+ let ready = false;
1283
+ try {
1284
+ for await (const item of stream) {
1285
+ if (!session.active || life !== lifecycle || !listeners.size) break;
1286
+ if (item.signal.aborted) continue;
1287
+ if (generation !== item.generation) {
1288
+ generation = item.generation;
1289
+ ready = false;
1290
+ }
1291
+ const frame = parseModelSettingsChangeFrame(item.value);
1292
+ if (frame.kind === "ready") {
1293
+ ready = true;
1294
+ failureMessage = void 0;
1295
+ item.accept();
1296
+ invalidate();
1297
+ } else {
1298
+ if (!ready) throw new Error("Model settings sync change arrived before ready");
1299
+ if (namespace === "*" || frame.ns === namespace) invalidate();
1300
+ }
1301
+ }
1302
+ } catch (error) {
1303
+ if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
1304
+ } finally {
1305
+ if (current === session) current = void 0;
1306
+ session.active = false;
1307
+ await stream.dispose().catch(() => {});
1308
+ }
1309
+ })();
1310
+ });
1311
+ queueMicrotask(() => {
1312
+ if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
1313
+ });
1314
+ };
1315
+ return Object.freeze({ subscribe(onChange, onError) {
1316
+ const listener = {
1317
+ change: onChange,
1318
+ error: onError
1319
+ };
1320
+ listeners.add(listener);
1321
+ if (!injection) start();
1322
+ else if (failureMessage) queueMicrotask(() => {
1323
+ if (listeners.has(listener) && failureMessage) try {
1324
+ listener.error?.(failureMessage);
1325
+ } catch {}
1326
+ });
1327
+ return () => {
1328
+ if (!listeners.delete(listener) || listeners.size) return;
1329
+ lifecycle++;
1330
+ if (current) {
1331
+ current.active = false;
1332
+ current.stream.dispose().catch(() => {});
1333
+ }
1334
+ current = void 0;
1335
+ failureMessage = void 0;
1336
+ const previous = injection;
1337
+ injection = void 0;
1338
+ try {
1339
+ Promise.resolve(previous?.dispose()).catch(() => {});
1340
+ } catch {}
1341
+ };
1342
+ } });
1343
+ }
1344
+ //#endregion
1150
1345
  //#region src/client.jsx
1151
1346
  const h = react.default.createElement;
1152
1347
  const name = "dsh-safe-auto-client";
@@ -1162,6 +1357,30 @@ const BUNDLE = "@klarkxy/dsh-safe-auto";
1162
1357
  const TEXT = {
1163
1358
  zh: {
1164
1359
  requestFailed: "请求失败",
1360
+ confirmationUnavailable: "确认已不可用",
1361
+ approvalInvalid: "确认请求无效",
1362
+ approvalExpiredError: "确认已超时或不再等待处理",
1363
+ invalidSession: "会话无效",
1364
+ sessionInactive: "会话未激活,请打开后重试",
1365
+ childSession: "子会话不能启用安全自动",
1366
+ invalidRequest: "请求无效",
1367
+ settingsChanged: "配置已更新,请刷新后重试",
1368
+ reviewerOnly: "这里只能修改审核配置",
1369
+ sessionChanged: "会话已更新,请刷新后重试",
1370
+ profileDisabled: "配置文件中已停用安全自动",
1371
+ workspaceUnavailable: "当前无法使用需原生审批的工作区写入权限",
1372
+ unknownPreset: "权限预设无效",
1373
+ controlUnloaded: "安全自动已停用",
1374
+ browserRequired: "请在浏览器中确认这次调用",
1375
+ unknownSetting: "审核模型设置无效",
1376
+ invalidModel: "审核模型或提供方标识无效",
1377
+ modelPair: "提供方和模型必须一起配置",
1378
+ effortRequiresModel: "选择思考强度前需先指定审核模型",
1379
+ invalidConfig: "配置无效",
1380
+ invalidConfigField: (field) => `配置项无效:${field}`,
1381
+ unknownConfigField: (field) => `未知配置项:${field}`,
1382
+ externalChange: "配置已在别处修改。草稿已保留,请刷新并丢弃草稿后重新确认。",
1383
+ syncUnavailable: "自动刷新不可用,请手动刷新配置。",
1165
1384
  follow: "跟随当前对话",
1166
1385
  searchModels: "搜索模型…",
1167
1386
  noModels: "没有匹配的模型",
@@ -1190,7 +1409,7 @@ const TEXT = {
1190
1409
  details: "安全说明",
1191
1410
  tabLimits: "预算边界",
1192
1411
  safetyDetail: "只处理原生 write/edit/bash/pwsh 的单次沙箱提权。审核会保留真人任务授权及后续限制,并按需读取工作区内的包配置和脚本;模型通过时自动放行。其余有效请求请你在 60 秒内确认,超时拒绝。确认只适用于这一次调用。",
1193
- unableLabel: "无法自动判断时",
1412
+ unableLabel: "无法自动判断时:",
1194
1413
  unableValue: "请你在 60 秒内确认;超时拒绝",
1195
1414
  approvalTitle: "确认这一次调用",
1196
1415
  approvalPending: (count) => `${count} 个请求待确认`,
@@ -1198,8 +1417,6 @@ const TEXT = {
1198
1417
  approvalExpired: "确认已超时,已拒绝。",
1199
1418
  approvalAllow: "仅允许这一次",
1200
1419
  approvalDeny: "拒绝",
1201
- approvalPermission: "请求权限",
1202
- approvalScope: "仅此调用",
1203
1420
  approvalReview: "模型审核意见",
1204
1421
  approvalRefresh: (message) => `${message}。正在刷新请求;不会自动重试决定。`,
1205
1422
  approvalEffect: "完整调用内容",
@@ -1217,6 +1434,30 @@ const TEXT = {
1217
1434
  },
1218
1435
  en: {
1219
1436
  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
+ invalidSession: "Invalid session",
1441
+ sessionInactive: "Session is not active; open it and retry",
1442
+ childSession: "Child sessions cannot select Safe Auto",
1443
+ invalidRequest: "Invalid request",
1444
+ settingsChanged: "Settings changed; reload and retry",
1445
+ reviewerOnly: "Only reviewer settings may be edited here",
1446
+ sessionChanged: "Session changed; refresh and retry",
1447
+ profileDisabled: "Safe Auto is disabled in the profile",
1448
+ workspaceUnavailable: "Workspace Write with native approval is unavailable",
1449
+ unknownPreset: "Unknown permission preset",
1450
+ controlUnloaded: "Safe Auto control unloaded",
1451
+ browserRequired: "Browser confirmation required",
1452
+ unknownSetting: "Invalid reviewer model setting",
1453
+ invalidModel: "Invalid reviewer provider/model identifier",
1454
+ modelPair: "Provider and model must be configured together",
1455
+ effortRequiresModel: "Reasoning effort requires a reviewer model",
1456
+ invalidConfig: "Invalid configuration",
1457
+ invalidConfigField: (field) => `Invalid configuration field: ${field}`,
1458
+ unknownConfigField: (field) => `Unknown configuration field: ${field}`,
1459
+ externalChange: "Settings changed elsewhere. Draft preserved; refresh and discard it before reviewing changes.",
1460
+ syncUnavailable: "Automatic refresh unavailable; refresh settings manually.",
1220
1461
  follow: "Follow conversation",
1221
1462
  searchModels: "Search models…",
1222
1463
  noModels: "No matching models",
@@ -1245,7 +1486,7 @@ const TEXT = {
1245
1486
  details: "Safety notes",
1246
1487
  tabLimits: "Limits",
1247
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.",
1248
- unableLabel: "When it cannot judge automatically",
1489
+ unableLabel: "When it cannot judge automatically: ",
1249
1490
  unableValue: "Ask you to confirm within 60 seconds; deny on timeout",
1250
1491
  approvalTitle: "Confirm this call",
1251
1492
  approvalPending: (count) => `${count} pending confirmation${count === 1 ? "" : "s"}`,
@@ -1253,8 +1494,6 @@ const TEXT = {
1253
1494
  approvalExpired: "Confirmation expired and was denied.",
1254
1495
  approvalAllow: "Allow this call only",
1255
1496
  approvalDeny: "Reject",
1256
- approvalPermission: "Requested permission",
1257
- approvalScope: "This call only",
1258
1497
  approvalReview: "Model review opinion",
1259
1498
  approvalRefresh: (message) => `${message}. Refreshing requests; your decision will not be retried automatically.`,
1260
1499
  approvalEffect: "Complete call content",
@@ -1284,14 +1523,46 @@ function translator(language) {
1284
1523
  };
1285
1524
  }
1286
1525
  function useText(locale) {
1287
- const [language, setLanguage] = (0, react.useState)(() => localeLanguage(locale));
1288
- (0, react.useEffect)(() => {
1289
- setLanguage(localeLanguage(locale));
1290
- if (typeof locale?.subscribe !== "function") return void 0;
1291
- return locale.subscribe(() => setLanguage(localeLanguage(locale)));
1292
- }, [locale]);
1526
+ const language = (0, react.useSyncExternalStore)((listener) => locale?.subscribe?.(listener) ?? (() => {}), () => localeLanguage(locale), () => localeLanguage(locale));
1293
1527
  return (0, react.useMemo)(() => translator(language), [language]);
1294
1528
  }
1529
+ const LOCAL_ERRORS = {
1530
+ "Request failed": "requestFailed",
1531
+ "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
+ "Invalid session": "invalidSession",
1538
+ "Session is not active; open it and retry": "sessionInactive",
1539
+ "Child sessions cannot select Safe Auto": "childSession",
1540
+ "Invalid request": "invalidRequest",
1541
+ "Settings changed; reload and retry": "settingsChanged",
1542
+ "Only reviewer settings may be edited here": "reviewerOnly",
1543
+ "Session changed; refresh the list and retry": "sessionChanged",
1544
+ "Unknown endpoint": "invalidRequest",
1545
+ "Safe Auto is disabled in the profile": "profileDisabled",
1546
+ "Workspace Write with native approval is unavailable": "workspaceUnavailable",
1547
+ "Unknown permission preset": "unknownPreset",
1548
+ "Safe Auto control unloaded": "controlUnloaded",
1549
+ "Browser confirmation required": "browserRequired",
1550
+ "Unknown Safe Auto model setting": "unknownSetting",
1551
+ "invalid reviewer provider/model identifier": "invalidModel",
1552
+ "provider and model must be configured together": "modelPair",
1553
+ "reasoningEffort requires a reviewer model": "effortRequiresModel"
1554
+ };
1555
+ function errorMessage(error) {
1556
+ return (typeof error === "string" ? error : error?.message) || TEXT.en.requestFailed;
1557
+ }
1558
+ function errorText(t, error) {
1559
+ const message = typeof error === "string" ? error : error?.message;
1560
+ const key = Object.hasOwn(LOCAL_ERRORS, message) ? LOCAL_ERRORS[message] : void 0;
1561
+ 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 || "");
1563
+ 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
+ return error?.context ? t(error.context, text) : text;
1565
+ }
1295
1566
  async function unwrapRpc(call, endpoint, payload, fallback = TEXT.en.requestFailed) {
1296
1567
  const result = await call(CHANNEL, endpoint, payload);
1297
1568
  if (!result?.ok) throw new Error(result?.error?.message || fallback);
@@ -1387,7 +1658,7 @@ function createApprovalController({ api, sessionId, onState, now = monotonicNow,
1387
1658
  });
1388
1659
  }
1389
1660
  } catch (error) {
1390
- if (active && ticket === generation) publish({ error: error.message });
1661
+ if (active && ticket === generation) publish({ error: errorMessage(error) });
1391
1662
  } finally {
1392
1663
  if (readId === readSequence) {
1393
1664
  reading = false;
@@ -1432,7 +1703,7 @@ function createApprovalController({ api, sessionId, onState, now = monotonicNow,
1432
1703
  } catch (error) {
1433
1704
  if (active) publish({
1434
1705
  requests: state.requests.filter((item) => item.id !== requestId),
1435
- error: error.message
1706
+ error: errorMessage(error)
1436
1707
  });
1437
1708
  } finally {
1438
1709
  writing = false;
@@ -1517,6 +1788,10 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1517
1788
  max-height: 100%;
1518
1789
  max-width: 100%;
1519
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%); }
1520
1795
  .dsh-safe-auto-approval-content {
1521
1796
  flex: 1 1 auto;
1522
1797
  min-height: 0;
@@ -1540,11 +1815,11 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
1540
1815
  function Style() {
1541
1816
  return h("style", null, css);
1542
1817
  }
1543
- function ErrorText({ error }) {
1818
+ function ErrorText({ error, t }) {
1544
1819
  return error ? h("p", {
1545
1820
  role: "alert",
1546
1821
  className: "dsh-ui-error dsh-ui-wrap"
1547
- }, error) : null;
1822
+ }, errorText(t, error)) : null;
1548
1823
  }
1549
1824
  /** Values are rendered as plain text, with no Markdown or HTML interpretation. */
1550
1825
  function ApprovalDialog({ request, pendingCount = 1, remainingMs, busy = false, error = "", onAnswer, locale }) {
@@ -1579,13 +1854,13 @@ function ApprovalDialog({ request, pendingCount = 1, remainingMs, busy = false,
1579
1854
  }, h("div", { className: "dsh-safe-auto dsh-ui-stack" }, h(Style), h("p", {
1580
1855
  className: "dsh-ui-meta",
1581
1856
  role: "status"
1582
- }, 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("approvalPermission")), h("pre", { className: "dsh-safe-auto-approval-pre" }, JSON.stringify({
1583
- ...action.permission,
1584
- scope: action.permission?.scope
1585
- }, null, 2)), h("span", { className: "dsh-ui-help" }, t("approvalScope"))), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("approvalEffect")), fields.map(([field, value]) => h("div", {
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", {
1586
1858
  key: field,
1587
1859
  className: "dsh-ui-field"
1588
- }, 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, { error })));
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
+ })));
1589
1864
  }
1590
1865
  /** Composer permission control via the replacement slot: native presets plus Safe Auto. */
1591
1866
  function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null }) {
@@ -1609,7 +1884,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1609
1884
  setSnapshot(value);
1610
1885
  setError("");
1611
1886
  },
1612
- error: (e) => setError(e.message)
1887
+ error: (e) => setError(errorMessage(e))
1613
1888
  }, false, automatic);
1614
1889
  refreshRef.current = refresh;
1615
1890
  (0, react.useEffect)(() => {
@@ -1694,7 +1969,10 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1694
1969
  setOpen(false);
1695
1970
  },
1696
1971
  error: (e) => {
1697
- setError(t("refreshRetry", e.message));
1972
+ setError({
1973
+ message: errorMessage(e),
1974
+ context: "refreshRetry"
1975
+ });
1698
1976
  refreshRef.current();
1699
1977
  },
1700
1978
  settled: () => setBusy(false)
@@ -1740,7 +2018,16 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
1740
2018
  if (next) refreshRef.current();
1741
2019
  }
1742
2020
  }, CurrentIcon && h(CurrentIcon), current ? permissionLabel(current.value, current.name, t) : snapshot?.current || t("permission"), h(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutlineRegular))
1743
- }), h(ErrorText, { error }), h(ErrorText, { error: approvals.error && t("approvalRefresh", approvals.error) }), pending.length ? h(ApprovalDialog, {
2021
+ }), h(ErrorText, {
2022
+ error,
2023
+ t
2024
+ }), h(ErrorText, {
2025
+ error: approvals.error && {
2026
+ message: approvals.error,
2027
+ context: "approvalRefresh"
2028
+ },
2029
+ t
2030
+ }), pending.length ? h(ApprovalDialog, {
1744
2031
  key: pending[0].id,
1745
2032
  request: pending[0],
1746
2033
  pendingCount: pending.length,
@@ -1758,7 +2045,8 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
1758
2045
  const state = reviewerMenuState(values, catalog);
1759
2046
  const effort = values.reasoningEffort || "";
1760
2047
  const currentLabel = state.follow ? t("follow") : state.selected?.label || `${state.provider} / ${state.model}`;
1761
- const effortName = state.efforts.find((item) => item.id === effort)?.name;
2048
+ const effortOption = state.efforts.find((item) => item.id === effort);
2049
+ const effortName = effortOption && modelMenuEffortLabel(effortOption, localeLanguage(locale));
1762
2050
  const unknownEffort = Boolean(effort && !state.follow && state.selected && !state.selected.efforts.some((item) => item.id === effort));
1763
2051
  const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
1764
2052
  return h("fieldset", {
@@ -1768,6 +2056,7 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
1768
2056
  open,
1769
2057
  onOpenChange: setOpen,
1770
2058
  listClassName: "dsh-safe-auto",
2059
+ locale: localeLanguage(locale),
1771
2060
  anchor: h("button", {
1772
2061
  type: "button",
1773
2062
  className: "dsh-model-menu-trigger",
@@ -1817,42 +2106,97 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1817
2106
  const [tab, setTab] = (0, react.useState)(initialTab);
1818
2107
  const tabsId = (0, react.useId)();
1819
2108
  const dirty = (0, react.useRef)(false);
2109
+ const reading = (0, react.useRef)(false);
2110
+ const pending = (0, react.useRef)(false);
2111
+ const external = (0, react.useRef)(false);
2112
+ const [externalChange, setExternalChange] = (0, react.useState)(false);
2113
+ const [syncError, setSyncError] = (0, react.useState)("");
1820
2114
  const scopeRef = (0, react.useRef)(null);
1821
2115
  const refreshRef = (0, react.useRef)(null);
1822
- const refresh = (discardDraft = false, automatic = false) => scopeRef.current?.run(() => api.getSettings(), {
1823
- value: (next) => {
1824
- if (dirty.current && !discardDraft) return;
1825
- setSettings(next);
1826
- setValues(next.values);
1827
- dirty.current = false;
1828
- setError("");
1829
- if (!automatic) setSaved(false);
1830
- },
1831
- error: (e) => setError(e.message)
1832
- }, false, automatic);
2116
+ const refresh = (discardDraft = false, automatic = false) => {
2117
+ const scope = scopeRef.current;
2118
+ if (!scope) return;
2119
+ if (reading.current || scope.writing) {
2120
+ pending.current = true;
2121
+ return;
2122
+ }
2123
+ pending.current = false;
2124
+ if (dirty.current && !discardDraft) {
2125
+ if (external.current) setExternalChange(true);
2126
+ return;
2127
+ }
2128
+ reading.current = true;
2129
+ setBusy(true);
2130
+ const request = scope.run(() => api.getSettings(), {
2131
+ value: (next) => {
2132
+ if (pending.current || dirty.current && !discardDraft) return;
2133
+ setSettings(next);
2134
+ setValues(next.values);
2135
+ dirty.current = false;
2136
+ setError("");
2137
+ external.current = false;
2138
+ setExternalChange(false);
2139
+ if (!automatic) setSaved(false);
2140
+ },
2141
+ error: (e) => {
2142
+ if (!pending.current) setError(errorMessage(e));
2143
+ },
2144
+ settled: () => {
2145
+ if (scopeRef.current !== scope) return;
2146
+ reading.current = false;
2147
+ setBusy(false);
2148
+ if (pending.current) refreshRef.current(false, true);
2149
+ }
2150
+ }, false, automatic && !external.current);
2151
+ request.then((ran) => {
2152
+ if (!ran && scopeRef.current === scope) {
2153
+ reading.current = false;
2154
+ setBusy(false);
2155
+ }
2156
+ });
2157
+ return request;
2158
+ };
1833
2159
  refreshRef.current = refresh;
1834
2160
  (0, react.useEffect)(() => {
1835
2161
  const scope = createRequestScope();
1836
2162
  scopeRef.current = scope;
2163
+ reading.current = false;
2164
+ pending.current = false;
2165
+ dirty.current = false;
2166
+ external.current = false;
2167
+ setBusy(false);
2168
+ setExternalChange(false);
2169
+ setSyncError("");
1837
2170
  refreshRef.current(false, true);
1838
2171
  let alive = true;
1839
2172
  Promise.resolve().then(() => api.modelCatalog()).then((value) => {
1840
2173
  if (alive) setCatalog(value);
1841
2174
  }, (e) => {
1842
- if (alive) setCatalogError(e.message);
2175
+ if (alive) setCatalogError(errorMessage(e));
1843
2176
  });
1844
2177
  const focus = () => {
1845
2178
  if (!dirty.current) refreshRef.current(false, true);
1846
2179
  };
1847
2180
  const stopConnection = api.subscribeGeneration?.(() => {
1848
2181
  scope.invalidate();
1849
- if (!dirty.current) refreshRef.current(false, true);
2182
+ external.current = true;
2183
+ refreshRef.current(false, true);
2184
+ });
2185
+ const stopSettings = api.subscribeSettings?.(() => {
2186
+ if (!alive) return;
2187
+ setSyncError("");
2188
+ external.current = true;
2189
+ if (dirty.current && !scope.writing) setExternalChange(true);
2190
+ refreshRef.current(false, true);
2191
+ }, (message) => {
2192
+ if (alive) setSyncError(message);
1850
2193
  });
1851
2194
  globalThis.addEventListener?.("focus", focus);
1852
2195
  return () => {
1853
2196
  alive = false;
1854
2197
  scope.dispose();
1855
2198
  stopConnection?.();
2199
+ stopSettings?.();
1856
2200
  globalThis.removeEventListener?.("focus", focus);
1857
2201
  };
1858
2202
  }, [api]);
@@ -1862,7 +2206,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1862
2206
  setValues(next);
1863
2207
  };
1864
2208
  const save = () => {
1865
- if (!settings || !values || scopeRef.current?.writing || (values.reviewerPrompt || "").length > 4096) return;
2209
+ if (!settings || !values || reading.current || external.current || scopeRef.current?.writing || (values.reviewerPrompt || "").length > 4096) return;
1866
2210
  setBusy(true);
1867
2211
  setError("");
1868
2212
  setSaved(false);
@@ -1876,8 +2220,14 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1876
2220
  dirty.current = false;
1877
2221
  setSaved(true);
1878
2222
  },
1879
- error: (e) => setError(t("saveFailed", e.message)),
1880
- settled: () => setBusy(false)
2223
+ error: (e) => setError({
2224
+ message: errorMessage(e),
2225
+ context: "saveFailed"
2226
+ }),
2227
+ settled: () => {
2228
+ setBusy(false);
2229
+ if (pending.current) refreshRef.current(false, true);
2230
+ }
1881
2231
  }, true);
1882
2232
  };
1883
2233
  const activeTab = tab === "limits" && !settings?.policyLimits ? "reviewer" : tab;
@@ -1895,7 +2245,10 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1895
2245
  tabIndex: 0,
1896
2246
  className
1897
2247
  }, children);
1898
- if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, { error }));
2248
+ if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
2249
+ error,
2250
+ t
2251
+ }));
1899
2252
  return h("section", {
1900
2253
  className: "dsh-safe-auto dsh-ui-stack",
1901
2254
  "aria-label": t("panelLabel"),
@@ -1938,7 +2291,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1938
2291
  id: "sa-prompt-hint",
1939
2292
  className: "dsh-ui-help"
1940
2293
  }, t("promptHint", (values.reviewerPrompt || "").length))
1941
- ]), 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", [
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", [
1942
2295
  h("p", {
1943
2296
  key: "line",
1944
2297
  className: "dsh-ui-meta dsh-ui-wrap"
@@ -1953,20 +2306,37 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
1953
2306
  }, t("limitsHelp"))
1954
2307
  ]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1955
2308
  variant: "primary",
1956
- disabled: busy || !dirty.current || (values.reviewerPrompt || "").length > 4096,
2309
+ disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
1957
2310
  onClick: save
1958
2311
  }, t("save")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1959
2312
  variant: "ghost",
1960
2313
  disabled: busy,
1961
2314
  onClick: () => refresh(true)
1962
- }, t("refreshDiscard")))), h(ErrorText, { error }), h(ErrorText, { error: catalogError && t("catalogFailed", catalogError) }), saved && h("p", {
2315
+ }, t("refreshDiscard")))), h(ErrorText, {
2316
+ error,
2317
+ t
2318
+ }), h(ErrorText, {
2319
+ error: catalogError && {
2320
+ message: catalogError,
2321
+ context: "catalogFailed"
2322
+ },
2323
+ t
2324
+ }), externalChange && h("p", {
2325
+ role: "alert",
2326
+ className: "dsh-ui-error"
2327
+ }, t("externalChange")), syncError && h("p", {
2328
+ role: "status",
2329
+ className: "dsh-ui-hint"
2330
+ }, t("syncUnavailable")), saved && h("p", {
1963
2331
  role: "status",
1964
2332
  className: "dsh-ui-notice"
1965
2333
  }, t("saved")));
1966
2334
  }
1967
2335
  function apply(ctx) {
1968
- const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload, translator(localeLanguage(ctx.locale))("requestFailed"));
2336
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
2337
+ const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
1969
2338
  const api = Object.freeze({
2339
+ subscribeSettings: (change, error) => settingsSubscriber.subscribe(change, error),
1970
2340
  subscribeGeneration: (listener) => ctx.connection.generation?.subscribe(listener) ?? (() => {}),
1971
2341
  getSettings: () => call("settings.get", {}),
1972
2342
  saveSettings: (payload) => call("settings.save", payload),
@@ -2004,6 +2374,7 @@ exports.apply = apply;
2004
2374
  exports.approvalRemaining = approvalRemaining;
2005
2375
  exports.createApprovalController = createApprovalController;
2006
2376
  exports.createRequestScope = createRequestScope;
2377
+ exports.errorText = errorText;
2007
2378
  exports.inject = inject;
2008
2379
  exports.localeLanguage = localeLanguage;
2009
2380
  exports.name = name;