@klarkxy/dsh-safe-auto 0.2.3 → 0.3.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/README.md +21 -9
- package/README.zh-CN.md +21 -9
- package/dsh.plugin.json +2 -2
- package/lib/client.js +314 -403
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +6 -10
- package/src/config.js +12 -11
- package/src/control.js +27 -6
- package/src/dangerous.js +154 -0
- package/src/index.js +217 -235
- package/src/model-route.js +7 -3
- package/src/reviewer.js +114 -91
- package/src/transcript.js +107 -0
- package/src/ui-host.js +4 -27
- package/src/approval-review.js +0 -80
- package/src/credential-paths.js +0 -29
- package/src/credential-reads.js +0 -227
- package/src/credential-text.js +0 -361
- package/src/execution-evidence.js +0 -115
- package/src/gate.js +0 -81
- package/src/host-runtime.js +0 -159
- package/src/human-approval.js +0 -64
- package/src/policy.js +0 -58
- package/src/review-context.js +0 -35
- package/src/review-paths.js +0 -53
- package/src/shell-command.js +0 -547
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%; }
|
|
@@ -1348,8 +1575,7 @@ const name = "dsh-safe-auto-client";
|
|
|
1348
1575
|
const inject = [
|
|
1349
1576
|
"slots",
|
|
1350
1577
|
"connection",
|
|
1351
|
-
|
|
1352
|
-
"remote.session",
|
|
1578
|
+
...MODEL_CATALOG_INJECT,
|
|
1353
1579
|
"locale"
|
|
1354
1580
|
];
|
|
1355
1581
|
const CHANNEL = "/dsh-safe-auto";
|
|
@@ -1357,11 +1583,7 @@ const BUNDLE = "@klarkxy/dsh-safe-auto";
|
|
|
1357
1583
|
const TEXT = {
|
|
1358
1584
|
zh: {
|
|
1359
1585
|
requestFailed: "请求失败",
|
|
1360
|
-
confirmationUnavailable: "确认已不可用",
|
|
1361
|
-
approvalInvalid: "确认请求无效",
|
|
1362
|
-
approvalExpiredError: "确认已超时或不再等待处理",
|
|
1363
1586
|
invalidSession: "会话无效",
|
|
1364
|
-
sessionInactive: "会话未激活,请打开后重试",
|
|
1365
1587
|
childSession: "子会话不能启用安全自动",
|
|
1366
1588
|
invalidRequest: "请求无效",
|
|
1367
1589
|
settingsChanged: "配置已更新,请刷新后重试",
|
|
@@ -1371,7 +1593,6 @@ const TEXT = {
|
|
|
1371
1593
|
workspaceUnavailable: "当前无法使用需原生审批的工作区写入权限",
|
|
1372
1594
|
unknownPreset: "权限预设无效",
|
|
1373
1595
|
controlUnloaded: "安全自动已停用",
|
|
1374
|
-
browserRequired: "请在浏览器中确认这次调用",
|
|
1375
1596
|
unknownSetting: "审核模型设置无效",
|
|
1376
1597
|
invalidModel: "审核模型或提供方标识无效",
|
|
1377
1598
|
modelPair: "提供方和模型必须一起配置",
|
|
@@ -1408,37 +1629,22 @@ const TEXT = {
|
|
|
1408
1629
|
tabReviewer: "审核配置",
|
|
1409
1630
|
details: "安全说明",
|
|
1410
1631
|
tabLimits: "预算边界",
|
|
1411
|
-
safetyDetail: "
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
approvalTitle: "确认这一次调用",
|
|
1415
|
-
approvalPending: (count) => `${count} 个请求待确认`,
|
|
1416
|
-
approvalCountdown: (seconds) => `剩余 ${seconds} 秒 · 超时拒绝`,
|
|
1417
|
-
approvalExpired: "确认已超时,已拒绝。",
|
|
1418
|
-
approvalAllow: "仅允许这一次",
|
|
1419
|
-
approvalDeny: "拒绝",
|
|
1420
|
-
approvalReview: "模型审核意见",
|
|
1421
|
-
approvalRefresh: (message) => `${message}。正在刷新请求;不会自动重试决定。`,
|
|
1422
|
-
approvalEffect: "完整调用内容",
|
|
1632
|
+
safetyDetail: "普通调用照常在工作区沙箱内运行,插件不拦截。需要审批的请求(沙箱提权、少量危险命令,以及其他工具的审批)交给审核模型:通过就只放行这一次,拒绝或审核失败就把原因告诉智能体。",
|
|
1633
|
+
reviewInput: "发给审核模型的内容:对话记录(你的消息标为可信,其余只作参考,超长时截断)、工作目录和这次操作的完整参数。",
|
|
1634
|
+
breakerDetail: "同一轮对话里连续拒绝 3 次,或最近 50 次审核拒绝 10 次,本轮暂停自动审批,等你发新消息。",
|
|
1423
1635
|
prompt: "额外审核提示词",
|
|
1424
1636
|
promptHint: (count) => `${count}/4096 · 只可补充审核约束,不可扩大授权或覆盖安全规则。`,
|
|
1425
|
-
limitsLine: (l) =>
|
|
1426
|
-
|
|
1427
|
-
limitsHelp: "预算在 profile 中配置,保存设置或重新启用不会补充;动作、真人指令和有界的本地脚本证据会发送给审核模型。",
|
|
1637
|
+
limitsLine: (l) => `单次审核超时 ${Math.round(l.timeoutMs / 1e3)} 秒 · 输出上限 ${l.outputTokens} tokens · 对话记录上限 ${l.maxTranscriptBytes} bytes`,
|
|
1638
|
+
limitsHelp: "这些上限在 profile 中配置。",
|
|
1428
1639
|
save: "保存",
|
|
1429
1640
|
refreshDiscard: "刷新(丢弃草稿)",
|
|
1430
1641
|
saveFailed: (message) => `${message}。草稿已保留,刷新后重新编辑,不会自动重试。`,
|
|
1431
|
-
catalogFailed: (message) => `模型目录读取失败,已保存值保持不变:${message}`,
|
|
1432
1642
|
saved: "已保存",
|
|
1433
|
-
summary: "
|
|
1643
|
+
summary: "安全自动:需要审批的请求交给审核模型判断,通过才放行这一次。"
|
|
1434
1644
|
},
|
|
1435
1645
|
en: {
|
|
1436
1646
|
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
1647
|
invalidSession: "Invalid session",
|
|
1441
|
-
sessionInactive: "Session is not active; open it and retry",
|
|
1442
1648
|
childSession: "Child sessions cannot select Safe Auto",
|
|
1443
1649
|
invalidRequest: "Invalid request",
|
|
1444
1650
|
settingsChanged: "Settings changed; reload and retry",
|
|
@@ -1448,7 +1654,6 @@ const TEXT = {
|
|
|
1448
1654
|
workspaceUnavailable: "Workspace Write with native approval is unavailable",
|
|
1449
1655
|
unknownPreset: "Unknown permission preset",
|
|
1450
1656
|
controlUnloaded: "Safe Auto control unloaded",
|
|
1451
|
-
browserRequired: "Browser confirmation required",
|
|
1452
1657
|
unknownSetting: "Invalid reviewer model setting",
|
|
1453
1658
|
invalidModel: "Invalid reviewer provider/model identifier",
|
|
1454
1659
|
modelPair: "Provider and model must be configured together",
|
|
@@ -1485,29 +1690,18 @@ const TEXT = {
|
|
|
1485
1690
|
tabReviewer: "Reviewer",
|
|
1486
1691
|
details: "Safety notes",
|
|
1487
1692
|
tabLimits: "Limits",
|
|
1488
|
-
safetyDetail: "
|
|
1489
|
-
|
|
1490
|
-
|
|
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",
|
|
1693
|
+
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.",
|
|
1694
|
+
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.",
|
|
1695
|
+
breakerDetail: "After 3 consecutive denials, or 10 denials among the last 50 reviews in one turn, automatic approval pauses until your next message.",
|
|
1500
1696
|
prompt: "Additional reviewer prompt",
|
|
1501
1697
|
promptHint: (count) => `${count}/4096 · May add review constraints, never expand authorization or override safety rules.`,
|
|
1502
|
-
limitsLine: (l) => `Review timeout ${l.timeoutMs}
|
|
1503
|
-
|
|
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.",
|
|
1698
|
+
limitsLine: (l) => `Review timeout ${Math.round(l.timeoutMs / 1e3)} s · Output cap ${l.outputTokens} tokens · Transcript cap ${l.maxTranscriptBytes} bytes`,
|
|
1699
|
+
limitsHelp: "These limits are configured in the profile.",
|
|
1505
1700
|
save: "Save",
|
|
1506
1701
|
refreshDiscard: "Refresh (discard draft)",
|
|
1507
1702
|
saveFailed: (message) => `${message}. Draft retained; refresh before editing again. No automatic retry.`,
|
|
1508
|
-
catalogFailed: (message) => `Model catalog failed; saved values retained: ${message}`,
|
|
1509
1703
|
saved: "Saved",
|
|
1510
|
-
summary: "Safe Auto:
|
|
1704
|
+
summary: "Safe Auto: a reviewer model answers approval requests and approves only the calls that pass."
|
|
1511
1705
|
}
|
|
1512
1706
|
};
|
|
1513
1707
|
/** The host locale may arrive as the injected service or as a plain tag. */
|
|
@@ -1529,13 +1723,7 @@ function useText(locale) {
|
|
|
1529
1723
|
const LOCAL_ERRORS = {
|
|
1530
1724
|
"Request failed": "requestFailed",
|
|
1531
1725
|
"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
1726
|
"Invalid session": "invalidSession",
|
|
1538
|
-
"Session is not active; open it and retry": "sessionInactive",
|
|
1539
1727
|
"Child sessions cannot select Safe Auto": "childSession",
|
|
1540
1728
|
"Invalid request": "invalidRequest",
|
|
1541
1729
|
"Settings changed; reload and retry": "settingsChanged",
|
|
@@ -1546,7 +1734,6 @@ const LOCAL_ERRORS = {
|
|
|
1546
1734
|
"Workspace Write with native approval is unavailable": "workspaceUnavailable",
|
|
1547
1735
|
"Unknown permission preset": "unknownPreset",
|
|
1548
1736
|
"Safe Auto control unloaded": "controlUnloaded",
|
|
1549
|
-
"Browser confirmation required": "browserRequired",
|
|
1550
1737
|
"Unknown Safe Auto model setting": "unknownSetting",
|
|
1551
1738
|
"invalid reviewer provider/model identifier": "invalidModel",
|
|
1552
1739
|
"provider and model must be configured together": "modelPair",
|
|
@@ -1555,11 +1742,16 @@ const LOCAL_ERRORS = {
|
|
|
1555
1742
|
function errorMessage(error) {
|
|
1556
1743
|
return (typeof error === "string" ? error : error?.message) || TEXT.en.requestFailed;
|
|
1557
1744
|
}
|
|
1745
|
+
const INACTIVE_SESSION = "Session is not active; open it and retry";
|
|
1746
|
+
function shownPermissionError(error) {
|
|
1747
|
+
const message = errorMessage(error);
|
|
1748
|
+
return message === INACTIVE_SESSION ? "" : message;
|
|
1749
|
+
}
|
|
1558
1750
|
function errorText(t, error) {
|
|
1559
1751
|
const message = typeof error === "string" ? error : error?.message;
|
|
1560
1752
|
const key = Object.hasOwn(LOCAL_ERRORS, message) ? LOCAL_ERRORS[message] : void 0;
|
|
1561
1753
|
const unknownField = /^unknown config field: (.+)$/.exec(message || "");
|
|
1562
|
-
const invalidField = /^(?:(enabled
|
|
1754
|
+
const invalidField = /^(?:(enabled) must be a boolean|(provider|model|reasoningEffort|reviewerPrompt) must be a string|invalid (timeoutMs|outputTokens|maxTranscriptBytes))$/.exec(message || "");
|
|
1563
1755
|
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
1756
|
return error?.context ? t(error.context, text) : text;
|
|
1565
1757
|
}
|
|
@@ -1603,120 +1795,6 @@ function createRequestScope() {
|
|
|
1603
1795
|
}
|
|
1604
1796
|
};
|
|
1605
1797
|
}
|
|
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
1798
|
function reviewerMenuState(values, catalog) {
|
|
1721
1799
|
const provider = values.provider || "", model = values.model || "";
|
|
1722
1800
|
const follow = !provider && !model;
|
|
@@ -1765,52 +1843,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
|
|
|
1765
1843
|
/* A fieldset is the settings group and takes the contract's card surface; its
|
|
1766
1844
|
* legend keeps the UA inline padding from indenting the group title. */
|
|
1767
1845
|
.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
1846
|
`;
|
|
1815
1847
|
function Style() {
|
|
1816
1848
|
return h("style", null, css);
|
|
@@ -1821,47 +1853,6 @@ function ErrorText({ error, t }) {
|
|
|
1821
1853
|
className: "dsh-ui-error dsh-ui-wrap"
|
|
1822
1854
|
}, errorText(t, error)) : null;
|
|
1823
1855
|
}
|
|
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
1856
|
/** Composer permission control via the replacement slot: native presets plus Safe Auto. */
|
|
1866
1857
|
function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null }) {
|
|
1867
1858
|
const t = useText(locale);
|
|
@@ -1871,20 +1862,15 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1871
1862
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1872
1863
|
const scopeRef = (0, react.useRef)(null);
|
|
1873
1864
|
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
1865
|
const refresh = (automatic = false) => scopeRef.current?.run(() => api.getSession(sessionId), {
|
|
1883
1866
|
value: (value) => {
|
|
1884
1867
|
setSnapshot(value);
|
|
1885
1868
|
setError("");
|
|
1886
1869
|
},
|
|
1887
|
-
error: (e) =>
|
|
1870
|
+
error: (e) => {
|
|
1871
|
+
const message = shownPermissionError(e);
|
|
1872
|
+
if (message) setError(message);
|
|
1873
|
+
}
|
|
1888
1874
|
}, false, automatic);
|
|
1889
1875
|
refreshRef.current = refresh;
|
|
1890
1876
|
(0, react.useEffect)(() => {
|
|
@@ -1905,50 +1891,6 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1905
1891
|
globalThis.removeEventListener?.("focus", focus);
|
|
1906
1892
|
};
|
|
1907
1893
|
}, [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
1894
|
const choose = (value) => {
|
|
1953
1895
|
const scope = scopeRef.current;
|
|
1954
1896
|
if (!snapshot || scope?.writing) return;
|
|
@@ -1969,8 +1911,9 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1969
1911
|
setOpen(false);
|
|
1970
1912
|
},
|
|
1971
1913
|
error: (e) => {
|
|
1972
|
-
|
|
1973
|
-
|
|
1914
|
+
const message = shownPermissionError(e);
|
|
1915
|
+
if (message) setError({
|
|
1916
|
+
message,
|
|
1974
1917
|
context: "refreshRetry"
|
|
1975
1918
|
});
|
|
1976
1919
|
refreshRef.current();
|
|
@@ -2021,23 +1964,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
2021
1964
|
}), h(ErrorText, {
|
|
2022
1965
|
error,
|
|
2023
1966
|
t
|
|
2024
|
-
})
|
|
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);
|
|
1967
|
+
}));
|
|
2041
1968
|
}
|
|
2042
1969
|
function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
|
|
2043
1970
|
const t = useText(locale);
|
|
@@ -2098,9 +2025,12 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2098
2025
|
const t = useText(locale);
|
|
2099
2026
|
const [settings, setSettings] = (0, react.useState)(initialSettings);
|
|
2100
2027
|
const [values, setValues] = (0, react.useState)(initialSettings?.values || null);
|
|
2101
|
-
const
|
|
2028
|
+
const catalogState = useModelCatalog((0, react.useCallback)(() => api.modelCatalog(), [api]), {
|
|
2029
|
+
initialCatalog: initialCatalog ?? void 0,
|
|
2030
|
+
subscribe: api.subscribeGeneration
|
|
2031
|
+
});
|
|
2032
|
+
const { catalog, refresh: refreshCatalog } = catalogState;
|
|
2102
2033
|
const [error, setError] = (0, react.useState)("");
|
|
2103
|
-
const [catalogError, setCatalogError] = (0, react.useState)("");
|
|
2104
2034
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2105
2035
|
const [saved, setSaved] = (0, react.useState)(false);
|
|
2106
2036
|
const [tab, setTab] = (0, react.useState)(initialTab);
|
|
@@ -2169,11 +2099,6 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2169
2099
|
setSyncError("");
|
|
2170
2100
|
refreshRef.current(false, true);
|
|
2171
2101
|
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
2102
|
const focus = () => {
|
|
2178
2103
|
if (!dirty.current) refreshRef.current(false, true);
|
|
2179
2104
|
};
|
|
@@ -2186,6 +2111,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2186
2111
|
if (!alive) return;
|
|
2187
2112
|
setSyncError("");
|
|
2188
2113
|
external.current = true;
|
|
2114
|
+
refreshCatalog();
|
|
2189
2115
|
if (dirty.current && !scope.writing) setExternalChange(true);
|
|
2190
2116
|
refreshRef.current(false, true);
|
|
2191
2117
|
}, (message) => {
|
|
@@ -2199,7 +2125,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2199
2125
|
stopSettings?.();
|
|
2200
2126
|
globalThis.removeEventListener?.("focus", focus);
|
|
2201
2127
|
};
|
|
2202
|
-
}, [api]);
|
|
2128
|
+
}, [api, refreshCatalog]);
|
|
2203
2129
|
const change = (next) => {
|
|
2204
2130
|
dirty.current = true;
|
|
2205
2131
|
setSaved(false);
|
|
@@ -2275,7 +2201,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2275
2201
|
key: "prompt",
|
|
2276
2202
|
className: "dsh-ui-field"
|
|
2277
2203
|
}, h("span", { className: "dsh-ui-label" }, t("prompt")), h("textarea", {
|
|
2278
|
-
className: "dsh-
|
|
2204
|
+
className: "dsh-ui-textarea",
|
|
2279
2205
|
rows: 6,
|
|
2280
2206
|
maxLength: 4096,
|
|
2281
2207
|
disabled: busy,
|
|
@@ -2291,20 +2217,13 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2291
2217
|
id: "sa-prompt-hint",
|
|
2292
2218
|
className: "dsh-ui-help"
|
|
2293
2219
|
}, 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,
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
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, {
|
|
2220
|
+
]), 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", {
|
|
2221
|
+
key: "line",
|
|
2222
|
+
className: "dsh-ui-meta dsh-ui-wrap"
|
|
2223
|
+
}, t("limitsLine", settings.policyLimits)), h("p", {
|
|
2224
|
+
key: "help",
|
|
2225
|
+
className: "dsh-ui-help"
|
|
2226
|
+
}, t("limitsHelp"))]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2308
2227
|
variant: "primary",
|
|
2309
2228
|
disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
|
|
2310
2229
|
onClick: save
|
|
@@ -2315,12 +2234,9 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2315
2234
|
}, t("refreshDiscard")))), h(ErrorText, {
|
|
2316
2235
|
error,
|
|
2317
2236
|
t
|
|
2318
|
-
}), h(
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
context: "catalogFailed"
|
|
2322
|
-
},
|
|
2323
|
-
t
|
|
2237
|
+
}), h(ModelCatalogStatus, {
|
|
2238
|
+
state: catalogState,
|
|
2239
|
+
locale: localeLanguage(locale)
|
|
2324
2240
|
}), externalChange && h("p", {
|
|
2325
2241
|
role: "alert",
|
|
2326
2242
|
className: "dsh-ui-error"
|
|
@@ -2340,11 +2256,9 @@ function apply(ctx) {
|
|
|
2340
2256
|
subscribeGeneration: (listener) => ctx.connection.generation?.subscribe(listener) ?? (() => {}),
|
|
2341
2257
|
getSettings: () => call("settings.get", {}),
|
|
2342
2258
|
saveSettings: (payload) => call("settings.save", payload),
|
|
2343
|
-
modelCatalog: () => ctx
|
|
2259
|
+
modelCatalog: () => readModelCatalog(ctx),
|
|
2344
2260
|
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)
|
|
2261
|
+
selectSession: (payload) => call("session.select", payload)
|
|
2348
2262
|
});
|
|
2349
2263
|
ctx.slots.inject("conversation.input.permission", () => ctx.slots.register({
|
|
2350
2264
|
name: "conversation.input.permission",
|
|
@@ -2364,15 +2278,12 @@ function apply(ctx) {
|
|
|
2364
2278
|
}, SettingsPanel));
|
|
2365
2279
|
}
|
|
2366
2280
|
//#endregion
|
|
2367
|
-
exports.ApprovalDialog = ApprovalDialog;
|
|
2368
2281
|
exports.BUNDLE = BUNDLE;
|
|
2369
2282
|
exports.CHANNEL = CHANNEL;
|
|
2370
2283
|
exports.PermissionMenu = PermissionMenu;
|
|
2371
2284
|
exports.ReviewerFields = ReviewerFields;
|
|
2372
2285
|
exports.SettingsPanel = SettingsPanel;
|
|
2373
2286
|
exports.apply = apply;
|
|
2374
|
-
exports.approvalRemaining = approvalRemaining;
|
|
2375
|
-
exports.createApprovalController = createApprovalController;
|
|
2376
2287
|
exports.createRequestScope = createRequestScope;
|
|
2377
2288
|
exports.errorText = errorText;
|
|
2378
2289
|
exports.inject = inject;
|