@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/README.md +22 -22
- package/README.zh-CN.md +22 -22
- package/docs/ADR-0006.md +13 -5
- package/dsh.plugin.json +1 -1
- package/lib/client.js +420 -49
- package/package.json +12 -8
- package/src/approval-review.js +6 -2
- package/src/control.js +3 -3
- package/src/credential-paths.js +29 -0
- package/src/credential-reads.js +227 -0
- package/src/credential-text.js +361 -0
- package/src/execution-evidence.js +3 -2
- package/src/host-runtime.js +159 -0
- package/src/index.js +24 -14
- package/src/policy.js +24 -8
- package/src/review-context.js +4 -0
- package/src/review-paths.js +53 -0
- package/src/shell-command.js +48 -1
- package/src/ui-host.js +19 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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, {
|
|
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
|
|
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(
|
|
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, {
|
|
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
|
|
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) =>
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
1880
|
-
|
|
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, {
|
|
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")
|
|
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, {
|
|
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
|
|
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;
|