@klarkxy/dsh-safe-auto 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/README.zh-CN.md +21 -9
- package/dsh.plugin.json +3 -3
- package/lib/client.js +372 -434
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +20 -16
- 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 -5
- package/src/reviewer.js +119 -91
- package/src/transcript.js +107 -0
- package/src/ui-host.js +7 -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%; }
|
|
@@ -1187,25 +1414,18 @@ function createPluginReadGate(now = Date.now) {
|
|
|
1187
1414
|
}
|
|
1188
1415
|
//#endregion
|
|
1189
1416
|
//#region ../dsh-plugin-kit/lib/contracts.js
|
|
1190
|
-
/**
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
method: "watch",
|
|
1203
|
-
mode: "stream",
|
|
1204
|
-
invocation: { kind: "direct" },
|
|
1205
|
-
parameters: [],
|
|
1206
|
-
cancellation: { parameter: "signal" }
|
|
1207
|
-
}]
|
|
1208
|
-
};
|
|
1417
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
1418
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
1419
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
1420
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1421
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
1422
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
1423
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
1424
|
+
return {
|
|
1425
|
+
service: segment,
|
|
1426
|
+
namespace: segment
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1209
1429
|
//#endregion
|
|
1210
1430
|
//#region ../dsh-plugin-kit/lib/model-settings-client.js
|
|
1211
1431
|
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
@@ -1220,17 +1440,44 @@ function parseModelSettingsChangeFrame(value) {
|
|
|
1220
1440
|
};
|
|
1221
1441
|
throw new Error("Invalid model settings change frame");
|
|
1222
1442
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1443
|
+
/** One owner's contribution. Each package mounts only its own endpoint. */
|
|
1444
|
+
function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
|
|
1445
|
+
return {
|
|
1446
|
+
package: packageName,
|
|
1447
|
+
descriptors: [{
|
|
1448
|
+
id: `${identity.namespace}/watch`,
|
|
1449
|
+
service: identity.service,
|
|
1450
|
+
namespace: identity.namespace,
|
|
1451
|
+
method: "watch",
|
|
1452
|
+
mode: "stream",
|
|
1453
|
+
invocation: { kind: "direct" },
|
|
1454
|
+
parameters: [],
|
|
1455
|
+
cancellation: { parameter: "signal" },
|
|
1456
|
+
result: {
|
|
1457
|
+
mode: "strict",
|
|
1458
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
1459
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
1460
|
+
}
|
|
1461
|
+
}]
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/** Mount from a synchronous apply. Disposal waits for an in-flight mount and
|
|
1465
|
+
* then withdraws it, so the contribution cannot outlive the plugin. */
|
|
1466
|
+
function mountModelSettingsChanges(ctx, packageName) {
|
|
1467
|
+
const contribution = modelSettingsChangesRemote(packageName);
|
|
1468
|
+
ctx.inject(["remote"], (injected) => {
|
|
1469
|
+
const scope = injected;
|
|
1470
|
+
const mounting = scope.remote.$mount(contribution);
|
|
1471
|
+
mounting.catch(() => {});
|
|
1472
|
+
scope.effect(() => async () => {
|
|
1473
|
+
await (await mounting.catch(() => void 0))?.();
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1231
1477
|
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
1232
1478
|
* A factory shares one logical stream while at least one subscriber remains. */
|
|
1233
|
-
function createModelSettingsSubscriber(ctx,
|
|
1479
|
+
function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
|
|
1480
|
+
const endpoint = identity;
|
|
1234
1481
|
const listeners = /* @__PURE__ */ new Set();
|
|
1235
1482
|
let injection;
|
|
1236
1483
|
let current;
|
|
@@ -1256,12 +1503,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
1256
1503
|
};
|
|
1257
1504
|
const start = () => {
|
|
1258
1505
|
const life = ++lifecycle;
|
|
1259
|
-
injection = ctx.inject(["remote",
|
|
1506
|
+
injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
|
|
1260
1507
|
if (life !== lifecycle || !listeners.size) return;
|
|
1261
1508
|
const scope = injected;
|
|
1509
|
+
const service = scope.remote[endpoint.namespace];
|
|
1510
|
+
if (!service || !("watch" in service) || !service.watch) {
|
|
1511
|
+
report("Model settings sync service unavailable");
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const watch = service.watch;
|
|
1262
1515
|
const stream = scope.remote.$stream({
|
|
1263
1516
|
name: "Model settings sync",
|
|
1264
|
-
open: (signal) =>
|
|
1517
|
+
open: (signal) => watch(signal),
|
|
1265
1518
|
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
1266
1519
|
});
|
|
1267
1520
|
const session = {
|
|
@@ -1296,7 +1549,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
|
|
|
1296
1549
|
invalidate();
|
|
1297
1550
|
} else {
|
|
1298
1551
|
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
1299
|
-
if (
|
|
1552
|
+
if (accept(frame.ns)) invalidate();
|
|
1300
1553
|
}
|
|
1301
1554
|
}
|
|
1302
1555
|
} catch (error) {
|
|
@@ -1348,8 +1601,7 @@ const name = "dsh-safe-auto-client";
|
|
|
1348
1601
|
const inject = [
|
|
1349
1602
|
"slots",
|
|
1350
1603
|
"connection",
|
|
1351
|
-
|
|
1352
|
-
"remote.session",
|
|
1604
|
+
...MODEL_CATALOG_INJECT,
|
|
1353
1605
|
"locale"
|
|
1354
1606
|
];
|
|
1355
1607
|
const CHANNEL = "/dsh-safe-auto";
|
|
@@ -1357,11 +1609,7 @@ const BUNDLE = "@klarkxy/dsh-safe-auto";
|
|
|
1357
1609
|
const TEXT = {
|
|
1358
1610
|
zh: {
|
|
1359
1611
|
requestFailed: "请求失败",
|
|
1360
|
-
confirmationUnavailable: "确认已不可用",
|
|
1361
|
-
approvalInvalid: "确认请求无效",
|
|
1362
|
-
approvalExpiredError: "确认已超时或不再等待处理",
|
|
1363
1612
|
invalidSession: "会话无效",
|
|
1364
|
-
sessionInactive: "会话未激活,请打开后重试",
|
|
1365
1613
|
childSession: "子会话不能启用安全自动",
|
|
1366
1614
|
invalidRequest: "请求无效",
|
|
1367
1615
|
settingsChanged: "配置已更新,请刷新后重试",
|
|
@@ -1371,7 +1619,6 @@ const TEXT = {
|
|
|
1371
1619
|
workspaceUnavailable: "当前无法使用需原生审批的工作区写入权限",
|
|
1372
1620
|
unknownPreset: "权限预设无效",
|
|
1373
1621
|
controlUnloaded: "安全自动已停用",
|
|
1374
|
-
browserRequired: "请在浏览器中确认这次调用",
|
|
1375
1622
|
unknownSetting: "审核模型设置无效",
|
|
1376
1623
|
invalidModel: "审核模型或提供方标识无效",
|
|
1377
1624
|
modelPair: "提供方和模型必须一起配置",
|
|
@@ -1408,37 +1655,22 @@ const TEXT = {
|
|
|
1408
1655
|
tabReviewer: "审核配置",
|
|
1409
1656
|
details: "安全说明",
|
|
1410
1657
|
tabLimits: "预算边界",
|
|
1411
|
-
safetyDetail: "
|
|
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: "完整调用内容",
|
|
1658
|
+
safetyDetail: "普通调用照常在工作区沙箱内运行,插件不拦截。需要审批的请求(沙箱提权、少量危险命令,以及其他工具的审批)交给审核模型:通过就只放行这一次,拒绝或审核失败就把原因告诉智能体。",
|
|
1659
|
+
reviewInput: "发给审核模型的内容:对话记录(你的消息标为可信,其余只作参考,超长时截断)、工作目录和这次操作的完整参数。",
|
|
1660
|
+
breakerDetail: "同一轮对话里连续拒绝 3 次,或最近 50 次审核拒绝 10 次,本轮暂停自动审批,等你发新消息。",
|
|
1423
1661
|
prompt: "额外审核提示词",
|
|
1424
1662
|
promptHint: (count) => `${count}/4096 · 只可补充审核约束,不可扩大授权或覆盖安全规则。`,
|
|
1425
|
-
limitsLine: (l) =>
|
|
1426
|
-
|
|
1427
|
-
limitsHelp: "预算在 profile 中配置,保存设置或重新启用不会补充;动作、真人指令和有界的本地脚本证据会发送给审核模型。",
|
|
1663
|
+
limitsLine: (l) => `单次审核超时 ${Math.round(l.timeoutMs / 1e3)} 秒 · 输出上限 ${l.outputTokens} tokens · 对话记录上限 ${l.maxTranscriptBytes} bytes`,
|
|
1664
|
+
limitsHelp: "这些上限在 profile 中配置。",
|
|
1428
1665
|
save: "保存",
|
|
1429
1666
|
refreshDiscard: "刷新(丢弃草稿)",
|
|
1430
1667
|
saveFailed: (message) => `${message}。草稿已保留,刷新后重新编辑,不会自动重试。`,
|
|
1431
|
-
catalogFailed: (message) => `模型目录读取失败,已保存值保持不变:${message}`,
|
|
1432
1668
|
saved: "已保存",
|
|
1433
|
-
summary: "
|
|
1669
|
+
summary: "安全自动:需要审批的请求交给审核模型判断,通过才放行这一次。"
|
|
1434
1670
|
},
|
|
1435
1671
|
en: {
|
|
1436
1672
|
requestFailed: "Request failed",
|
|
1437
|
-
confirmationUnavailable: "Confirmation is no longer available",
|
|
1438
|
-
approvalInvalid: "Invalid approval request",
|
|
1439
|
-
approvalExpiredError: "Approval expired or is no longer pending",
|
|
1440
1673
|
invalidSession: "Invalid session",
|
|
1441
|
-
sessionInactive: "Session is not active; open it and retry",
|
|
1442
1674
|
childSession: "Child sessions cannot select Safe Auto",
|
|
1443
1675
|
invalidRequest: "Invalid request",
|
|
1444
1676
|
settingsChanged: "Settings changed; reload and retry",
|
|
@@ -1448,7 +1680,6 @@ const TEXT = {
|
|
|
1448
1680
|
workspaceUnavailable: "Workspace Write with native approval is unavailable",
|
|
1449
1681
|
unknownPreset: "Unknown permission preset",
|
|
1450
1682
|
controlUnloaded: "Safe Auto control unloaded",
|
|
1451
|
-
browserRequired: "Browser confirmation required",
|
|
1452
1683
|
unknownSetting: "Invalid reviewer model setting",
|
|
1453
1684
|
invalidModel: "Invalid reviewer provider/model identifier",
|
|
1454
1685
|
modelPair: "Provider and model must be configured together",
|
|
@@ -1485,29 +1716,18 @@ const TEXT = {
|
|
|
1485
1716
|
tabReviewer: "Reviewer",
|
|
1486
1717
|
details: "Safety notes",
|
|
1487
1718
|
tabLimits: "Limits",
|
|
1488
|
-
safetyDetail: "
|
|
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",
|
|
1719
|
+
safetyDetail: "Ordinary calls run inside the workspace sandbox as usual; the plugin does not intercept them. Requests that need approval (sandbox escalations, a short list of dangerous commands, and approval prompts from other tools) go to the reviewer model: a pass approves that one call; a denial or review failure is explained to the agent.",
|
|
1720
|
+
reviewInput: "Sent to the reviewer: the conversation (your messages marked trusted, everything else as evidence, truncated when long), the working directory, and the complete arguments of the action.",
|
|
1721
|
+
breakerDetail: "After 3 consecutive denials, or 10 denials among the last 50 reviews in one turn, automatic approval pauses until your next message.",
|
|
1500
1722
|
prompt: "Additional reviewer prompt",
|
|
1501
1723
|
promptHint: (count) => `${count}/4096 · May add review constraints, never expand authorization or override safety rules.`,
|
|
1502
|
-
limitsLine: (l) => `Review timeout ${l.timeoutMs}
|
|
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.",
|
|
1724
|
+
limitsLine: (l) => `Review timeout ${Math.round(l.timeoutMs / 1e3)} s · Output cap ${l.outputTokens} tokens · Transcript cap ${l.maxTranscriptBytes} bytes`,
|
|
1725
|
+
limitsHelp: "These limits are configured in the profile.",
|
|
1505
1726
|
save: "Save",
|
|
1506
1727
|
refreshDiscard: "Refresh (discard draft)",
|
|
1507
1728
|
saveFailed: (message) => `${message}. Draft retained; refresh before editing again. No automatic retry.`,
|
|
1508
|
-
catalogFailed: (message) => `Model catalog failed; saved values retained: ${message}`,
|
|
1509
1729
|
saved: "Saved",
|
|
1510
|
-
summary: "Safe Auto:
|
|
1730
|
+
summary: "Safe Auto: a reviewer model answers approval requests and approves only the calls that pass."
|
|
1511
1731
|
}
|
|
1512
1732
|
};
|
|
1513
1733
|
/** The host locale may arrive as the injected service or as a plain tag. */
|
|
@@ -1529,13 +1749,7 @@ function useText(locale) {
|
|
|
1529
1749
|
const LOCAL_ERRORS = {
|
|
1530
1750
|
"Request failed": "requestFailed",
|
|
1531
1751
|
"Request rejected": "requestFailed",
|
|
1532
|
-
"Confirmation is no longer available": "confirmationUnavailable",
|
|
1533
|
-
"Invalid approval response": "approvalInvalid",
|
|
1534
|
-
"Invalid approval payload": "approvalInvalid",
|
|
1535
|
-
"Approval expired or is no longer pending": "approvalExpiredError",
|
|
1536
|
-
"Approval expired or invalidated": "approvalExpiredError",
|
|
1537
1752
|
"Invalid session": "invalidSession",
|
|
1538
|
-
"Session is not active; open it and retry": "sessionInactive",
|
|
1539
1753
|
"Child sessions cannot select Safe Auto": "childSession",
|
|
1540
1754
|
"Invalid request": "invalidRequest",
|
|
1541
1755
|
"Settings changed; reload and retry": "settingsChanged",
|
|
@@ -1546,7 +1760,6 @@ const LOCAL_ERRORS = {
|
|
|
1546
1760
|
"Workspace Write with native approval is unavailable": "workspaceUnavailable",
|
|
1547
1761
|
"Unknown permission preset": "unknownPreset",
|
|
1548
1762
|
"Safe Auto control unloaded": "controlUnloaded",
|
|
1549
|
-
"Browser confirmation required": "browserRequired",
|
|
1550
1763
|
"Unknown Safe Auto model setting": "unknownSetting",
|
|
1551
1764
|
"invalid reviewer provider/model identifier": "invalidModel",
|
|
1552
1765
|
"provider and model must be configured together": "modelPair",
|
|
@@ -1555,11 +1768,16 @@ const LOCAL_ERRORS = {
|
|
|
1555
1768
|
function errorMessage(error) {
|
|
1556
1769
|
return (typeof error === "string" ? error : error?.message) || TEXT.en.requestFailed;
|
|
1557
1770
|
}
|
|
1771
|
+
const INACTIVE_SESSION = "Session is not active; open it and retry";
|
|
1772
|
+
function shownPermissionError(error) {
|
|
1773
|
+
const message = errorMessage(error);
|
|
1774
|
+
return message === INACTIVE_SESSION ? "" : message;
|
|
1775
|
+
}
|
|
1558
1776
|
function errorText(t, error) {
|
|
1559
1777
|
const message = typeof error === "string" ? error : error?.message;
|
|
1560
1778
|
const key = Object.hasOwn(LOCAL_ERRORS, message) ? LOCAL_ERRORS[message] : void 0;
|
|
1561
1779
|
const unknownField = /^unknown config field: (.+)$/.exec(message || "");
|
|
1562
|
-
const invalidField = /^(?:(enabled
|
|
1780
|
+
const invalidField = /^(?:(enabled) must be a boolean|(provider|model|reasoningEffort|reviewerPrompt) must be a string|invalid (timeoutMs|outputTokens|maxTranscriptBytes))$/.exec(message || "");
|
|
1563
1781
|
const text = key ? t(key) : unknownField ? t("unknownConfigField", unknownField[1]) : invalidField ? t("invalidConfigField", invalidField[1] || invalidField[2] || invalidField[3]) : ["config must be an object", "invalid configuration"].includes(message) ? t("invalidConfig") : message || t("requestFailed");
|
|
1564
1782
|
return error?.context ? t(error.context, text) : text;
|
|
1565
1783
|
}
|
|
@@ -1603,120 +1821,6 @@ function createRequestScope() {
|
|
|
1603
1821
|
}
|
|
1604
1822
|
};
|
|
1605
1823
|
}
|
|
1606
|
-
const monotonicNow = () => globalThis.performance?.now?.() ?? Date.now();
|
|
1607
|
-
function approvalRemaining(request, now = monotonicNow()) {
|
|
1608
|
-
return Math.max(0, request.deadline - now);
|
|
1609
|
-
}
|
|
1610
|
-
/** Per-session serial reads and one-shot writes. A response's server remainder
|
|
1611
|
-
* is anchored before the round trip so network delay cannot extend the prompt. */
|
|
1612
|
-
function createApprovalController({ api, sessionId, onState, now = monotonicNow, schedule = setTimeout, cancel = clearTimeout, intervalMs = 1e3, readGate = createPluginReadGate() }) {
|
|
1613
|
-
let active = true, reading = false, writing = false, generation = 0, readSequence = 0, timer;
|
|
1614
|
-
let state = {
|
|
1615
|
-
requests: [],
|
|
1616
|
-
busy: false,
|
|
1617
|
-
error: ""
|
|
1618
|
-
};
|
|
1619
|
-
const publish = (next) => {
|
|
1620
|
-
state = {
|
|
1621
|
-
...state,
|
|
1622
|
-
...next
|
|
1623
|
-
};
|
|
1624
|
-
if (active) onState(state);
|
|
1625
|
-
};
|
|
1626
|
-
const arm = () => {
|
|
1627
|
-
if (active) {
|
|
1628
|
-
cancel(timer);
|
|
1629
|
-
timer = schedule(() => {
|
|
1630
|
-
refresh();
|
|
1631
|
-
}, intervalMs);
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
async function refresh(manual = false) {
|
|
1635
|
-
if (!active || reading || writing) return false;
|
|
1636
|
-
if (manual) {
|
|
1637
|
-
generation++;
|
|
1638
|
-
readGate.reset();
|
|
1639
|
-
}
|
|
1640
|
-
if (!readGate.canRead()) {
|
|
1641
|
-
arm();
|
|
1642
|
-
return false;
|
|
1643
|
-
}
|
|
1644
|
-
cancel(timer);
|
|
1645
|
-
reading = true;
|
|
1646
|
-
const ticket = generation, readId = ++readSequence, started = now();
|
|
1647
|
-
try {
|
|
1648
|
-
const value = await readGate.run(() => api.getApprovals(sessionId));
|
|
1649
|
-
if (active && ticket === generation) {
|
|
1650
|
-
const previous = new Map(state.requests.map((request) => [request.id, request.deadline]));
|
|
1651
|
-
const requests = (value?.requests || []).map((request) => ({
|
|
1652
|
-
...request,
|
|
1653
|
-
deadline: Math.min(previous.get(request.id) ?? Infinity, started + Math.max(0, Number(request.remainingMs) || 0))
|
|
1654
|
-
})).filter((request) => approvalRemaining(request, now()) > 0);
|
|
1655
|
-
publish({
|
|
1656
|
-
requests,
|
|
1657
|
-
error: ""
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
} catch (error) {
|
|
1661
|
-
if (active && ticket === generation) publish({ error: errorMessage(error) });
|
|
1662
|
-
} finally {
|
|
1663
|
-
if (readId === readSequence) {
|
|
1664
|
-
reading = false;
|
|
1665
|
-
arm();
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
return true;
|
|
1669
|
-
}
|
|
1670
|
-
return {
|
|
1671
|
-
refresh,
|
|
1672
|
-
reconnect() {
|
|
1673
|
-
generation++;
|
|
1674
|
-
readSequence++;
|
|
1675
|
-
readGate.reset();
|
|
1676
|
-
reading = false;
|
|
1677
|
-
if (!writing) refresh();
|
|
1678
|
-
},
|
|
1679
|
-
dispose() {
|
|
1680
|
-
active = false;
|
|
1681
|
-
generation++;
|
|
1682
|
-
readGate.reset();
|
|
1683
|
-
cancel(timer);
|
|
1684
|
-
},
|
|
1685
|
-
async answer(requestId, outcome) {
|
|
1686
|
-
const request = state.requests.find((item) => item.id === requestId);
|
|
1687
|
-
if (!active || writing || !request || approvalRemaining(request, now()) <= 0) return false;
|
|
1688
|
-
writing = true;
|
|
1689
|
-
generation++;
|
|
1690
|
-
cancel(timer);
|
|
1691
|
-
publish({
|
|
1692
|
-
busy: true,
|
|
1693
|
-
error: ""
|
|
1694
|
-
});
|
|
1695
|
-
readGate.reset();
|
|
1696
|
-
try {
|
|
1697
|
-
if ((await api.answerApproval({
|
|
1698
|
-
sessionId,
|
|
1699
|
-
requestId,
|
|
1700
|
-
outcome
|
|
1701
|
-
}))?.accepted !== true) throw new Error("Confirmation is no longer available");
|
|
1702
|
-
if (active) publish({ requests: state.requests.filter((item) => item.id !== requestId) });
|
|
1703
|
-
} catch (error) {
|
|
1704
|
-
if (active) publish({
|
|
1705
|
-
requests: state.requests.filter((item) => item.id !== requestId),
|
|
1706
|
-
error: errorMessage(error)
|
|
1707
|
-
});
|
|
1708
|
-
} finally {
|
|
1709
|
-
writing = false;
|
|
1710
|
-
if (active) {
|
|
1711
|
-
publish({ busy: false });
|
|
1712
|
-
if (!reading) refresh();
|
|
1713
|
-
else arm();
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
return true;
|
|
1717
|
-
}
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1720
1824
|
function reviewerMenuState(values, catalog) {
|
|
1721
1825
|
const provider = values.provider || "", model = values.model || "";
|
|
1722
1826
|
const follow = !provider && !model;
|
|
@@ -1765,52 +1869,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
|
|
|
1765
1869
|
/* A fieldset is the settings group and takes the contract's card surface; its
|
|
1766
1870
|
* legend keeps the UA inline padding from indenting the group title. */
|
|
1767
1871
|
.dsh-safe-auto legend { padding: 0; }
|
|
1768
|
-
/* The primitives ship no textarea, so it keeps the platform element at the
|
|
1769
|
-
* official field geometry, exactly as dsh-ui-select does for a native select. */
|
|
1770
|
-
.dsh-safe-auto-input {
|
|
1771
|
-
box-sizing: border-box;
|
|
1772
|
-
width: 100%;
|
|
1773
|
-
min-width: 0;
|
|
1774
|
-
padding: 8px 12px;
|
|
1775
|
-
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1776
|
-
border-radius: var(--dsw-radius-md);
|
|
1777
|
-
background: var(--dsw-alias-bg-layer-3);
|
|
1778
|
-
font: inherit;
|
|
1779
|
-
font-size: 13px;
|
|
1780
|
-
line-height: 20px;
|
|
1781
|
-
color: var(--dsw-alias-label-primary);
|
|
1782
|
-
resize: vertical;
|
|
1783
|
-
}
|
|
1784
|
-
.dsh-safe-auto-input:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
1785
|
-
.dsh-safe-auto-approval-dialog {
|
|
1786
|
-
display: flex;
|
|
1787
|
-
flex-direction: column;
|
|
1788
|
-
max-height: 100%;
|
|
1789
|
-
max-width: 100%;
|
|
1790
|
-
}
|
|
1791
|
-
/* The host card sizes every modal to a 380px column; an approval shows whole
|
|
1792
|
-
* commands and file contents, so it claims a wider one. The doubled class
|
|
1793
|
-
* keeps this rule ahead of the card's own width regardless of style order. */
|
|
1794
|
-
.dsh-safe-auto.dsh-safe-auto-approval-dialog { width: min(640px, 100%); }
|
|
1795
|
-
.dsh-safe-auto-approval-content {
|
|
1796
|
-
flex: 1 1 auto;
|
|
1797
|
-
min-height: 0;
|
|
1798
|
-
min-width: 0;
|
|
1799
|
-
overflow: auto;
|
|
1800
|
-
}
|
|
1801
|
-
.dsh-safe-auto-approval-pre {
|
|
1802
|
-
max-height: 35vh;
|
|
1803
|
-
overflow: auto;
|
|
1804
|
-
white-space: pre-wrap;
|
|
1805
|
-
overflow-wrap: anywhere;
|
|
1806
|
-
margin: 0;
|
|
1807
|
-
padding: 8px 12px;
|
|
1808
|
-
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1809
|
-
border-radius: var(--dsw-radius-md);
|
|
1810
|
-
background: var(--dsw-alias-bg-layer-3);
|
|
1811
|
-
color: var(--dsw-alias-label-primary);
|
|
1812
|
-
font-size: 13px;
|
|
1813
|
-
}
|
|
1814
1872
|
`;
|
|
1815
1873
|
function Style() {
|
|
1816
1874
|
return h("style", null, css);
|
|
@@ -1821,47 +1879,6 @@ function ErrorText({ error, t }) {
|
|
|
1821
1879
|
className: "dsh-ui-error dsh-ui-wrap"
|
|
1822
1880
|
}, errorText(t, error)) : null;
|
|
1823
1881
|
}
|
|
1824
|
-
/** Values are rendered as plain text, with no Markdown or HTML interpretation. */
|
|
1825
|
-
function ApprovalDialog({ request, pendingCount = 1, remainingMs, busy = false, error = "", onAnswer, locale }) {
|
|
1826
|
-
const t = useText(locale);
|
|
1827
|
-
if (!request) return null;
|
|
1828
|
-
const expired = remainingMs <= 0;
|
|
1829
|
-
const action = request.action || {};
|
|
1830
|
-
const fields = Object.entries({
|
|
1831
|
-
tool: action.tool,
|
|
1832
|
-
cwd: action.cwd,
|
|
1833
|
-
...action.arguments
|
|
1834
|
-
});
|
|
1835
|
-
return h(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1836
|
-
open: true,
|
|
1837
|
-
title: t("approvalTitle"),
|
|
1838
|
-
closeLabel: t("approvalDeny"),
|
|
1839
|
-
onClose: () => {
|
|
1840
|
-
if (!busy && !expired) onAnswer(request.id, "deny");
|
|
1841
|
-
},
|
|
1842
|
-
className: "dsh-safe-auto dsh-safe-auto-approval-dialog",
|
|
1843
|
-
contentClassName: "dsh-safe-auto-approval-content",
|
|
1844
|
-
footer: h("div", { className: "dsh-safe-auto dsh-ui-actions" }, h(Style), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1845
|
-
variant: "ghost",
|
|
1846
|
-
disabled: busy || expired,
|
|
1847
|
-
"data-modal-autofocus": true,
|
|
1848
|
-
onClick: () => onAnswer(request.id, "deny")
|
|
1849
|
-
}, t("approvalDeny")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1850
|
-
variant: "primary",
|
|
1851
|
-
disabled: busy || expired,
|
|
1852
|
-
onClick: () => onAnswer(request.id, "allow")
|
|
1853
|
-
}, t("approvalAllow")))
|
|
1854
|
-
}, h("div", { className: "dsh-safe-auto dsh-ui-stack" }, h(Style), h("p", {
|
|
1855
|
-
className: "dsh-ui-meta",
|
|
1856
|
-
role: "status"
|
|
1857
|
-
}, t("approvalPending", pendingCount), " · ", expired ? t("approvalExpired") : t("approvalCountdown", Math.ceil(remainingMs / 1e3))), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("approvalEffect")), fields.map(([field, value]) => h("div", {
|
|
1858
|
-
key: field,
|
|
1859
|
-
className: "dsh-ui-field"
|
|
1860
|
-
}, h("span", { className: "dsh-ui-meta" }, field), h("pre", { className: "dsh-safe-auto-approval-pre" }, typeof value === "string" ? value : JSON.stringify(value, null, 2))))), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("approvalReview")), h("pre", { className: "dsh-safe-auto-approval-pre" }, JSON.stringify(request.review || {}, null, 2))), h(ErrorText, {
|
|
1861
|
-
error,
|
|
1862
|
-
t
|
|
1863
|
-
})));
|
|
1864
|
-
}
|
|
1865
1882
|
/** Composer permission control via the replacement slot: native presets plus Safe Auto. */
|
|
1866
1883
|
function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null }) {
|
|
1867
1884
|
const t = useText(locale);
|
|
@@ -1871,20 +1888,15 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1871
1888
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1872
1889
|
const scopeRef = (0, react.useRef)(null);
|
|
1873
1890
|
const refreshRef = (0, react.useRef)(null);
|
|
1874
|
-
const approvalsRef = (0, react.useRef)(null);
|
|
1875
|
-
const [approvals, setApprovals] = (0, react.useState)({
|
|
1876
|
-
sessionId,
|
|
1877
|
-
requests: [],
|
|
1878
|
-
busy: false,
|
|
1879
|
-
error: ""
|
|
1880
|
-
});
|
|
1881
|
-
const [approvalNow, setApprovalNow] = (0, react.useState)(monotonicNow);
|
|
1882
1891
|
const refresh = (automatic = false) => scopeRef.current?.run(() => api.getSession(sessionId), {
|
|
1883
1892
|
value: (value) => {
|
|
1884
1893
|
setSnapshot(value);
|
|
1885
1894
|
setError("");
|
|
1886
1895
|
},
|
|
1887
|
-
error: (e) =>
|
|
1896
|
+
error: (e) => {
|
|
1897
|
+
const message = shownPermissionError(e);
|
|
1898
|
+
if (message) setError(message);
|
|
1899
|
+
}
|
|
1888
1900
|
}, false, automatic);
|
|
1889
1901
|
refreshRef.current = refresh;
|
|
1890
1902
|
(0, react.useEffect)(() => {
|
|
@@ -1905,50 +1917,6 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1905
1917
|
globalThis.removeEventListener?.("focus", focus);
|
|
1906
1918
|
};
|
|
1907
1919
|
}, [api, sessionId]);
|
|
1908
|
-
const approvalsEnabled = Boolean(snapshot?.safeAuto);
|
|
1909
|
-
(0, react.useEffect)(() => {
|
|
1910
|
-
setApprovals({
|
|
1911
|
-
sessionId,
|
|
1912
|
-
requests: [],
|
|
1913
|
-
busy: false,
|
|
1914
|
-
error: ""
|
|
1915
|
-
});
|
|
1916
|
-
if (!approvalsEnabled) return void 0;
|
|
1917
|
-
const controller = createApprovalController({
|
|
1918
|
-
api,
|
|
1919
|
-
sessionId,
|
|
1920
|
-
onState: (value) => {
|
|
1921
|
-
setApprovalNow(monotonicNow());
|
|
1922
|
-
setApprovals({
|
|
1923
|
-
...value,
|
|
1924
|
-
sessionId
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
|
-
});
|
|
1928
|
-
approvalsRef.current = controller;
|
|
1929
|
-
controller.refresh();
|
|
1930
|
-
const stopConnection = api.subscribeGeneration?.(() => controller.reconnect());
|
|
1931
|
-
const focus = () => {
|
|
1932
|
-
controller.refresh();
|
|
1933
|
-
};
|
|
1934
|
-
globalThis.addEventListener?.("focus", focus);
|
|
1935
|
-
return () => {
|
|
1936
|
-
controller.dispose();
|
|
1937
|
-
stopConnection?.();
|
|
1938
|
-
approvalsRef.current = null;
|
|
1939
|
-
globalThis.removeEventListener?.("focus", focus);
|
|
1940
|
-
};
|
|
1941
|
-
}, [
|
|
1942
|
-
api,
|
|
1943
|
-
sessionId,
|
|
1944
|
-
approvalsEnabled
|
|
1945
|
-
]);
|
|
1946
|
-
const pending = approvalsEnabled && approvals.sessionId === sessionId ? approvals.requests.filter((request) => approvalRemaining(request, approvalNow) > 0) : [];
|
|
1947
|
-
(0, react.useEffect)(() => {
|
|
1948
|
-
if (!pending.length) return void 0;
|
|
1949
|
-
const timer = setInterval(() => setApprovalNow(monotonicNow()), 250);
|
|
1950
|
-
return () => clearInterval(timer);
|
|
1951
|
-
}, [pending.length]);
|
|
1952
1920
|
const choose = (value) => {
|
|
1953
1921
|
const scope = scopeRef.current;
|
|
1954
1922
|
if (!snapshot || scope?.writing) return;
|
|
@@ -1969,8 +1937,9 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1969
1937
|
setOpen(false);
|
|
1970
1938
|
},
|
|
1971
1939
|
error: (e) => {
|
|
1972
|
-
|
|
1973
|
-
|
|
1940
|
+
const message = shownPermissionError(e);
|
|
1941
|
+
if (message) setError({
|
|
1942
|
+
message,
|
|
1974
1943
|
context: "refreshRetry"
|
|
1975
1944
|
});
|
|
1976
1945
|
refreshRef.current();
|
|
@@ -2021,23 +1990,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
2021
1990
|
}), h(ErrorText, {
|
|
2022
1991
|
error,
|
|
2023
1992
|
t
|
|
2024
|
-
})
|
|
2025
|
-
error: approvals.error && {
|
|
2026
|
-
message: approvals.error,
|
|
2027
|
-
context: "approvalRefresh"
|
|
2028
|
-
},
|
|
2029
|
-
t
|
|
2030
|
-
}), pending.length ? h(ApprovalDialog, {
|
|
2031
|
-
key: pending[0].id,
|
|
2032
|
-
request: pending[0],
|
|
2033
|
-
pendingCount: pending.length,
|
|
2034
|
-
remainingMs: approvalRemaining(pending[0], approvalNow),
|
|
2035
|
-
busy: approvals.busy,
|
|
2036
|
-
onAnswer: (id, outcome) => {
|
|
2037
|
-
approvalsRef.current?.answer(id, outcome);
|
|
2038
|
-
},
|
|
2039
|
-
locale
|
|
2040
|
-
}) : null);
|
|
1993
|
+
}));
|
|
2041
1994
|
}
|
|
2042
1995
|
function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
|
|
2043
1996
|
const t = useText(locale);
|
|
@@ -2098,9 +2051,12 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2098
2051
|
const t = useText(locale);
|
|
2099
2052
|
const [settings, setSettings] = (0, react.useState)(initialSettings);
|
|
2100
2053
|
const [values, setValues] = (0, react.useState)(initialSettings?.values || null);
|
|
2101
|
-
const
|
|
2054
|
+
const catalogState = useModelCatalog((0, react.useCallback)(() => api.modelCatalog(), [api]), {
|
|
2055
|
+
initialCatalog: initialCatalog ?? void 0,
|
|
2056
|
+
subscribe: api.subscribeGeneration
|
|
2057
|
+
});
|
|
2058
|
+
const { catalog, refresh: refreshCatalog } = catalogState;
|
|
2102
2059
|
const [error, setError] = (0, react.useState)("");
|
|
2103
|
-
const [catalogError, setCatalogError] = (0, react.useState)("");
|
|
2104
2060
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2105
2061
|
const [saved, setSaved] = (0, react.useState)(false);
|
|
2106
2062
|
const [tab, setTab] = (0, react.useState)(initialTab);
|
|
@@ -2169,11 +2125,6 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2169
2125
|
setSyncError("");
|
|
2170
2126
|
refreshRef.current(false, true);
|
|
2171
2127
|
let alive = true;
|
|
2172
|
-
Promise.resolve().then(() => api.modelCatalog()).then((value) => {
|
|
2173
|
-
if (alive) setCatalog(value);
|
|
2174
|
-
}, (e) => {
|
|
2175
|
-
if (alive) setCatalogError(errorMessage(e));
|
|
2176
|
-
});
|
|
2177
2128
|
const focus = () => {
|
|
2178
2129
|
if (!dirty.current) refreshRef.current(false, true);
|
|
2179
2130
|
};
|
|
@@ -2186,6 +2137,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2186
2137
|
if (!alive) return;
|
|
2187
2138
|
setSyncError("");
|
|
2188
2139
|
external.current = true;
|
|
2140
|
+
refreshCatalog();
|
|
2189
2141
|
if (dirty.current && !scope.writing) setExternalChange(true);
|
|
2190
2142
|
refreshRef.current(false, true);
|
|
2191
2143
|
}, (message) => {
|
|
@@ -2199,7 +2151,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2199
2151
|
stopSettings?.();
|
|
2200
2152
|
globalThis.removeEventListener?.("focus", focus);
|
|
2201
2153
|
};
|
|
2202
|
-
}, [api]);
|
|
2154
|
+
}, [api, refreshCatalog]);
|
|
2203
2155
|
const change = (next) => {
|
|
2204
2156
|
dirty.current = true;
|
|
2205
2157
|
setSaved(false);
|
|
@@ -2275,7 +2227,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2275
2227
|
key: "prompt",
|
|
2276
2228
|
className: "dsh-ui-field"
|
|
2277
2229
|
}, h("span", { className: "dsh-ui-label" }, t("prompt")), h("textarea", {
|
|
2278
|
-
className: "dsh-
|
|
2230
|
+
className: "dsh-ui-textarea",
|
|
2279
2231
|
rows: 6,
|
|
2280
2232
|
maxLength: 4096,
|
|
2281
2233
|
disabled: busy,
|
|
@@ -2291,20 +2243,13 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2291
2243
|
id: "sa-prompt-hint",
|
|
2292
2244
|
className: "dsh-ui-help"
|
|
2293
2245
|
}, t("promptHint", (values.reviewerPrompt || "").length))
|
|
2294
|
-
]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null,
|
|
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, {
|
|
2246
|
+
]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [h("p", {
|
|
2247
|
+
key: "line",
|
|
2248
|
+
className: "dsh-ui-meta dsh-ui-wrap"
|
|
2249
|
+
}, t("limitsLine", settings.policyLimits)), h("p", {
|
|
2250
|
+
key: "help",
|
|
2251
|
+
className: "dsh-ui-help"
|
|
2252
|
+
}, t("limitsHelp"))]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2308
2253
|
variant: "primary",
|
|
2309
2254
|
disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
|
|
2310
2255
|
onClick: save
|
|
@@ -2315,12 +2260,9 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2315
2260
|
}, t("refreshDiscard")))), h(ErrorText, {
|
|
2316
2261
|
error,
|
|
2317
2262
|
t
|
|
2318
|
-
}), h(
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
context: "catalogFailed"
|
|
2322
|
-
},
|
|
2323
|
-
t
|
|
2263
|
+
}), h(ModelCatalogStatus, {
|
|
2264
|
+
state: catalogState,
|
|
2265
|
+
locale: localeLanguage(locale)
|
|
2324
2266
|
}), externalChange && h("p", {
|
|
2325
2267
|
role: "alert",
|
|
2326
2268
|
className: "dsh-ui-error"
|
|
@@ -2333,6 +2275,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2333
2275
|
}, t("saved")));
|
|
2334
2276
|
}
|
|
2335
2277
|
function apply(ctx) {
|
|
2278
|
+
mountModelSettingsChanges(ctx, BUNDLE);
|
|
2336
2279
|
const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
|
|
2337
2280
|
const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
|
|
2338
2281
|
const api = Object.freeze({
|
|
@@ -2340,11 +2283,9 @@ function apply(ctx) {
|
|
|
2340
2283
|
subscribeGeneration: (listener) => ctx.connection.generation?.subscribe(listener) ?? (() => {}),
|
|
2341
2284
|
getSettings: () => call("settings.get", {}),
|
|
2342
2285
|
saveSettings: (payload) => call("settings.save", payload),
|
|
2343
|
-
modelCatalog: () => ctx
|
|
2286
|
+
modelCatalog: () => readModelCatalog(ctx),
|
|
2344
2287
|
getSession: (sessionId) => call("session.get", { sessionId }),
|
|
2345
|
-
selectSession: (payload) => call("session.select", payload)
|
|
2346
|
-
getApprovals: (sessionId) => call("approval.list", { sessionId }),
|
|
2347
|
-
answerApproval: (payload) => call("approval.answer", payload)
|
|
2288
|
+
selectSession: (payload) => call("session.select", payload)
|
|
2348
2289
|
});
|
|
2349
2290
|
ctx.slots.inject("conversation.input.permission", () => ctx.slots.register({
|
|
2350
2291
|
name: "conversation.input.permission",
|
|
@@ -2364,15 +2305,12 @@ function apply(ctx) {
|
|
|
2364
2305
|
}, SettingsPanel));
|
|
2365
2306
|
}
|
|
2366
2307
|
//#endregion
|
|
2367
|
-
exports.ApprovalDialog = ApprovalDialog;
|
|
2368
2308
|
exports.BUNDLE = BUNDLE;
|
|
2369
2309
|
exports.CHANNEL = CHANNEL;
|
|
2370
2310
|
exports.PermissionMenu = PermissionMenu;
|
|
2371
2311
|
exports.ReviewerFields = ReviewerFields;
|
|
2372
2312
|
exports.SettingsPanel = SettingsPanel;
|
|
2373
2313
|
exports.apply = apply;
|
|
2374
|
-
exports.approvalRemaining = approvalRemaining;
|
|
2375
|
-
exports.createApprovalController = createApprovalController;
|
|
2376
2314
|
exports.createRequestScope = createRequestScope;
|
|
2377
2315
|
exports.errorText = errorText;
|
|
2378
2316
|
exports.inject = inject;
|