@klarkxy/dsh-safe-auto 0.1.0-rc.6 → 0.2.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 +80 -130
- package/README.zh-CN.md +82 -119
- package/docs/ADR-0001.md +1 -1
- package/docs/ADR-0002.md +1 -1
- package/docs/ADR-0003.md +1 -1
- package/docs/ADR-0004.md +33 -0
- package/docs/ADR-0005.md +31 -0
- package/docs/ADR-0006.md +58 -0
- package/dsh.plugin.json +9 -4
- package/lib/client.js +1738 -494
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +18 -10
- package/src/approval-review.js +24 -2
- package/src/config.js +12 -35
- package/src/control.js +29 -17
- package/src/credential-paths.js +29 -0
- package/src/credential-reads.js +227 -0
- package/src/credential-text.js +361 -0
- package/src/execution-evidence.js +115 -0
- package/src/gate.js +37 -31
- package/src/host-runtime.js +159 -0
- package/src/human-approval.js +64 -0
- package/src/index.js +175 -203
- package/src/model-route.js +10 -29
- package/src/policy.js +39 -78
- package/src/review-context.js +35 -0
- package/src/review-paths.js +53 -0
- package/src/reviewer.js +61 -120
- package/src/shell-command.js +547 -0
- package/src/ui-host.js +60 -43
- package/src/escalation.js +0 -118
package/lib/client.js
CHANGED
|
@@ -1,168 +1,7 @@
|
|
|
1
|
-
function __createDshNodeShims() {
|
|
2
|
-
class EventEmitter {
|
|
3
|
-
constructor() {
|
|
4
|
-
this._e = Object.create(null)
|
|
5
|
-
}
|
|
6
|
-
on(type, fn) {
|
|
7
|
-
(this._e[type] ||= []).push(fn)
|
|
8
|
-
return this
|
|
9
|
-
}
|
|
10
|
-
addListener(type, fn) {
|
|
11
|
-
return this.on(type, fn)
|
|
12
|
-
}
|
|
13
|
-
once(type, fn) {
|
|
14
|
-
const wrap = (...args) => {
|
|
15
|
-
this.off(type, wrap)
|
|
16
|
-
fn(...args)
|
|
17
|
-
}
|
|
18
|
-
return this.on(type, wrap)
|
|
19
|
-
}
|
|
20
|
-
off(type, fn) {
|
|
21
|
-
const list = this._e[type]
|
|
22
|
-
if (list) this._e[type] = list.filter((item) => item !== fn)
|
|
23
|
-
return this
|
|
24
|
-
}
|
|
25
|
-
removeListener(type, fn) {
|
|
26
|
-
return this.off(type, fn)
|
|
27
|
-
}
|
|
28
|
-
emit(type, ...args) {
|
|
29
|
-
const list = this._e[type]
|
|
30
|
-
if (!list) return false
|
|
31
|
-
for (const fn of list.slice()) fn(...args)
|
|
32
|
-
return true
|
|
33
|
-
}
|
|
34
|
-
removeAllListeners(type) {
|
|
35
|
-
if (type) delete this._e[type]
|
|
36
|
-
else this._e = Object.create(null)
|
|
37
|
-
return this
|
|
38
|
-
}
|
|
39
|
-
listeners(type) {
|
|
40
|
-
return (this._e[type] || []).slice()
|
|
41
|
-
}
|
|
42
|
-
setMaxListeners() {
|
|
43
|
-
return this
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
class Buffer extends Uint8Array {
|
|
48
|
-
static from(value, encodingOrOffset, length) {
|
|
49
|
-
if (typeof value === 'string') return new Buffer(new TextEncoder().encode(value))
|
|
50
|
-
if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value, encodingOrOffset, length))
|
|
51
|
-
if (ArrayBuffer.isView(value)) return new Buffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
|
|
52
|
-
if (Array.isArray(value) || value && typeof value.length === 'number') return new Buffer(Uint8Array.from(value))
|
|
53
|
-
return new Buffer(0)
|
|
54
|
-
}
|
|
55
|
-
static alloc(size, fill) {
|
|
56
|
-
const buf = new Buffer(size)
|
|
57
|
-
if (fill !== undefined && fill !== 0) {
|
|
58
|
-
if (typeof fill === 'string') {
|
|
59
|
-
const bytes = new TextEncoder().encode(fill)
|
|
60
|
-
for (let i = 0; i < size; i += 1) buf[i] = bytes[i % bytes.length]
|
|
61
|
-
} else buf.fill(fill)
|
|
62
|
-
}
|
|
63
|
-
return buf
|
|
64
|
-
}
|
|
65
|
-
static allocUnsafe(size) {
|
|
66
|
-
return new Buffer(size)
|
|
67
|
-
}
|
|
68
|
-
static allocUnsafeSlow(size) {
|
|
69
|
-
return new Buffer(size)
|
|
70
|
-
}
|
|
71
|
-
static concat(list, totalLength) {
|
|
72
|
-
const length = totalLength ?? list.reduce((sum, item) => sum + item.length, 0)
|
|
73
|
-
const out = new Buffer(length)
|
|
74
|
-
let offset = 0
|
|
75
|
-
for (const item of list) {
|
|
76
|
-
out.set(item, offset)
|
|
77
|
-
offset += item.length
|
|
78
|
-
}
|
|
79
|
-
return out
|
|
80
|
-
}
|
|
81
|
-
static isBuffer(value) {
|
|
82
|
-
return value instanceof Buffer
|
|
83
|
-
}
|
|
84
|
-
static byteLength(value) {
|
|
85
|
-
if (typeof value === 'string') return new TextEncoder().encode(value).length
|
|
86
|
-
return value?.byteLength ?? value?.length ?? 0
|
|
87
|
-
}
|
|
88
|
-
static isEncoding(encoding) {
|
|
89
|
-
return /^(utf8|utf-8|ascii|latin1|binary|hex|base64|base64url)$/i.test(String(encoding || ''))
|
|
90
|
-
}
|
|
91
|
-
toString(encoding) {
|
|
92
|
-
if (encoding === 'hex') return [...this].map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
93
|
-
if (encoding === 'base64') {
|
|
94
|
-
let binary = ''
|
|
95
|
-
this.forEach((byte) => {
|
|
96
|
-
binary += String.fromCharCode(byte)
|
|
97
|
-
})
|
|
98
|
-
return btoa(binary)
|
|
99
|
-
}
|
|
100
|
-
return new TextDecoder().decode(this)
|
|
101
|
-
}
|
|
102
|
-
slice(start, end) {
|
|
103
|
-
return Buffer.from(super.subarray(start, end))
|
|
104
|
-
}
|
|
105
|
-
copy(target, targetStart = 0, start = 0, end = this.length) {
|
|
106
|
-
const sliced = this.subarray(start, end)
|
|
107
|
-
target.set(sliced, targetStart)
|
|
108
|
-
return sliced.length
|
|
109
|
-
}
|
|
110
|
-
write(string, offset = 0) {
|
|
111
|
-
const bytes = new TextEncoder().encode(string)
|
|
112
|
-
const written = Math.min(bytes.length, this.length - offset)
|
|
113
|
-
this.set(bytes.subarray(0, written), offset)
|
|
114
|
-
return written
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function inherits(ctor, superCtor) {
|
|
119
|
-
ctor.super_ = superCtor
|
|
120
|
-
ctor.prototype = Object.create(superCtor.prototype, {
|
|
121
|
-
constructor: { value: ctor, writable: true, configurable: true },
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!globalThis.Buffer) globalThis.Buffer = Buffer
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
buffer: { Buffer, SlowBuffer: Buffer, INSPECT_MAX_BYTES: 50, kMaxLength: 0x7fffffff },
|
|
129
|
-
stream: {
|
|
130
|
-
Stream: EventEmitter,
|
|
131
|
-
Readable: EventEmitter,
|
|
132
|
-
Writable: EventEmitter,
|
|
133
|
-
Duplex: EventEmitter,
|
|
134
|
-
Transform: EventEmitter,
|
|
135
|
-
PassThrough: EventEmitter,
|
|
136
|
-
},
|
|
137
|
-
events: { EventEmitter },
|
|
138
|
-
util: {
|
|
139
|
-
deprecate(fn) {
|
|
140
|
-
return fn
|
|
141
|
-
},
|
|
142
|
-
inherits,
|
|
143
|
-
inspect() {
|
|
144
|
-
return ''
|
|
145
|
-
},
|
|
146
|
-
format(value) {
|
|
147
|
-
return String(value)
|
|
148
|
-
},
|
|
149
|
-
types: { isBuffer: Buffer.isBuffer },
|
|
150
|
-
},
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
1
|
window.__ModuleLoader__.load({
|
|
155
2
|
id: "@klarkxy/dsh-safe-auto",
|
|
156
3
|
factory: (dshRequire) => {
|
|
157
|
-
var
|
|
158
|
-
var process = globalThis.process || { env: {}, nextTick: function (fn) { var args = [].slice.call(arguments, 1); queueMicrotask(function () { fn.apply(null, args); }); } };
|
|
159
|
-
var require = function (id) {
|
|
160
|
-
if (id === 'buffer' || id === 'stream' || id === 'util' || id === 'events') {
|
|
161
|
-
if (!__nodeShims) __nodeShims = __createDshNodeShims();
|
|
162
|
-
return __nodeShims[id];
|
|
163
|
-
}
|
|
164
|
-
return dshRequire(id);
|
|
165
|
-
};
|
|
4
|
+
var require = dshRequire;
|
|
166
5
|
var module = { exports: {} };
|
|
167
6
|
var exports = module.exports;
|
|
168
7
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
@@ -191,6 +30,763 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
191
30
|
let react = require("react");
|
|
192
31
|
react = __toESM(react, 1);
|
|
193
32
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
33
|
+
let react_dom = require("react-dom");
|
|
34
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
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
|
+
/**
|
|
77
|
+
* Host model-catalog parsing shared by plugin settings UIs and model hubs.
|
|
78
|
+
* Pure and browser-safe: no React, no host imports. The catalog envelope is
|
|
79
|
+
* structural so hosts may add fields without breaking consumers.
|
|
80
|
+
*/
|
|
81
|
+
function asRecord(value) {
|
|
82
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
83
|
+
}
|
|
84
|
+
function asText(value) {
|
|
85
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
86
|
+
}
|
|
87
|
+
function parseEffort(value) {
|
|
88
|
+
const row = asRecord(value);
|
|
89
|
+
const id = row ? asText(row.id) : void 0;
|
|
90
|
+
if (!id) return void 0;
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
name: row ? asText(row.name) ?? id : id
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Parse the host session catalog into flat choices, keeping a saved route selectable. */
|
|
97
|
+
function parseModelMenuChoices(value, bound) {
|
|
98
|
+
const envelope = asRecord(value);
|
|
99
|
+
const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
|
|
100
|
+
const groups = Array.isArray(root.groups) ? root.groups : [];
|
|
101
|
+
const choices = [];
|
|
102
|
+
for (const group of groups) {
|
|
103
|
+
const groupRow = asRecord(group);
|
|
104
|
+
const provider = groupRow ? asText(groupRow.id) : void 0;
|
|
105
|
+
if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
|
|
106
|
+
const providerName = asText(groupRow.name) ?? provider;
|
|
107
|
+
for (const entry of groupRow.models) {
|
|
108
|
+
const modelRow = asRecord(entry);
|
|
109
|
+
const model = modelRow ? asText(modelRow.id) : void 0;
|
|
110
|
+
if (!model || !modelRow) continue;
|
|
111
|
+
const name = asText(modelRow.name) ?? model;
|
|
112
|
+
const reasoning = asRecord(modelRow.reasoning);
|
|
113
|
+
const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
|
|
114
|
+
const parsed = parseEffort(effort);
|
|
115
|
+
return parsed ? [parsed] : [];
|
|
116
|
+
}) : [];
|
|
117
|
+
choices.push({
|
|
118
|
+
provider,
|
|
119
|
+
model,
|
|
120
|
+
label: `${providerName} / ${name}`,
|
|
121
|
+
efforts
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
126
|
+
provider: bound.provider,
|
|
127
|
+
model: bound.model,
|
|
128
|
+
label: `${bound.provider} / ${bound.model}`,
|
|
129
|
+
efforts: []
|
|
130
|
+
});
|
|
131
|
+
return choices;
|
|
132
|
+
}
|
|
133
|
+
/** Reasoning-effort options for one choice; the saved value stays selectable. */
|
|
134
|
+
function modelMenuEffortOptions(choice, current) {
|
|
135
|
+
const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
|
|
136
|
+
if (current && !listed.some((item) => item.id === current)) listed.push({
|
|
137
|
+
id: current,
|
|
138
|
+
name: current
|
|
139
|
+
});
|
|
140
|
+
return listed;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ../dsh-model-route/lib/ui.js
|
|
144
|
+
/**
|
|
145
|
+
* ModelMenu: the standard editor of one model-route field — the visible half
|
|
146
|
+
* of the x-model-route contract. A feature plugin's settings page renders it
|
|
147
|
+
* for its own marked field; the model hub renders one per detected field.
|
|
148
|
+
*
|
|
149
|
+
* It is a plugin-side copy of the host's composer model picker
|
|
150
|
+
* (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
|
|
151
|
+
* primitives and the published bundle's behavior: one card that opens on the
|
|
152
|
+
* root pane (Model / Effort cells), drills into the provider-grouped,
|
|
153
|
+
* searchable model list or the effort list, and Escapes back before closing.
|
|
154
|
+
* Sticky group headings, subsequence ranking, highlight walking and focus
|
|
155
|
+
* hand-back all match the host. SYNC POINT: when the host's ModelSelect or
|
|
156
|
+
* ModelSelect.module.css changes, this copy must be re-transcribed; styles
|
|
157
|
+
* below may only reference --dsw-* theme tokens, never literal colors.
|
|
158
|
+
*
|
|
159
|
+
* It statically imports the primitives, so only browser clients should load
|
|
160
|
+
* this entry; host code and node tests use the core entry (`./index.ts`).
|
|
161
|
+
*/
|
|
162
|
+
/** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
|
|
163
|
+
const MEASURE_STYLE = {
|
|
164
|
+
visibility: "hidden",
|
|
165
|
+
left: 0,
|
|
166
|
+
top: 0
|
|
167
|
+
};
|
|
168
|
+
function ModelMenu(props) {
|
|
169
|
+
const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
170
|
+
const copy = language === "zh" ? {
|
|
171
|
+
model: "模型",
|
|
172
|
+
effort: "思考强度",
|
|
173
|
+
default: "默认",
|
|
174
|
+
search: "搜索模型…",
|
|
175
|
+
empty: "没有匹配的模型",
|
|
176
|
+
clear: "清除搜索"
|
|
177
|
+
} : {
|
|
178
|
+
model: "Model",
|
|
179
|
+
effort: "Reasoning",
|
|
180
|
+
default: "Default",
|
|
181
|
+
search: "Search models…",
|
|
182
|
+
empty: "No matching models",
|
|
183
|
+
clear: "Clear search"
|
|
184
|
+
};
|
|
185
|
+
const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, className, listClassName } = props;
|
|
186
|
+
const rootRef = (0, react.useRef)(null);
|
|
187
|
+
const openRef = (0, react.useRef)(open);
|
|
188
|
+
openRef.current = open;
|
|
189
|
+
const pendingOpenRef = (0, react.useRef)(false);
|
|
190
|
+
const pendingRestoreFocus = (0, react.useRef)(false);
|
|
191
|
+
const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
|
|
192
|
+
const menuRef = (0, react.useRef)(null);
|
|
193
|
+
const groupsRef = (0, react.useRef)(null);
|
|
194
|
+
const searchRef = (0, react.useRef)(null);
|
|
195
|
+
const itemRefs = (0, react.useRef)([]);
|
|
196
|
+
const paneFocus = (0, react.useRef)(null);
|
|
197
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
198
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
199
|
+
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
200
|
+
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
201
|
+
const id = (0, react.useId)();
|
|
202
|
+
const showSearch = choices.length > 4;
|
|
203
|
+
const effortRows = (0, react.useMemo)(() => [{
|
|
204
|
+
id: "",
|
|
205
|
+
name: defaultEffortLabel
|
|
206
|
+
}, ...efforts.map((effort) => ({
|
|
207
|
+
...effort,
|
|
208
|
+
name: modelMenuEffortLabel(effort, language)
|
|
209
|
+
}))], [
|
|
210
|
+
defaultEffortLabel,
|
|
211
|
+
efforts,
|
|
212
|
+
language
|
|
213
|
+
]);
|
|
214
|
+
const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
|
|
215
|
+
...group,
|
|
216
|
+
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
217
|
+
choice,
|
|
218
|
+
name: modelMenuShortName(choice),
|
|
219
|
+
label: choice.model
|
|
220
|
+
})), showSearch ? query.trim() : "")
|
|
221
|
+
})).filter((group) => group.items.length > 0), [
|
|
222
|
+
choices,
|
|
223
|
+
query,
|
|
224
|
+
showSearch
|
|
225
|
+
]);
|
|
226
|
+
const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
|
|
227
|
+
const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
|
|
228
|
+
const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
|
|
229
|
+
const changeOpen = (next) => {
|
|
230
|
+
if (openRef.current === next) return;
|
|
231
|
+
openRef.current = next;
|
|
232
|
+
onOpenChange(next);
|
|
233
|
+
};
|
|
234
|
+
const requestOpen = () => {
|
|
235
|
+
if (open || pendingOpenRef.current) return;
|
|
236
|
+
pendingOpenRef.current = true;
|
|
237
|
+
pendingRestoreFocus.current = false;
|
|
238
|
+
setRequestedOpen(true);
|
|
239
|
+
};
|
|
240
|
+
const close = (restoreFocus = false) => {
|
|
241
|
+
const active = document.activeElement;
|
|
242
|
+
pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
|
|
243
|
+
changeOpen(false);
|
|
244
|
+
};
|
|
245
|
+
const drill = (next) => {
|
|
246
|
+
setQuery("");
|
|
247
|
+
setHighlightedIndex(null);
|
|
248
|
+
paneFocus.current = "drill";
|
|
249
|
+
setPane(next);
|
|
250
|
+
};
|
|
251
|
+
/** Leave a drilled pane for the root one, handing the keyboard back to its cell. */
|
|
252
|
+
const back = (from) => {
|
|
253
|
+
paneFocus.current = from;
|
|
254
|
+
setPane("root");
|
|
255
|
+
};
|
|
256
|
+
const moveFocus = (offset) => {
|
|
257
|
+
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
258
|
+
if (!items.length) return;
|
|
259
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
260
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
261
|
+
};
|
|
262
|
+
(0, react.useLayoutEffect)(() => {
|
|
263
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
264
|
+
const active = document.activeElement;
|
|
265
|
+
if (active !== document.body && !rootRef.current?.contains(active)) {
|
|
266
|
+
pendingRestoreFocus.current = false;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const trigger = rootRef.current?.querySelector("button");
|
|
270
|
+
if (!trigger || trigger.disabled) return;
|
|
271
|
+
trigger.focus();
|
|
272
|
+
if (document.activeElement === trigger) pendingRestoreFocus.current = false;
|
|
273
|
+
});
|
|
274
|
+
(0, react.useEffect)(() => {
|
|
275
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
276
|
+
const cancelOutside = (event) => {
|
|
277
|
+
if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
|
|
278
|
+
};
|
|
279
|
+
document.addEventListener("focusin", cancelOutside);
|
|
280
|
+
document.addEventListener("pointerdown", cancelOutside);
|
|
281
|
+
return () => {
|
|
282
|
+
document.removeEventListener("focusin", cancelOutside);
|
|
283
|
+
document.removeEventListener("pointerdown", cancelOutside);
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
(0, react.useEffect)(() => {
|
|
287
|
+
if (!requestedOpen) return;
|
|
288
|
+
pendingOpenRef.current = false;
|
|
289
|
+
setRequestedOpen(false);
|
|
290
|
+
if (!open) changeOpen(true);
|
|
291
|
+
}, [requestedOpen, open]);
|
|
292
|
+
(0, react.useEffect)(() => {
|
|
293
|
+
if (!open) return;
|
|
294
|
+
setPane("root");
|
|
295
|
+
setQuery("");
|
|
296
|
+
setHighlightedIndex(null);
|
|
297
|
+
}, [open]);
|
|
298
|
+
(0, react.useEffect)(() => {
|
|
299
|
+
if (!open) return;
|
|
300
|
+
const closeOutside = (event) => {
|
|
301
|
+
const target = event.target;
|
|
302
|
+
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
303
|
+
changeOpen(false);
|
|
304
|
+
};
|
|
305
|
+
document.addEventListener("mousedown", closeOutside);
|
|
306
|
+
return () => document.removeEventListener("mousedown", closeOutside);
|
|
307
|
+
}, [open, onOpenChange]);
|
|
308
|
+
(0, react.useEffect)(() => {
|
|
309
|
+
const intent = paneFocus.current;
|
|
310
|
+
if (!open || menuPos === null || intent === null) return;
|
|
311
|
+
if (intent !== "drill" && pane !== "root") return;
|
|
312
|
+
paneFocus.current = null;
|
|
313
|
+
if (intent === "drill") {
|
|
314
|
+
if (pane === "model" && showSearch) {
|
|
315
|
+
searchRef.current?.focus();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
|
|
322
|
+
}, [
|
|
323
|
+
open,
|
|
324
|
+
pane,
|
|
325
|
+
showSearch,
|
|
326
|
+
menuPos
|
|
327
|
+
]);
|
|
328
|
+
(0, react.useEffect)(() => {
|
|
329
|
+
const viewport = groupsRef.current;
|
|
330
|
+
if (viewport === null) return;
|
|
331
|
+
return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
|
|
332
|
+
}, [
|
|
333
|
+
open,
|
|
334
|
+
pane,
|
|
335
|
+
filteredGroups
|
|
336
|
+
]);
|
|
337
|
+
(0, react.useLayoutEffect)(() => {
|
|
338
|
+
if (open && pane === "model" && activeModelIndex >= 0) itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
339
|
+
}, [
|
|
340
|
+
open,
|
|
341
|
+
pane,
|
|
342
|
+
activeModelIndex,
|
|
343
|
+
visibleModels,
|
|
344
|
+
leading.length
|
|
345
|
+
]);
|
|
346
|
+
(0, react.useLayoutEffect)(() => {
|
|
347
|
+
if (!open) {
|
|
348
|
+
setMenuPos(null);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const place = () => {
|
|
352
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
353
|
+
if (!rect) return;
|
|
354
|
+
const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
|
|
355
|
+
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
356
|
+
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
357
|
+
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
358
|
+
setMenuPos({
|
|
359
|
+
left: x,
|
|
360
|
+
top: y
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
place();
|
|
364
|
+
window.addEventListener("scroll", place, true);
|
|
365
|
+
window.addEventListener("resize", place);
|
|
366
|
+
return () => {
|
|
367
|
+
window.removeEventListener("scroll", place, true);
|
|
368
|
+
window.removeEventListener("resize", place);
|
|
369
|
+
};
|
|
370
|
+
}, [
|
|
371
|
+
open,
|
|
372
|
+
pane,
|
|
373
|
+
choices,
|
|
374
|
+
query
|
|
375
|
+
]);
|
|
376
|
+
function onKeyDown(event) {
|
|
377
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
378
|
+
if (!open && [
|
|
379
|
+
"Enter",
|
|
380
|
+
" ",
|
|
381
|
+
"ArrowDown",
|
|
382
|
+
"ArrowUp"
|
|
383
|
+
].includes(event.key)) {
|
|
384
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
385
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
paneFocus.current = "model";
|
|
388
|
+
requestOpen();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (event.key === "Escape" && open) {
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
if (pane !== "root") back(pane);
|
|
394
|
+
else close(true);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!open) return;
|
|
398
|
+
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
if (visibleModels.length > 0) {
|
|
401
|
+
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
402
|
+
searchRef.current?.focus();
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
|
|
407
|
+
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
const highlighted = visibleModels[activeModelIndex];
|
|
410
|
+
if (highlighted) {
|
|
411
|
+
onPick({
|
|
412
|
+
provider: highlighted.provider,
|
|
413
|
+
model: highlighted.model
|
|
414
|
+
});
|
|
415
|
+
close(true);
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (event.key === "Tab") {
|
|
420
|
+
if (event.shiftKey) {
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
if (pane !== "root") back(pane);
|
|
423
|
+
else close(true);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const focused = document.activeElement;
|
|
427
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
428
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
focused.click();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (pane === "model" && showSearch) {
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
setHighlightedIndex(null);
|
|
436
|
+
searchRef.current?.focus();
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function onBlur(event) {
|
|
446
|
+
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) || menuRef.current?.contains(event.relatedTarget))) return;
|
|
447
|
+
if (open) changeOpen(false);
|
|
448
|
+
}
|
|
449
|
+
itemRefs.current = [];
|
|
450
|
+
let itemIndex = 0;
|
|
451
|
+
let modelIndex = 0;
|
|
452
|
+
const itemRef = () => {
|
|
453
|
+
const at = itemIndex++;
|
|
454
|
+
return (node) => {
|
|
455
|
+
itemRefs.current[at] = node;
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
459
|
+
ref: menuRef,
|
|
460
|
+
id: `${id}-menu`,
|
|
461
|
+
className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
|
|
462
|
+
style: menuPos ?? MEASURE_STYLE,
|
|
463
|
+
role: pane === "model" ? "group" : "menu",
|
|
464
|
+
"aria-label": modelLabel,
|
|
465
|
+
lang: language,
|
|
466
|
+
children: [
|
|
467
|
+
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
468
|
+
ref: itemRef(),
|
|
469
|
+
type: "button",
|
|
470
|
+
role: "menuitem",
|
|
471
|
+
className: "dsh-model-menu-cell",
|
|
472
|
+
onClick: () => drill("model"),
|
|
473
|
+
children: [
|
|
474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
475
|
+
className: "dsh-model-menu-cellLabel",
|
|
476
|
+
children: modelLabel
|
|
477
|
+
}),
|
|
478
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
479
|
+
className: "dsh-model-menu-cellValue",
|
|
480
|
+
children: modelValue
|
|
481
|
+
}),
|
|
482
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
483
|
+
]
|
|
484
|
+
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
485
|
+
ref: itemRef(),
|
|
486
|
+
type: "button",
|
|
487
|
+
role: "menuitem",
|
|
488
|
+
className: "dsh-model-menu-cell",
|
|
489
|
+
onClick: () => drill("effort"),
|
|
490
|
+
children: [
|
|
491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
492
|
+
className: "dsh-model-menu-cellLabel",
|
|
493
|
+
children: effortLabel
|
|
494
|
+
}),
|
|
495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
496
|
+
className: "dsh-model-menu-cellValue",
|
|
497
|
+
children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
|
|
498
|
+
}),
|
|
499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
500
|
+
]
|
|
501
|
+
}) : null] }) : null,
|
|
502
|
+
pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
503
|
+
showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
504
|
+
className: "dsh-model-menu-searchRow",
|
|
505
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
506
|
+
ref: searchRef,
|
|
507
|
+
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
508
|
+
type: "text",
|
|
509
|
+
role: "searchbox",
|
|
510
|
+
"aria-label": searchPlaceholder,
|
|
511
|
+
"aria-controls": `${id}-models`,
|
|
512
|
+
"aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
|
|
513
|
+
placeholder: searchPlaceholder,
|
|
514
|
+
value: query,
|
|
515
|
+
onChange: (event) => {
|
|
516
|
+
setQuery(event.target.value);
|
|
517
|
+
setHighlightedIndex(0);
|
|
518
|
+
}
|
|
519
|
+
}), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
520
|
+
type: "button",
|
|
521
|
+
className: "dsh-model-menu-searchClear",
|
|
522
|
+
"aria-label": clearSearchLabel,
|
|
523
|
+
onClick: () => {
|
|
524
|
+
setQuery("");
|
|
525
|
+
setHighlightedIndex(null);
|
|
526
|
+
searchRef.current?.focus();
|
|
527
|
+
},
|
|
528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
|
|
529
|
+
}) : null]
|
|
530
|
+
}) : null,
|
|
531
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
532
|
+
ref: groupsRef,
|
|
533
|
+
id: `${id}-models`,
|
|
534
|
+
className: "dsh-model-menu-groups scrollable",
|
|
535
|
+
role: "menu",
|
|
536
|
+
"aria-label": modelLabel,
|
|
537
|
+
hidden: filteredGroups.length === 0,
|
|
538
|
+
children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
539
|
+
className: "dsh-model-menu-leading",
|
|
540
|
+
children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
541
|
+
ref: itemRef(),
|
|
542
|
+
type: "button",
|
|
543
|
+
role: "menuitemradio",
|
|
544
|
+
"aria-checked": item.selected === true,
|
|
545
|
+
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
onPickLeading?.(item.id);
|
|
548
|
+
close(true);
|
|
549
|
+
},
|
|
550
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
551
|
+
className: "dsh-model-menu-optionCopy",
|
|
552
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
553
|
+
className: "dsh-model-menu-modelName",
|
|
554
|
+
children: item.label
|
|
555
|
+
})
|
|
556
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
557
|
+
className: "dsh-model-menu-check",
|
|
558
|
+
children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
559
|
+
})]
|
|
560
|
+
}, item.id))
|
|
561
|
+
}) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
|
|
562
|
+
label: group.name,
|
|
563
|
+
children: group.items.map((item) => {
|
|
564
|
+
const index = modelIndex++;
|
|
565
|
+
const choice = item.choice;
|
|
566
|
+
const checked = selected?.provider === choice.provider && selected.model === choice.model;
|
|
567
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
568
|
+
ref: itemRef(),
|
|
569
|
+
type: "button",
|
|
570
|
+
role: "menuitemradio",
|
|
571
|
+
"aria-checked": checked,
|
|
572
|
+
id: `${id}-model-${index}`,
|
|
573
|
+
tabIndex: showSearch ? -1 : 0,
|
|
574
|
+
onFocus: () => setHighlightedIndex(index),
|
|
575
|
+
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
576
|
+
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
577
|
+
onMouseMove: index === activeModelIndex ? void 0 : () => {
|
|
578
|
+
if (showSearch) setHighlightedIndex(index);
|
|
579
|
+
else itemRefs.current[leading.length + index]?.focus();
|
|
580
|
+
},
|
|
581
|
+
title: choice.label,
|
|
582
|
+
onClick: () => {
|
|
583
|
+
onPick({
|
|
584
|
+
provider: choice.provider,
|
|
585
|
+
model: choice.model
|
|
586
|
+
});
|
|
587
|
+
close(true);
|
|
588
|
+
},
|
|
589
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
590
|
+
className: "dsh-model-menu-optionCopy",
|
|
591
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
592
|
+
className: "dsh-model-menu-modelName",
|
|
593
|
+
children: modelMenuShortName(choice)
|
|
594
|
+
})
|
|
595
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
596
|
+
className: "dsh-model-menu-check",
|
|
597
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
598
|
+
})]
|
|
599
|
+
}, `${choice.provider}/${choice.model}`);
|
|
600
|
+
})
|
|
601
|
+
}, group.provider))]
|
|
602
|
+
}),
|
|
603
|
+
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
604
|
+
className: "dsh-model-menu-empty",
|
|
605
|
+
role: "status",
|
|
606
|
+
children: emptyLabel
|
|
607
|
+
}) : null
|
|
608
|
+
] }) : null,
|
|
609
|
+
pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
|
|
610
|
+
const checked = (selectedEffort ?? "") === row.id;
|
|
611
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
612
|
+
ref: itemRef(),
|
|
613
|
+
type: "button",
|
|
614
|
+
role: "menuitemradio",
|
|
615
|
+
"aria-checked": checked,
|
|
616
|
+
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
617
|
+
onClick: () => {
|
|
618
|
+
onPickEffort?.(row.id || void 0);
|
|
619
|
+
close(true);
|
|
620
|
+
},
|
|
621
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
622
|
+
className: "dsh-model-menu-optionCopy",
|
|
623
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
624
|
+
className: "dsh-model-menu-modelName",
|
|
625
|
+
children: row.name
|
|
626
|
+
})
|
|
627
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
628
|
+
className: "dsh-model-menu-check",
|
|
629
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
630
|
+
})]
|
|
631
|
+
}, row.id || "__default");
|
|
632
|
+
}) }) : null
|
|
633
|
+
]
|
|
634
|
+
}), document.body) : null;
|
|
635
|
+
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
636
|
+
"aria-haspopup": "menu",
|
|
637
|
+
"aria-expanded": open,
|
|
638
|
+
"aria-controls": open ? `${id}-menu` : void 0
|
|
639
|
+
}) : anchor;
|
|
640
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
641
|
+
ref: rootRef,
|
|
642
|
+
className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
|
|
643
|
+
onKeyDown,
|
|
644
|
+
onBlur,
|
|
645
|
+
onClick: (event) => {
|
|
646
|
+
if (event.defaultPrevented) return;
|
|
647
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
648
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
649
|
+
requestOpen();
|
|
650
|
+
},
|
|
651
|
+
onMouseDown: (event) => {
|
|
652
|
+
if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
|
|
653
|
+
},
|
|
654
|
+
children: [trigger, menu]
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
659
|
+
* published dsh-client-ui-model-selection bundle): card capped at
|
|
660
|
+
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
661
|
+
* 34px root cells at 13/20, a borderless 12px search row with a clear button,
|
|
662
|
+
* sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
|
|
663
|
+
* elevation at portal z-index 1100. The trigger mirrors the composer's 28px
|
|
664
|
+
* model pill, chevron rotating open. MenuSurface paints fill and backdrop.
|
|
665
|
+
* Only --dsw-* theme tokens may appear here — a literal color is a bug.
|
|
666
|
+
*/
|
|
667
|
+
const modelMenuCss = `
|
|
668
|
+
.dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
|
|
669
|
+
.dsh-model-menu-trigger {
|
|
670
|
+
border-radius: var(--dsw-radius-sm);
|
|
671
|
+
min-width: 0;
|
|
672
|
+
max-width: min(360px, 45cqw);
|
|
673
|
+
height: 28px;
|
|
674
|
+
color: var(--dsw-alias-label-secondary);
|
|
675
|
+
cursor: pointer;
|
|
676
|
+
background: none;
|
|
677
|
+
border: none;
|
|
678
|
+
outline: none;
|
|
679
|
+
align-items: center;
|
|
680
|
+
gap: 4px;
|
|
681
|
+
padding: 0 4px 0 8px;
|
|
682
|
+
font-size: 13px;
|
|
683
|
+
font-weight: 400;
|
|
684
|
+
line-height: 20px;
|
|
685
|
+
display: flex;
|
|
686
|
+
}
|
|
687
|
+
.dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
688
|
+
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
689
|
+
.dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
690
|
+
.dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
|
|
691
|
+
.dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
|
|
692
|
+
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
|
|
693
|
+
.dsh-model-menu-chevronOpen { transform: rotate(180deg); }
|
|
694
|
+
.dsh-model-menu-menu {
|
|
695
|
+
z-index: 1100;
|
|
696
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
697
|
+
width: max-content;
|
|
698
|
+
min-width: min(240px, 100vw - 32px);
|
|
699
|
+
max-width: min(420px, 100vw - 32px);
|
|
700
|
+
max-height: min(360px, 100vh - 96px);
|
|
701
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
702
|
+
color: var(--dsw-alias-label-primary);
|
|
703
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
704
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
705
|
+
border: 0;
|
|
706
|
+
flex-direction: column;
|
|
707
|
+
padding: 4px;
|
|
708
|
+
display: flex;
|
|
709
|
+
position: fixed;
|
|
710
|
+
overflow: hidden;
|
|
711
|
+
}
|
|
712
|
+
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
713
|
+
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
714
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
|
|
715
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
716
|
+
.dsh-model-menu-searchWithQuery { padding-right: 34px; }
|
|
717
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
|
|
718
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
|
|
719
|
+
.dsh-model-menu-searchClear {
|
|
720
|
+
width: 24px;
|
|
721
|
+
height: 24px;
|
|
722
|
+
color: var(--dsw-alias-label-secondary);
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
background: none;
|
|
725
|
+
border: none;
|
|
726
|
+
border-radius: 50%;
|
|
727
|
+
justify-content: center;
|
|
728
|
+
align-items: center;
|
|
729
|
+
padding: 0;
|
|
730
|
+
display: inline-flex;
|
|
731
|
+
position: absolute;
|
|
732
|
+
top: 50%;
|
|
733
|
+
right: 4px;
|
|
734
|
+
transform: translateY(-50%);
|
|
735
|
+
}
|
|
736
|
+
.dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
|
|
737
|
+
.dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
|
|
738
|
+
.dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
|
|
739
|
+
.dsh-model-menu-option {
|
|
740
|
+
box-sizing: border-box;
|
|
741
|
+
border-radius: var(--dsw-radius-md);
|
|
742
|
+
width: auto;
|
|
743
|
+
min-width: 100%;
|
|
744
|
+
min-height: 34px;
|
|
745
|
+
color: inherit;
|
|
746
|
+
text-align: left;
|
|
747
|
+
cursor: pointer;
|
|
748
|
+
background: none;
|
|
749
|
+
border: none;
|
|
750
|
+
outline: none;
|
|
751
|
+
align-items: center;
|
|
752
|
+
gap: 6px;
|
|
753
|
+
padding: 5px 7px;
|
|
754
|
+
display: flex;
|
|
755
|
+
}
|
|
756
|
+
.dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
|
|
757
|
+
.dsh-model-menu-option:focus-visible,
|
|
758
|
+
.dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
759
|
+
.dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
760
|
+
.dsh-model-menu-selected { background: none; }
|
|
761
|
+
.dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
762
|
+
.dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
|
|
763
|
+
.dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
|
|
764
|
+
.dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
|
|
765
|
+
.dsh-model-menu-check svg { width: 14px; height: 14px; }
|
|
766
|
+
.dsh-model-menu-cell {
|
|
767
|
+
box-sizing: border-box;
|
|
768
|
+
border-radius: var(--dsw-radius-md);
|
|
769
|
+
width: auto;
|
|
770
|
+
min-width: 100%;
|
|
771
|
+
height: 34px;
|
|
772
|
+
color: var(--dsw-alias-label-primary);
|
|
773
|
+
cursor: pointer;
|
|
774
|
+
text-align: left;
|
|
775
|
+
background: none;
|
|
776
|
+
border: none;
|
|
777
|
+
outline: none;
|
|
778
|
+
align-items: center;
|
|
779
|
+
gap: 6px;
|
|
780
|
+
padding: 0 8px;
|
|
781
|
+
font-size: 13px;
|
|
782
|
+
line-height: 20px;
|
|
783
|
+
display: flex;
|
|
784
|
+
}
|
|
785
|
+
.dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
786
|
+
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
787
|
+
.dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
|
|
788
|
+
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
789
|
+
`;
|
|
194
790
|
Object.values({
|
|
195
791
|
/** Surfaces, from the page up to the topmost nested card. */
|
|
196
792
|
surface: [
|
|
@@ -488,7 +1084,7 @@ ${scope} {
|
|
|
488
1084
|
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
489
1085
|
border-radius: var(--dsw-radius-md);
|
|
490
1086
|
background: var(--dsw-alias-bg-layer-1);
|
|
491
|
-
font-family: var(--ds-font-family-code
|
|
1087
|
+
font-family: var(--ds-font-family-code);
|
|
492
1088
|
font-size: 12px;
|
|
493
1089
|
line-height: 20px;
|
|
494
1090
|
color: var(--dsw-alias-label-secondary);
|
|
@@ -564,6 +1160,188 @@ ${scope} {
|
|
|
564
1160
|
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
|
|
565
1161
|
}
|
|
566
1162
|
//#endregion
|
|
1163
|
+
//#region ../dsh-plugin-kit/lib/client-utils.js
|
|
1164
|
+
/** Fixed pause for failing background reads; manual work is never replayed. */
|
|
1165
|
+
function createPluginReadGate(now = Date.now) {
|
|
1166
|
+
let pausedUntil = 0;
|
|
1167
|
+
let epoch = 0;
|
|
1168
|
+
return {
|
|
1169
|
+
canRead: () => now() >= pausedUntil,
|
|
1170
|
+
reset: () => {
|
|
1171
|
+
epoch++;
|
|
1172
|
+
pausedUntil = 0;
|
|
1173
|
+
},
|
|
1174
|
+
async run(read) {
|
|
1175
|
+
const current = epoch;
|
|
1176
|
+
try {
|
|
1177
|
+
const value = await read();
|
|
1178
|
+
if (current === epoch) pausedUntil = 0;
|
|
1179
|
+
return value;
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
const message = error instanceof Error ? error.message : "";
|
|
1182
|
+
if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
|
|
1183
|
+
throw error;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region ../dsh-plugin-kit/lib/contracts.js
|
|
1190
|
+
/**
|
|
1191
|
+
* Remote contribution for model-settings change notifications.
|
|
1192
|
+
* Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
|
|
1193
|
+
* subscribers degrade silently when that package is not installed.
|
|
1194
|
+
* The stream carries invalidations only — `ready` is never a settings snapshot.
|
|
1195
|
+
*/
|
|
1196
|
+
const MODEL_SETTINGS_CHANGES_REMOTE = {
|
|
1197
|
+
package: "@klarkxy/dsh-model-hub",
|
|
1198
|
+
descriptors: [{
|
|
1199
|
+
id: "modelSettingsChanges/watch",
|
|
1200
|
+
service: "modelSettingsChangesController",
|
|
1201
|
+
namespace: "modelSettingsChanges",
|
|
1202
|
+
method: "watch",
|
|
1203
|
+
mode: "stream",
|
|
1204
|
+
invocation: { kind: "direct" },
|
|
1205
|
+
parameters: [],
|
|
1206
|
+
cancellation: { parameter: "signal" }
|
|
1207
|
+
}]
|
|
1208
|
+
};
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region ../dsh-plugin-kit/lib/model-settings-client.js
|
|
1211
|
+
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
1212
|
+
function parseModelSettingsChangeFrame(value) {
|
|
1213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
|
|
1214
|
+
const frame = value;
|
|
1215
|
+
if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
|
|
1216
|
+
if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
|
|
1217
|
+
kind: "changed",
|
|
1218
|
+
ns: frame.ns,
|
|
1219
|
+
revision: frame.revision
|
|
1220
|
+
};
|
|
1221
|
+
throw new Error("Invalid model settings change frame");
|
|
1222
|
+
}
|
|
1223
|
+
MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
|
|
1224
|
+
...descriptor,
|
|
1225
|
+
result: {
|
|
1226
|
+
mode: "strict",
|
|
1227
|
+
typeSymbol: "ModelSettings.ChangeFrame",
|
|
1228
|
+
create: () => ({ parse: parseModelSettingsChangeFrame })
|
|
1229
|
+
}
|
|
1230
|
+
}));
|
|
1231
|
+
/** Native RemoteStream owns carrier reconnects; no timers or browser transport.
|
|
1232
|
+
* A factory shares one logical stream while at least one subscriber remains. */
|
|
1233
|
+
function createModelSettingsSubscriber(ctx, namespace) {
|
|
1234
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1235
|
+
let injection;
|
|
1236
|
+
let current;
|
|
1237
|
+
let lifecycle = 0;
|
|
1238
|
+
let failureMessage;
|
|
1239
|
+
const report = (message) => {
|
|
1240
|
+
failureMessage = message;
|
|
1241
|
+
for (const listener of [...listeners]) try {
|
|
1242
|
+
listener.error?.(message);
|
|
1243
|
+
} catch {}
|
|
1244
|
+
};
|
|
1245
|
+
const invalidate = () => {
|
|
1246
|
+
for (const listener of [...listeners]) {
|
|
1247
|
+
if (!listeners.has(listener)) continue;
|
|
1248
|
+
try {
|
|
1249
|
+
listener.change();
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
try {
|
|
1252
|
+
listener.error?.(error instanceof Error ? error.message : String(error));
|
|
1253
|
+
} catch {}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
const start = () => {
|
|
1258
|
+
const life = ++lifecycle;
|
|
1259
|
+
injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
|
|
1260
|
+
if (life !== lifecycle || !listeners.size) return;
|
|
1261
|
+
const scope = injected;
|
|
1262
|
+
const stream = scope.remote.$stream({
|
|
1263
|
+
name: "Model settings sync",
|
|
1264
|
+
open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
|
|
1265
|
+
ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
|
|
1266
|
+
});
|
|
1267
|
+
const session = {
|
|
1268
|
+
stream,
|
|
1269
|
+
active: true
|
|
1270
|
+
};
|
|
1271
|
+
current = session;
|
|
1272
|
+
scope.effect(() => () => {
|
|
1273
|
+
session.active = false;
|
|
1274
|
+
if (current === session) {
|
|
1275
|
+
current = void 0;
|
|
1276
|
+
if (life === lifecycle && listeners.size) report("Model settings sync service unavailable");
|
|
1277
|
+
}
|
|
1278
|
+
return stream.dispose();
|
|
1279
|
+
});
|
|
1280
|
+
(async () => {
|
|
1281
|
+
let generation;
|
|
1282
|
+
let ready = false;
|
|
1283
|
+
try {
|
|
1284
|
+
for await (const item of stream) {
|
|
1285
|
+
if (!session.active || life !== lifecycle || !listeners.size) break;
|
|
1286
|
+
if (item.signal.aborted) continue;
|
|
1287
|
+
if (generation !== item.generation) {
|
|
1288
|
+
generation = item.generation;
|
|
1289
|
+
ready = false;
|
|
1290
|
+
}
|
|
1291
|
+
const frame = parseModelSettingsChangeFrame(item.value);
|
|
1292
|
+
if (frame.kind === "ready") {
|
|
1293
|
+
ready = true;
|
|
1294
|
+
failureMessage = void 0;
|
|
1295
|
+
item.accept();
|
|
1296
|
+
invalidate();
|
|
1297
|
+
} else {
|
|
1298
|
+
if (!ready) throw new Error("Model settings sync change arrived before ready");
|
|
1299
|
+
if (namespace === "*" || frame.ns === namespace) invalidate();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error));
|
|
1304
|
+
} finally {
|
|
1305
|
+
if (current === session) current = void 0;
|
|
1306
|
+
session.active = false;
|
|
1307
|
+
await stream.dispose().catch(() => {});
|
|
1308
|
+
}
|
|
1309
|
+
})();
|
|
1310
|
+
});
|
|
1311
|
+
queueMicrotask(() => {
|
|
1312
|
+
if (life === lifecycle && listeners.size && !current) report("Model settings sync service unavailable");
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
return Object.freeze({ subscribe(onChange, onError) {
|
|
1316
|
+
const listener = {
|
|
1317
|
+
change: onChange,
|
|
1318
|
+
error: onError
|
|
1319
|
+
};
|
|
1320
|
+
listeners.add(listener);
|
|
1321
|
+
if (!injection) start();
|
|
1322
|
+
else if (failureMessage) queueMicrotask(() => {
|
|
1323
|
+
if (listeners.has(listener) && failureMessage) try {
|
|
1324
|
+
listener.error?.(failureMessage);
|
|
1325
|
+
} catch {}
|
|
1326
|
+
});
|
|
1327
|
+
return () => {
|
|
1328
|
+
if (!listeners.delete(listener) || listeners.size) return;
|
|
1329
|
+
lifecycle++;
|
|
1330
|
+
if (current) {
|
|
1331
|
+
current.active = false;
|
|
1332
|
+
current.stream.dispose().catch(() => {});
|
|
1333
|
+
}
|
|
1334
|
+
current = void 0;
|
|
1335
|
+
failureMessage = void 0;
|
|
1336
|
+
const previous = injection;
|
|
1337
|
+
injection = void 0;
|
|
1338
|
+
try {
|
|
1339
|
+
Promise.resolve(previous?.dispose()).catch(() => {});
|
|
1340
|
+
} catch {}
|
|
1341
|
+
};
|
|
1342
|
+
} });
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
567
1345
|
//#region src/client.jsx
|
|
568
1346
|
const h = react.default.createElement;
|
|
569
1347
|
const name = "dsh-safe-auto-client";
|
|
@@ -571,6 +1349,7 @@ const inject = [
|
|
|
571
1349
|
"slots",
|
|
572
1350
|
"connection",
|
|
573
1351
|
"remote",
|
|
1352
|
+
"remote.session",
|
|
574
1353
|
"locale"
|
|
575
1354
|
];
|
|
576
1355
|
const CHANNEL = "/dsh-safe-auto";
|
|
@@ -578,123 +1357,157 @@ const BUNDLE = "@klarkxy/dsh-safe-auto";
|
|
|
578
1357
|
const TEXT = {
|
|
579
1358
|
zh: {
|
|
580
1359
|
requestFailed: "请求失败",
|
|
1360
|
+
confirmationUnavailable: "确认已不可用",
|
|
1361
|
+
approvalInvalid: "确认请求无效",
|
|
1362
|
+
approvalExpiredError: "确认已超时或不再等待处理",
|
|
1363
|
+
invalidSession: "会话无效",
|
|
1364
|
+
sessionInactive: "会话未激活,请打开后重试",
|
|
1365
|
+
childSession: "子会话不能启用安全自动",
|
|
1366
|
+
invalidRequest: "请求无效",
|
|
1367
|
+
settingsChanged: "配置已更新,请刷新后重试",
|
|
1368
|
+
reviewerOnly: "这里只能修改审核配置",
|
|
1369
|
+
sessionChanged: "会话已更新,请刷新后重试",
|
|
1370
|
+
profileDisabled: "配置文件中已停用安全自动",
|
|
1371
|
+
workspaceUnavailable: "当前无法使用需原生审批的工作区写入权限",
|
|
1372
|
+
unknownPreset: "权限预设无效",
|
|
1373
|
+
controlUnloaded: "安全自动已停用",
|
|
1374
|
+
browserRequired: "请在浏览器中确认这次调用",
|
|
1375
|
+
unknownSetting: "审核模型设置无效",
|
|
1376
|
+
invalidModel: "审核模型或提供方标识无效",
|
|
1377
|
+
modelPair: "提供方和模型必须一起配置",
|
|
1378
|
+
effortRequiresModel: "选择思考强度前需先指定审核模型",
|
|
1379
|
+
invalidConfig: "配置无效",
|
|
1380
|
+
invalidConfigField: (field) => `配置项无效:${field}`,
|
|
1381
|
+
unknownConfigField: (field) => `未知配置项:${field}`,
|
|
1382
|
+
externalChange: "配置已在别处修改。草稿已保留,请刷新并丢弃草稿后重新确认。",
|
|
1383
|
+
syncUnavailable: "自动刷新不可用,请手动刷新配置。",
|
|
581
1384
|
follow: "跟随当前对话",
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
deepSwitch: "启用深度审核",
|
|
587
|
-
deepOff: "深度审核已关闭。",
|
|
588
|
-
deepNoModel: "选择模型后才会启用深度审核。",
|
|
1385
|
+
searchModels: "搜索模型…",
|
|
1386
|
+
noModels: "没有匹配的模型",
|
|
1387
|
+
clearSearch: "清除搜索",
|
|
1388
|
+
reviewer: "审核模型",
|
|
589
1389
|
model: "模型",
|
|
590
1390
|
effort: "思考强度",
|
|
591
1391
|
effortDefault: "默认",
|
|
592
|
-
followHelp: "
|
|
1392
|
+
followHelp: "跟随对话:使用当前会话的模型与默认思考强度。",
|
|
593
1393
|
savedEffortRetained: (effort) => `已保存的强度「${effort}」保留,需在 profile 中检查。`,
|
|
594
1394
|
modelAbsent: "当前模型不在目录中,保留已保存配置。",
|
|
595
1395
|
effortAbsent: "已保存的思考强度不在目录中,不会自动替换。",
|
|
596
|
-
effortClears: "
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
unsaved: "有未保存修改,启用前先保存。",
|
|
605
|
-
enable: "启用",
|
|
606
|
-
disable: "停用(不改权限)",
|
|
607
|
-
refreshSessions: "刷新会话",
|
|
608
|
-
noSessions: "没有可操作的活跃主会话,请打开一个。",
|
|
609
|
-
refreshRetry: (message) => `${message}。刷新后重试。`,
|
|
1396
|
+
effortClears: "更换模型会清空思考强度。",
|
|
1397
|
+
permission: "权限",
|
|
1398
|
+
readOnly: "仅可查看",
|
|
1399
|
+
workspaceWrite: "工作区内修改",
|
|
1400
|
+
fullAccess: "完全权限",
|
|
1401
|
+
safeAuto: "安全自动",
|
|
1402
|
+
unavailableShort: "当前不可用",
|
|
1403
|
+
refreshRetry: (message) => `${message}。重新打开菜单后重试。`,
|
|
610
1404
|
panelLabel: "安全自动设置",
|
|
611
|
-
intro: "
|
|
612
|
-
httpBanner: "HTTP 审核模式:面板只读,请在 profile 中编辑,不会覆盖已保存值。",
|
|
1405
|
+
intro: "在输入框的权限菜单里为本会话启用「安全自动」;这里只保存审核配置,不调用模型。",
|
|
613
1406
|
loading: "正在读取配置…",
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1407
|
+
tabsLabel: "设置分区",
|
|
1408
|
+
tabReviewer: "审核配置",
|
|
1409
|
+
details: "安全说明",
|
|
1410
|
+
tabLimits: "预算边界",
|
|
1411
|
+
safetyDetail: "只处理原生 write/edit/bash/pwsh 的单次沙箱提权。审核会保留真人任务授权及后续限制,并按需读取工作区内的包配置和脚本;模型通过时自动放行。其余有效请求请你在 60 秒内确认,超时拒绝。确认只适用于这一次调用。",
|
|
1412
|
+
unableLabel: "无法自动判断时:",
|
|
1413
|
+
unableValue: "请你在 60 秒内确认;超时拒绝",
|
|
1414
|
+
approvalTitle: "确认这一次调用",
|
|
1415
|
+
approvalPending: (count) => `${count} 个请求待确认`,
|
|
1416
|
+
approvalCountdown: (seconds) => `剩余 ${seconds} 秒 · 超时拒绝`,
|
|
1417
|
+
approvalExpired: "确认已超时,已拒绝。",
|
|
1418
|
+
approvalAllow: "仅允许这一次",
|
|
1419
|
+
approvalDeny: "拒绝",
|
|
1420
|
+
approvalReview: "模型审核意见",
|
|
1421
|
+
approvalRefresh: (message) => `${message}。正在刷新请求;不会自动重试决定。`,
|
|
1422
|
+
approvalEffect: "完整调用内容",
|
|
625
1423
|
prompt: "额外审核提示词",
|
|
626
1424
|
promptHint: (count) => `${count}/4096 · 只可补充审核约束,不可扩大授权或覆盖安全规则。`,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
limitsHelp: "预算在 profile 中配置,保存设置或重新启用不会补充;完整动作可能发送给所选审核模型。",
|
|
1425
|
+
limitsLine: (l) => `单次审查超时 ${l.timeoutMs} ms · 输入上限 ${l.maxInputBytes} bytes · 输出上限 ${l.outputTokens} tokens`,
|
|
1426
|
+
limitsFuses: (l) => `每个任务最多审查 ${l.maxReviewsPerTask} 次 · 连续拒绝 ${l.consecutiveDenials} 次后熔断`,
|
|
1427
|
+
limitsHelp: "预算在 profile 中配置,保存设置或重新启用不会补充;动作、真人指令和有界的本地脚本证据会发送给审核模型。",
|
|
631
1428
|
save: "保存",
|
|
632
1429
|
refreshDiscard: "刷新(丢弃草稿)",
|
|
633
1430
|
saveFailed: (message) => `${message}。草稿已保留,刷新后重新编辑,不会自动重试。`,
|
|
634
1431
|
catalogFailed: (message) => `模型目录读取失败,已保存值保持不变:${message}`,
|
|
635
1432
|
saved: "已保存",
|
|
636
|
-
summary: "
|
|
637
|
-
httpSummary: "HTTP 审核模式,保留 profile 配置。"
|
|
1433
|
+
summary: "安全自动:沙箱提权时由独立审核模型把关,通过才自动放行。"
|
|
638
1434
|
},
|
|
639
1435
|
en: {
|
|
640
1436
|
requestFailed: "Request failed",
|
|
1437
|
+
confirmationUnavailable: "Confirmation is no longer available",
|
|
1438
|
+
approvalInvalid: "Invalid approval request",
|
|
1439
|
+
approvalExpiredError: "Approval expired or is no longer pending",
|
|
1440
|
+
invalidSession: "Invalid session",
|
|
1441
|
+
sessionInactive: "Session is not active; open it and retry",
|
|
1442
|
+
childSession: "Child sessions cannot select Safe Auto",
|
|
1443
|
+
invalidRequest: "Invalid request",
|
|
1444
|
+
settingsChanged: "Settings changed; reload and retry",
|
|
1445
|
+
reviewerOnly: "Only reviewer settings may be edited here",
|
|
1446
|
+
sessionChanged: "Session changed; refresh and retry",
|
|
1447
|
+
profileDisabled: "Safe Auto is disabled in the profile",
|
|
1448
|
+
workspaceUnavailable: "Workspace Write with native approval is unavailable",
|
|
1449
|
+
unknownPreset: "Unknown permission preset",
|
|
1450
|
+
controlUnloaded: "Safe Auto control unloaded",
|
|
1451
|
+
browserRequired: "Browser confirmation required",
|
|
1452
|
+
unknownSetting: "Invalid reviewer model setting",
|
|
1453
|
+
invalidModel: "Invalid reviewer provider/model identifier",
|
|
1454
|
+
modelPair: "Provider and model must be configured together",
|
|
1455
|
+
effortRequiresModel: "Reasoning effort requires a reviewer model",
|
|
1456
|
+
invalidConfig: "Invalid configuration",
|
|
1457
|
+
invalidConfigField: (field) => `Invalid configuration field: ${field}`,
|
|
1458
|
+
unknownConfigField: (field) => `Unknown configuration field: ${field}`,
|
|
1459
|
+
externalChange: "Settings changed elsewhere. Draft preserved; refresh and discard it before reviewing changes.",
|
|
1460
|
+
syncUnavailable: "Automatic refresh unavailable; refresh settings manually.",
|
|
641
1461
|
follow: "Follow conversation",
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
deepSwitch: "Enable deep review",
|
|
647
|
-
deepOff: "Deep review is off.",
|
|
648
|
-
deepNoModel: "Deep review stays off until you choose a model.",
|
|
1462
|
+
searchModels: "Search models…",
|
|
1463
|
+
noModels: "No matching models",
|
|
1464
|
+
clearSearch: "Clear search",
|
|
1465
|
+
reviewer: "Reviewer model",
|
|
649
1466
|
model: "Model",
|
|
650
1467
|
effort: "Reasoning effort",
|
|
651
1468
|
effortDefault: "Default",
|
|
652
|
-
followHelp: "Follows the conversation with the default reasoning effort.",
|
|
1469
|
+
followHelp: "Follows the conversation model with the default reasoning effort.",
|
|
653
1470
|
savedEffortRetained: (effort) => `Saved effort "${effort}" retained; check the profile.`,
|
|
654
1471
|
modelAbsent: "This model is not in the catalog; the saved configuration is kept.",
|
|
655
1472
|
effortAbsent: "The saved effort is not advertised; it will not be replaced automatically.",
|
|
656
|
-
effortClears: "Changing the model clears
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
unsaved: "You have unsaved changes. Save before enabling.",
|
|
665
|
-
enable: "Enable",
|
|
666
|
-
disable: "Disable (keeps permissions)",
|
|
667
|
-
refreshSessions: "Refresh sessions",
|
|
668
|
-
noSessions: "No live root conversation. Open a conversation first.",
|
|
669
|
-
refreshRetry: (message) => `${message}. Refresh before retrying.`,
|
|
1473
|
+
effortClears: "Changing the model clears the reasoning effort.",
|
|
1474
|
+
permission: "Permissions",
|
|
1475
|
+
readOnly: "Read Only",
|
|
1476
|
+
workspaceWrite: "Workspace Write",
|
|
1477
|
+
fullAccess: "Full Access",
|
|
1478
|
+
safeAuto: "Safe Auto",
|
|
1479
|
+
unavailableShort: "unavailable",
|
|
1480
|
+
refreshRetry: (message) => `${message}. Reopen the menu and retry.`,
|
|
670
1481
|
panelLabel: "Safe Auto settings",
|
|
671
|
-
intro: "
|
|
672
|
-
httpBanner: "HTTP mode: read-only. Edit the profile; saved values will not be overwritten.",
|
|
1482
|
+
intro: "Enable Safe Auto per session from the permission menu in the composer; this page only saves reviewer configuration and never calls a model.",
|
|
673
1483
|
loading: "Loading settings…",
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1484
|
+
tabsLabel: "Settings sections",
|
|
1485
|
+
tabReviewer: "Reviewer",
|
|
1486
|
+
details: "Safety notes",
|
|
1487
|
+
tabLimits: "Limits",
|
|
1488
|
+
safetyDetail: "Reviews one native write/edit/bash/pwsh sandbox escalation at a time. Earlier human task authorization and later restrictions remain visible; workspace package definitions and scripts may supply evidence. A model pass auto-approves. Other valid requests ask you to confirm within 60 seconds and are denied on timeout. Confirmation grants this call only.",
|
|
1489
|
+
unableLabel: "When it cannot judge automatically: ",
|
|
1490
|
+
unableValue: "Ask you to confirm within 60 seconds; deny on timeout",
|
|
1491
|
+
approvalTitle: "Confirm this call",
|
|
1492
|
+
approvalPending: (count) => `${count} pending confirmation${count === 1 ? "" : "s"}`,
|
|
1493
|
+
approvalCountdown: (seconds) => `${seconds} seconds left · Deny on timeout`,
|
|
1494
|
+
approvalExpired: "Confirmation expired and was denied.",
|
|
1495
|
+
approvalAllow: "Allow this call only",
|
|
1496
|
+
approvalDeny: "Reject",
|
|
1497
|
+
approvalReview: "Model review opinion",
|
|
1498
|
+
approvalRefresh: (message) => `${message}. Refreshing requests; your decision will not be retried automatically.`,
|
|
1499
|
+
approvalEffect: "Complete call content",
|
|
685
1500
|
prompt: "Additional reviewer prompt",
|
|
686
1501
|
promptHint: (count) => `${count}/4096 · May add review constraints, never expand authorization or override safety rules.`,
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
limitsHelp: "Budgets live in the profile and are not refilled by saving or re-enabling. Complete actions may be sent to the selected reviewer model.",
|
|
1502
|
+
limitsLine: (l) => `Review timeout ${l.timeoutMs} ms · Input cap ${l.maxInputBytes} bytes · Output cap ${l.outputTokens} tokens`,
|
|
1503
|
+
limitsFuses: (l) => `At most ${l.maxReviewsPerTask} reviews per task · Breaker opens after ${l.consecutiveDenials} consecutive denials`,
|
|
1504
|
+
limitsHelp: "Budgets live in the profile and are not refilled by saving or re-enabling. Actions, human instructions and bounded local script evidence are sent to the reviewer model.",
|
|
691
1505
|
save: "Save",
|
|
692
1506
|
refreshDiscard: "Refresh (discard draft)",
|
|
693
1507
|
saveFailed: (message) => `${message}. Draft retained; refresh before editing again. No automatic retry.`,
|
|
694
1508
|
catalogFailed: (message) => `Model catalog failed; saved values retained: ${message}`,
|
|
695
1509
|
saved: "Saved",
|
|
696
|
-
summary: "Safe Auto: reviewer
|
|
697
|
-
httpSummary: "HTTP reviewer; profile settings retained."
|
|
1510
|
+
summary: "Safe Auto: an independent reviewer vets each sandbox escalation and only a pass auto-approves."
|
|
698
1511
|
}
|
|
699
1512
|
};
|
|
700
1513
|
/** The host locale may arrive as the injected service or as a plain tag. */
|
|
@@ -710,14 +1523,46 @@ function translator(language) {
|
|
|
710
1523
|
};
|
|
711
1524
|
}
|
|
712
1525
|
function useText(locale) {
|
|
713
|
-
const
|
|
714
|
-
(0, react.useEffect)(() => {
|
|
715
|
-
setLanguage(localeLanguage(locale));
|
|
716
|
-
if (typeof locale?.subscribe !== "function") return void 0;
|
|
717
|
-
return locale.subscribe(() => setLanguage(localeLanguage(locale)));
|
|
718
|
-
}, [locale]);
|
|
1526
|
+
const language = (0, react.useSyncExternalStore)((listener) => locale?.subscribe?.(listener) ?? (() => {}), () => localeLanguage(locale), () => localeLanguage(locale));
|
|
719
1527
|
return (0, react.useMemo)(() => translator(language), [language]);
|
|
720
1528
|
}
|
|
1529
|
+
const LOCAL_ERRORS = {
|
|
1530
|
+
"Request failed": "requestFailed",
|
|
1531
|
+
"Request rejected": "requestFailed",
|
|
1532
|
+
"Confirmation is no longer available": "confirmationUnavailable",
|
|
1533
|
+
"Invalid approval response": "approvalInvalid",
|
|
1534
|
+
"Invalid approval payload": "approvalInvalid",
|
|
1535
|
+
"Approval expired or is no longer pending": "approvalExpiredError",
|
|
1536
|
+
"Approval expired or invalidated": "approvalExpiredError",
|
|
1537
|
+
"Invalid session": "invalidSession",
|
|
1538
|
+
"Session is not active; open it and retry": "sessionInactive",
|
|
1539
|
+
"Child sessions cannot select Safe Auto": "childSession",
|
|
1540
|
+
"Invalid request": "invalidRequest",
|
|
1541
|
+
"Settings changed; reload and retry": "settingsChanged",
|
|
1542
|
+
"Only reviewer settings may be edited here": "reviewerOnly",
|
|
1543
|
+
"Session changed; refresh the list and retry": "sessionChanged",
|
|
1544
|
+
"Unknown endpoint": "invalidRequest",
|
|
1545
|
+
"Safe Auto is disabled in the profile": "profileDisabled",
|
|
1546
|
+
"Workspace Write with native approval is unavailable": "workspaceUnavailable",
|
|
1547
|
+
"Unknown permission preset": "unknownPreset",
|
|
1548
|
+
"Safe Auto control unloaded": "controlUnloaded",
|
|
1549
|
+
"Browser confirmation required": "browserRequired",
|
|
1550
|
+
"Unknown Safe Auto model setting": "unknownSetting",
|
|
1551
|
+
"invalid reviewer provider/model identifier": "invalidModel",
|
|
1552
|
+
"provider and model must be configured together": "modelPair",
|
|
1553
|
+
"reasoningEffort requires a reviewer model": "effortRequiresModel"
|
|
1554
|
+
};
|
|
1555
|
+
function errorMessage(error) {
|
|
1556
|
+
return (typeof error === "string" ? error : error?.message) || TEXT.en.requestFailed;
|
|
1557
|
+
}
|
|
1558
|
+
function errorText(t, error) {
|
|
1559
|
+
const message = typeof error === "string" ? error : error?.message;
|
|
1560
|
+
const key = Object.hasOwn(LOCAL_ERRORS, message) ? LOCAL_ERRORS[message] : void 0;
|
|
1561
|
+
const unknownField = /^unknown config field: (.+)$/.exec(message || "");
|
|
1562
|
+
const invalidField = /^(?:(enabled|staticReadonly) must be a boolean|(provider|model|reasoningEffort|reviewerPrompt) must be a string|invalid (timeoutMs|maxInputBytes|outputTokens|maxReviewsPerTask|consecutiveDenials|humanApprovalTimeoutMs))$/.exec(message || "");
|
|
1563
|
+
const text = key ? t(key) : unknownField ? t("unknownConfigField", unknownField[1]) : invalidField ? t("invalidConfigField", invalidField[1] || invalidField[2] || invalidField[3]) : ["config must be an object", "invalid configuration"].includes(message) ? t("invalidConfig") : message || t("requestFailed");
|
|
1564
|
+
return error?.context ? t(error.context, text) : text;
|
|
1565
|
+
}
|
|
721
1566
|
async function unwrapRpc(call, endpoint, payload, fallback = TEXT.en.requestFailed) {
|
|
722
1567
|
const result = await call(CHANNEL, endpoint, payload);
|
|
723
1568
|
if (!result?.ok) throw new Error(result?.error?.message || fallback);
|
|
@@ -725,23 +1570,28 @@ async function unwrapRpc(call, endpoint, payload, fallback = TEXT.en.requestFail
|
|
|
725
1570
|
}
|
|
726
1571
|
function createRequestScope() {
|
|
727
1572
|
let generation = 0, active = true, writing = false;
|
|
1573
|
+
const readGate = createPluginReadGate();
|
|
728
1574
|
return {
|
|
729
1575
|
invalidate() {
|
|
730
1576
|
generation++;
|
|
1577
|
+
readGate.reset();
|
|
731
1578
|
},
|
|
732
1579
|
dispose() {
|
|
733
1580
|
active = false;
|
|
734
1581
|
generation++;
|
|
1582
|
+
readGate.reset();
|
|
735
1583
|
},
|
|
736
1584
|
get writing() {
|
|
737
1585
|
return writing;
|
|
738
1586
|
},
|
|
739
|
-
async run(operation, handlers = {}, write = false) {
|
|
1587
|
+
async run(operation, handlers = {}, write = false, automatic = false) {
|
|
740
1588
|
if (!active || writing) return false;
|
|
1589
|
+
if (!write && automatic && !readGate.canRead()) return false;
|
|
1590
|
+
readGate.reset();
|
|
741
1591
|
if (write) writing = true;
|
|
742
1592
|
const ticket = ++generation;
|
|
743
1593
|
try {
|
|
744
|
-
const value = await operation();
|
|
1594
|
+
const value = write ? await operation() : await readGate.run(operation);
|
|
745
1595
|
if (active && generation === ticket) handlers.value?.(value);
|
|
746
1596
|
} catch (error) {
|
|
747
1597
|
if (active && generation === ticket) handlers.error?.(error);
|
|
@@ -753,49 +1603,165 @@ function createRequestScope() {
|
|
|
753
1603
|
}
|
|
754
1604
|
};
|
|
755
1605
|
}
|
|
756
|
-
|
|
757
|
-
|
|
1606
|
+
const monotonicNow = () => globalThis.performance?.now?.() ?? Date.now();
|
|
1607
|
+
function approvalRemaining(request, now = monotonicNow()) {
|
|
1608
|
+
return Math.max(0, request.deadline - now);
|
|
758
1609
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
+
function reviewerMenuState(values, catalog) {
|
|
1721
|
+
const provider = values.provider || "", model = values.model || "";
|
|
1722
|
+
const follow = !provider && !model;
|
|
1723
|
+
const listed = parseModelMenuChoices(catalog);
|
|
1724
|
+
const known = follow || listed.some((choice) => choice.provider === provider && choice.model === model);
|
|
1725
|
+
const choices = follow || known ? listed : parseModelMenuChoices(catalog, {
|
|
1726
|
+
provider,
|
|
1727
|
+
model
|
|
773
1728
|
});
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
value: current,
|
|
777
|
-
name: t("savedUnavailable", `${provider || "—"} / ${model || "—"}`),
|
|
1729
|
+
const selected = choices.find((choice) => choice.provider === provider && choice.model === model);
|
|
1730
|
+
return {
|
|
778
1731
|
provider,
|
|
779
1732
|
model,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1733
|
+
follow,
|
|
1734
|
+
known,
|
|
1735
|
+
choices,
|
|
1736
|
+
selected,
|
|
1737
|
+
efforts: follow ? [] : modelMenuEffortOptions(selected, values.reasoningEffort || "")
|
|
1738
|
+
};
|
|
784
1739
|
}
|
|
785
|
-
|
|
1740
|
+
/** Picking a model clears the effort; picking "follow" clears the whole route. */
|
|
1741
|
+
function pickReviewerModel(values, route) {
|
|
786
1742
|
return {
|
|
787
1743
|
...values,
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1744
|
+
provider: route.provider,
|
|
1745
|
+
model: route.model,
|
|
1746
|
+
reasoningEffort: ""
|
|
791
1747
|
};
|
|
792
1748
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1749
|
+
const PERMISSION_ICONS = {
|
|
1750
|
+
"read-only": _deepseek_ai_dsh_client_ui_primitives.PermissionIconReadOnlyRegular,
|
|
1751
|
+
"workspace-write": _deepseek_ai_dsh_client_ui_primitives.PermissionIconWorkspaceWriteRegular,
|
|
1752
|
+
"danger-full-access": _deepseek_ai_dsh_client_ui_primitives.PermissionIconFullAccessRegular,
|
|
1753
|
+
"safe-auto": _deepseek_ai_dsh_client_ui_primitives.IconShieldOutlineRegular
|
|
1754
|
+
};
|
|
1755
|
+
/** Known preset values get localized names matching the host menu; anything else keeps its catalog name. */
|
|
1756
|
+
function permissionLabel(value, name, t = translator("en")) {
|
|
1757
|
+
return {
|
|
1758
|
+
"read-only": t("readOnly"),
|
|
1759
|
+
"workspace-write": t("workspaceWrite"),
|
|
1760
|
+
"danger-full-access": t("fullAccess"),
|
|
1761
|
+
"safe-auto": t("safeAuto")
|
|
1762
|
+
}[value] || name || value;
|
|
797
1763
|
}
|
|
798
|
-
const css = `${officialUiCss("dsh-safe-auto")}
|
|
1764
|
+
const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
|
|
799
1765
|
/* A fieldset is the settings group and takes the contract's card surface; its
|
|
800
1766
|
* legend keeps the UA inline padding from indenting the group title. */
|
|
801
1767
|
.dsh-safe-auto legend { padding: 0; }
|
|
@@ -816,142 +1782,319 @@ const css = `${officialUiCss("dsh-safe-auto")}
|
|
|
816
1782
|
resize: vertical;
|
|
817
1783
|
}
|
|
818
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
|
+
}
|
|
819
1814
|
`;
|
|
820
1815
|
function Style() {
|
|
821
1816
|
return h("style", null, css);
|
|
822
1817
|
}
|
|
823
|
-
function ErrorText({ error }) {
|
|
1818
|
+
function ErrorText({ error, t }) {
|
|
824
1819
|
return error ? h("p", {
|
|
825
1820
|
role: "alert",
|
|
826
1821
|
className: "dsh-ui-error dsh-ui-wrap"
|
|
827
|
-
}, error) : null;
|
|
1822
|
+
}, errorText(t, error)) : null;
|
|
828
1823
|
}
|
|
829
|
-
|
|
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 }) {
|
|
830
1826
|
const t = useText(locale);
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
className: "dsh-ui-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
key: choice.value,
|
|
869
|
-
value: choice.value,
|
|
870
|
-
disabled: choice.placeholder
|
|
871
|
-
}, choice.name)))), on && follow && h("p", { className: "dsh-ui-help" }, deep ? t("deepNoModel") : t("followHelp"), retained), on && !follow && h("label", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("effort")), h("select", {
|
|
872
|
-
className: "dsh-ui-select",
|
|
873
|
-
value: effort,
|
|
874
|
-
onChange: (e) => onChange({
|
|
875
|
-
...values,
|
|
876
|
-
[`${stage}ReasoningEffort`]: e.target.value
|
|
877
|
-
})
|
|
878
|
-
}, h("option", { value: "" }, t("effortDefault")), selected.efforts.map((item) => h("option", {
|
|
879
|
-
key: item.id,
|
|
880
|
-
value: item.id
|
|
881
|
-
}, item.name || item.id)), unknownEffort && h("option", { value: effort }, t("savedUnavailable", effort)))), on && selected.unknown && h("p", { className: "dsh-ui-help" }, t("modelAbsent")), on && !follow && unknownEffort && h("p", { className: "dsh-ui-help" }, t("effortAbsent")), on && !follow && h("p", { className: "dsh-ui-help" }, t("effortClears")));
|
|
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
|
+
})));
|
|
882
1864
|
}
|
|
883
|
-
|
|
1865
|
+
/** Composer permission control via the replacement slot: native presets plus Safe Auto. */
|
|
1866
|
+
function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null }) {
|
|
884
1867
|
const t = useText(locale);
|
|
885
|
-
const [
|
|
886
|
-
const [
|
|
887
|
-
const [busy, setBusy] = (0, react.useState)(false);
|
|
1868
|
+
const [snapshot, setSnapshot] = (0, react.useState)(initialSnapshot);
|
|
1869
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
888
1870
|
const [error, setError] = (0, react.useState)("");
|
|
889
|
-
const
|
|
890
|
-
const
|
|
1871
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1872
|
+
const scopeRef = (0, react.useRef)(null);
|
|
1873
|
+
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
|
+
const refresh = (automatic = false) => scopeRef.current?.run(() => api.getSession(sessionId), {
|
|
891
1883
|
value: (value) => {
|
|
892
|
-
|
|
1884
|
+
setSnapshot(value);
|
|
893
1885
|
setError("");
|
|
894
1886
|
},
|
|
895
|
-
error: (e) => setError(e
|
|
896
|
-
});
|
|
1887
|
+
error: (e) => setError(errorMessage(e))
|
|
1888
|
+
}, false, automatic);
|
|
1889
|
+
refreshRef.current = refresh;
|
|
897
1890
|
(0, react.useEffect)(() => {
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return () => current.dispose();
|
|
902
|
-
}, [api]);
|
|
903
|
-
const row = rows.find((item) => item.sessionId === selected);
|
|
904
|
-
const change = (enabled) => {
|
|
905
|
-
if (!row || scope.current?.writing || enabled && dirty) return;
|
|
906
|
-
setBusy(true);
|
|
1891
|
+
const scope = createRequestScope();
|
|
1892
|
+
scopeRef.current = scope;
|
|
1893
|
+
setSnapshot(null);
|
|
907
1894
|
setError("");
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1895
|
+
refreshRef.current(true);
|
|
1896
|
+
const focus = () => refreshRef.current(true);
|
|
1897
|
+
const stopConnection = api.subscribeGeneration?.(() => {
|
|
1898
|
+
scope.invalidate();
|
|
1899
|
+
refreshRef.current(true);
|
|
1900
|
+
});
|
|
1901
|
+
globalThis.addEventListener?.("focus", focus);
|
|
1902
|
+
return () => {
|
|
1903
|
+
scope.dispose();
|
|
1904
|
+
stopConnection?.();
|
|
1905
|
+
globalThis.removeEventListener?.("focus", focus);
|
|
911
1906
|
};
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1907
|
+
}, [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
|
+
const choose = (value) => {
|
|
1953
|
+
const scope = scopeRef.current;
|
|
1954
|
+
if (!snapshot || scope?.writing) return;
|
|
1955
|
+
if (value === snapshot.current) {
|
|
1956
|
+
setOpen(false);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (value === "safe-auto" && !snapshot.available) return;
|
|
1960
|
+
setBusy(true);
|
|
1961
|
+
setError("");
|
|
1962
|
+
scope?.run(() => api.selectSession({
|
|
1963
|
+
sessionId,
|
|
1964
|
+
expectedRevision: snapshot.revision,
|
|
1965
|
+
value
|
|
1966
|
+
}), {
|
|
1967
|
+
value: (next) => {
|
|
1968
|
+
setSnapshot(next);
|
|
1969
|
+
setOpen(false);
|
|
1970
|
+
},
|
|
1971
|
+
error: (e) => {
|
|
1972
|
+
setError({
|
|
1973
|
+
message: errorMessage(e),
|
|
1974
|
+
context: "refreshRetry"
|
|
1975
|
+
});
|
|
1976
|
+
refreshRef.current();
|
|
1977
|
+
},
|
|
921
1978
|
settled: () => setBusy(false)
|
|
922
1979
|
}, true);
|
|
923
1980
|
};
|
|
1981
|
+
const options = snapshot?.options || [];
|
|
1982
|
+
const items = options.map((option) => ({
|
|
1983
|
+
id: option.value,
|
|
1984
|
+
label: permissionLabel(option.value, option.name, t),
|
|
1985
|
+
icon: h(PERMISSION_ICONS[option.value] || _deepseek_ai_dsh_client_ui_primitives.PermissionIconWorkspaceWriteRegular),
|
|
1986
|
+
disabled: Boolean(locked || option.value === "safe-auto" && !snapshot.available)
|
|
1987
|
+
}));
|
|
1988
|
+
if (snapshot && !snapshot.available && !options.some((o) => o.value === "safe-auto")) items.push({
|
|
1989
|
+
id: "safe-auto",
|
|
1990
|
+
label: `${t("safeAuto")} · ${t("unavailableShort")}`,
|
|
1991
|
+
icon: h(_deepseek_ai_dsh_client_ui_primitives.IconShieldOutlineRegular),
|
|
1992
|
+
disabled: true
|
|
1993
|
+
});
|
|
1994
|
+
const current = options.find((option) => option.value === snapshot?.current);
|
|
1995
|
+
const CurrentIcon = current && PERMISSION_ICONS[current.value];
|
|
1996
|
+
return h("span", {
|
|
1997
|
+
className: "dsh-safe-auto dsh-safe-auto-permission",
|
|
1998
|
+
"aria-busy": busy
|
|
1999
|
+
}, h(Style), h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
2000
|
+
open,
|
|
2001
|
+
side: "top",
|
|
2002
|
+
align: "start",
|
|
2003
|
+
portal: true,
|
|
2004
|
+
listClassName: "dsh-safe-auto",
|
|
2005
|
+
selectedId: snapshot?.current,
|
|
2006
|
+
items,
|
|
2007
|
+
onSelect: choose,
|
|
2008
|
+
onClose: () => setOpen(false),
|
|
2009
|
+
anchor: h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2010
|
+
variant: "ghost",
|
|
2011
|
+
disabled: Boolean(locked),
|
|
2012
|
+
"aria-haspopup": "menu",
|
|
2013
|
+
"aria-expanded": open,
|
|
2014
|
+
"aria-label": t("permission"),
|
|
2015
|
+
onClick: () => {
|
|
2016
|
+
const next = !open;
|
|
2017
|
+
setOpen(next);
|
|
2018
|
+
if (next) refreshRef.current();
|
|
2019
|
+
}
|
|
2020
|
+
}, CurrentIcon && h(CurrentIcon), current ? permissionLabel(current.value, current.name, t) : snapshot?.current || t("permission"), h(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutlineRegular))
|
|
2021
|
+
}), h(ErrorText, {
|
|
2022
|
+
error,
|
|
2023
|
+
t
|
|
2024
|
+
}), h(ErrorText, {
|
|
2025
|
+
error: approvals.error && {
|
|
2026
|
+
message: approvals.error,
|
|
2027
|
+
context: "approvalRefresh"
|
|
2028
|
+
},
|
|
2029
|
+
t
|
|
2030
|
+
}), pending.length ? h(ApprovalDialog, {
|
|
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);
|
|
2041
|
+
}
|
|
2042
|
+
function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
|
|
2043
|
+
const t = useText(locale);
|
|
2044
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2045
|
+
const state = reviewerMenuState(values, catalog);
|
|
2046
|
+
const effort = values.reasoningEffort || "";
|
|
2047
|
+
const currentLabel = state.follow ? t("follow") : state.selected?.label || `${state.provider} / ${state.model}`;
|
|
2048
|
+
const effortOption = state.efforts.find((item) => item.id === effort);
|
|
2049
|
+
const effortName = effortOption && modelMenuEffortLabel(effortOption, localeLanguage(locale));
|
|
2050
|
+
const unknownEffort = Boolean(effort && !state.follow && state.selected && !state.selected.efforts.some((item) => item.id === effort));
|
|
2051
|
+
const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
|
|
924
2052
|
return h("fieldset", {
|
|
925
|
-
disabled
|
|
2053
|
+
disabled,
|
|
926
2054
|
className: "dsh-ui-card"
|
|
927
|
-
}, h("legend", { className: "dsh-ui-heading" }, t("
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
2055
|
+
}, h("legend", { className: "dsh-ui-heading" }, t("reviewer")), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("model")), h(ModelMenu, {
|
|
2056
|
+
open,
|
|
2057
|
+
onOpenChange: setOpen,
|
|
2058
|
+
listClassName: "dsh-safe-auto",
|
|
2059
|
+
locale: localeLanguage(locale),
|
|
2060
|
+
anchor: h("button", {
|
|
2061
|
+
type: "button",
|
|
2062
|
+
className: "dsh-model-menu-trigger",
|
|
2063
|
+
disabled: Boolean(disabled),
|
|
2064
|
+
"aria-haspopup": "menu",
|
|
2065
|
+
"aria-expanded": open
|
|
2066
|
+
}, h("span", { className: "dsh-model-menu-triggerLabel" }, currentLabel), !state.follow && state.efforts.length ? h("span", { className: "dsh-model-menu-triggerEffort" }, effortName || t("effortDefault")) : null, h(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })),
|
|
2067
|
+
choices: state.choices,
|
|
2068
|
+
selected: state.follow ? void 0 : {
|
|
2069
|
+
provider: state.provider,
|
|
2070
|
+
model: state.model
|
|
2071
|
+
},
|
|
2072
|
+
onPick: (route) => onChange(pickReviewerModel(values, route)),
|
|
2073
|
+
leading: [{
|
|
2074
|
+
id: "follow",
|
|
2075
|
+
label: t("follow"),
|
|
2076
|
+
selected: state.follow
|
|
2077
|
+
}],
|
|
2078
|
+
onPickLeading: () => onChange(pickReviewerModel(values, {
|
|
2079
|
+
provider: "",
|
|
2080
|
+
model: ""
|
|
2081
|
+
})),
|
|
2082
|
+
efforts: state.efforts,
|
|
2083
|
+
selectedEffort: effort,
|
|
2084
|
+
onPickEffort: (next) => onChange({
|
|
2085
|
+
...values,
|
|
2086
|
+
reasoningEffort: next || ""
|
|
2087
|
+
}),
|
|
2088
|
+
modelLabel: t("model"),
|
|
2089
|
+
modelValue: currentLabel,
|
|
2090
|
+
effortLabel: t("effort"),
|
|
2091
|
+
defaultEffortLabel: t("effortDefault"),
|
|
2092
|
+
searchPlaceholder: t("searchModels"),
|
|
2093
|
+
emptyLabel: t("noModels"),
|
|
2094
|
+
clearSearchLabel: t("clearSearch")
|
|
2095
|
+
})), state.follow && h("p", { className: "dsh-ui-help" }, t("followHelp"), retained), !state.follow && !state.known && h("p", { className: "dsh-ui-help" }, t("modelAbsent")), !state.follow && unknownEffort && h("p", { className: "dsh-ui-help" }, t("effortAbsent")), !state.follow && h("p", { className: "dsh-ui-help" }, t("effortClears")));
|
|
953
2096
|
}
|
|
954
|
-
function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null }) {
|
|
2097
|
+
function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
|
|
955
2098
|
const t = useText(locale);
|
|
956
2099
|
const [settings, setSettings] = (0, react.useState)(initialSettings);
|
|
957
2100
|
const [values, setValues] = (0, react.useState)(initialSettings?.values || null);
|
|
@@ -960,40 +2103,100 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
960
2103
|
const [catalogError, setCatalogError] = (0, react.useState)("");
|
|
961
2104
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
962
2105
|
const [saved, setSaved] = (0, react.useState)(false);
|
|
963
|
-
const [
|
|
964
|
-
const
|
|
2106
|
+
const [tab, setTab] = (0, react.useState)(initialTab);
|
|
2107
|
+
const tabsId = (0, react.useId)();
|
|
965
2108
|
const dirty = (0, react.useRef)(false);
|
|
2109
|
+
const reading = (0, react.useRef)(false);
|
|
2110
|
+
const pending = (0, react.useRef)(false);
|
|
2111
|
+
const external = (0, react.useRef)(false);
|
|
2112
|
+
const [externalChange, setExternalChange] = (0, react.useState)(false);
|
|
2113
|
+
const [syncError, setSyncError] = (0, react.useState)("");
|
|
966
2114
|
const scopeRef = (0, react.useRef)(null);
|
|
967
2115
|
const refreshRef = (0, react.useRef)(null);
|
|
968
|
-
const refresh = (discardDraft = false, automatic = false) =>
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
2116
|
+
const refresh = (discardDraft = false, automatic = false) => {
|
|
2117
|
+
const scope = scopeRef.current;
|
|
2118
|
+
if (!scope) return;
|
|
2119
|
+
if (reading.current || scope.writing) {
|
|
2120
|
+
pending.current = true;
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
pending.current = false;
|
|
2124
|
+
if (dirty.current && !discardDraft) {
|
|
2125
|
+
if (external.current) setExternalChange(true);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
reading.current = true;
|
|
2129
|
+
setBusy(true);
|
|
2130
|
+
const request = scope.run(() => api.getSettings(), {
|
|
2131
|
+
value: (next) => {
|
|
2132
|
+
if (pending.current || dirty.current && !discardDraft) return;
|
|
2133
|
+
setSettings(next);
|
|
2134
|
+
setValues(next.values);
|
|
2135
|
+
dirty.current = false;
|
|
2136
|
+
setError("");
|
|
2137
|
+
external.current = false;
|
|
2138
|
+
setExternalChange(false);
|
|
2139
|
+
if (!automatic) setSaved(false);
|
|
2140
|
+
},
|
|
2141
|
+
error: (e) => {
|
|
2142
|
+
if (!pending.current) setError(errorMessage(e));
|
|
2143
|
+
},
|
|
2144
|
+
settled: () => {
|
|
2145
|
+
if (scopeRef.current !== scope) return;
|
|
2146
|
+
reading.current = false;
|
|
2147
|
+
setBusy(false);
|
|
2148
|
+
if (pending.current) refreshRef.current(false, true);
|
|
2149
|
+
}
|
|
2150
|
+
}, false, automatic && !external.current);
|
|
2151
|
+
request.then((ran) => {
|
|
2152
|
+
if (!ran && scopeRef.current === scope) {
|
|
2153
|
+
reading.current = false;
|
|
2154
|
+
setBusy(false);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
return request;
|
|
2158
|
+
};
|
|
979
2159
|
refreshRef.current = refresh;
|
|
980
2160
|
(0, react.useEffect)(() => {
|
|
981
2161
|
const scope = createRequestScope();
|
|
982
2162
|
scopeRef.current = scope;
|
|
2163
|
+
reading.current = false;
|
|
2164
|
+
pending.current = false;
|
|
2165
|
+
dirty.current = false;
|
|
2166
|
+
external.current = false;
|
|
2167
|
+
setBusy(false);
|
|
2168
|
+
setExternalChange(false);
|
|
2169
|
+
setSyncError("");
|
|
983
2170
|
refreshRef.current(false, true);
|
|
984
2171
|
let alive = true;
|
|
985
2172
|
Promise.resolve().then(() => api.modelCatalog()).then((value) => {
|
|
986
2173
|
if (alive) setCatalog(value);
|
|
987
2174
|
}, (e) => {
|
|
988
|
-
if (alive) setCatalogError(e
|
|
2175
|
+
if (alive) setCatalogError(errorMessage(e));
|
|
989
2176
|
});
|
|
990
2177
|
const focus = () => {
|
|
991
2178
|
if (!dirty.current) refreshRef.current(false, true);
|
|
992
2179
|
};
|
|
2180
|
+
const stopConnection = api.subscribeGeneration?.(() => {
|
|
2181
|
+
scope.invalidate();
|
|
2182
|
+
external.current = true;
|
|
2183
|
+
refreshRef.current(false, true);
|
|
2184
|
+
});
|
|
2185
|
+
const stopSettings = api.subscribeSettings?.(() => {
|
|
2186
|
+
if (!alive) return;
|
|
2187
|
+
setSyncError("");
|
|
2188
|
+
external.current = true;
|
|
2189
|
+
if (dirty.current && !scope.writing) setExternalChange(true);
|
|
2190
|
+
refreshRef.current(false, true);
|
|
2191
|
+
}, (message) => {
|
|
2192
|
+
if (alive) setSyncError(message);
|
|
2193
|
+
});
|
|
993
2194
|
globalThis.addEventListener?.("focus", focus);
|
|
994
2195
|
return () => {
|
|
995
2196
|
alive = false;
|
|
996
2197
|
scope.dispose();
|
|
2198
|
+
stopConnection?.();
|
|
2199
|
+
stopSettings?.();
|
|
997
2200
|
globalThis.removeEventListener?.("focus", focus);
|
|
998
2201
|
};
|
|
999
2202
|
}, [api]);
|
|
@@ -1003,7 +2206,7 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
1003
2206
|
setValues(next);
|
|
1004
2207
|
};
|
|
1005
2208
|
const save = () => {
|
|
1006
|
-
if (!settings || !values ||
|
|
2209
|
+
if (!settings || !values || reading.current || external.current || scopeRef.current?.writing || (values.reviewerPrompt || "").length > 4096) return;
|
|
1007
2210
|
setBusy(true);
|
|
1008
2211
|
setError("");
|
|
1009
2212
|
setSaved(false);
|
|
@@ -1017,102 +2220,140 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
1017
2220
|
dirty.current = false;
|
|
1018
2221
|
setSaved(true);
|
|
1019
2222
|
},
|
|
1020
|
-
error: (e) => setError(
|
|
1021
|
-
|
|
2223
|
+
error: (e) => setError({
|
|
2224
|
+
message: errorMessage(e),
|
|
2225
|
+
context: "saveFailed"
|
|
2226
|
+
}),
|
|
2227
|
+
settled: () => {
|
|
2228
|
+
setBusy(false);
|
|
2229
|
+
if (pending.current) refreshRef.current(false, true);
|
|
2230
|
+
}
|
|
1022
2231
|
}, true);
|
|
1023
2232
|
};
|
|
1024
|
-
|
|
1025
|
-
const
|
|
2233
|
+
const activeTab = tab === "limits" && !settings?.policyLimits ? "reviewer" : tab;
|
|
2234
|
+
const tabItem = (value, label) => ({
|
|
2235
|
+
value,
|
|
2236
|
+
label,
|
|
2237
|
+
id: `${tabsId}-${value}-tab`,
|
|
2238
|
+
panelId: `${tabsId}-${value}-panel`
|
|
2239
|
+
});
|
|
2240
|
+
const tabPanel = (value, className, children) => h("div", {
|
|
2241
|
+
role: "tabpanel",
|
|
2242
|
+
id: `${tabsId}-${value}-panel`,
|
|
2243
|
+
"aria-labelledby": `${tabsId}-${value}-tab`,
|
|
2244
|
+
hidden: activeTab !== value,
|
|
2245
|
+
tabIndex: 0,
|
|
2246
|
+
className
|
|
2247
|
+
}, children);
|
|
2248
|
+
if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
|
|
2249
|
+
error,
|
|
2250
|
+
t
|
|
2251
|
+
}));
|
|
1026
2252
|
return h("section", {
|
|
1027
2253
|
className: "dsh-safe-auto dsh-ui-stack",
|
|
1028
2254
|
"aria-label": t("panelLabel"),
|
|
1029
2255
|
"aria-busy": busy
|
|
1030
|
-
}, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("intro")),
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
"
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}, t("promptHint", (values.reviewerPrompt || "").length)), settings.policyLimits && h(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
|
|
1083
|
-
icon: h(_deepseek_ai_dsh_client_ui_primitives.IconShieldOutlineRegular),
|
|
1084
|
-
title: t("limits"),
|
|
1085
|
-
open: limitsOpen,
|
|
1086
|
-
expandable: true,
|
|
1087
|
-
expandOnRowClick: true,
|
|
1088
|
-
onToggle: () => setLimitsOpen((value) => !value)
|
|
1089
|
-
}, h("div", { className: "dsh-ui-stack" }, h("p", { className: "dsh-ui-meta dsh-ui-wrap" }, t("limitsTimes", settings.policyLimits)), h("p", { className: "dsh-ui-meta dsh-ui-wrap" }, t("limitsFuses", settings.policyLimits)), h("p", { className: "dsh-ui-help" }, t("limitsHelp")))), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2256
|
+
}, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("intro")), !values && h("p", { className: "dsh-ui-loading" }, t("loading")), values && h(react.default.Fragment, null, h(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
|
|
2257
|
+
value: activeTab,
|
|
2258
|
+
onChange: setTab,
|
|
2259
|
+
label: t("tabsLabel"),
|
|
2260
|
+
items: [
|
|
2261
|
+
tabItem("reviewer", t("tabReviewer")),
|
|
2262
|
+
tabItem("details", t("details")),
|
|
2263
|
+
...settings?.policyLimits ? [tabItem("limits", t("tabLimits"))] : []
|
|
2264
|
+
]
|
|
2265
|
+
}), tabPanel("reviewer", "dsh-ui-stack", [
|
|
2266
|
+
h(ReviewerFields, {
|
|
2267
|
+
key: "fields",
|
|
2268
|
+
values,
|
|
2269
|
+
catalog,
|
|
2270
|
+
disabled: busy,
|
|
2271
|
+
onChange: change,
|
|
2272
|
+
locale
|
|
2273
|
+
}),
|
|
2274
|
+
h("label", {
|
|
2275
|
+
key: "prompt",
|
|
2276
|
+
className: "dsh-ui-field"
|
|
2277
|
+
}, h("span", { className: "dsh-ui-label" }, t("prompt")), h("textarea", {
|
|
2278
|
+
className: "dsh-safe-auto-input",
|
|
2279
|
+
rows: 6,
|
|
2280
|
+
maxLength: 4096,
|
|
2281
|
+
disabled: busy,
|
|
2282
|
+
"aria-describedby": "sa-prompt-hint",
|
|
2283
|
+
value: values.reviewerPrompt || "",
|
|
2284
|
+
onChange: (e) => change({
|
|
2285
|
+
...values,
|
|
2286
|
+
reviewerPrompt: e.target.value
|
|
2287
|
+
})
|
|
2288
|
+
})),
|
|
2289
|
+
h("p", {
|
|
2290
|
+
key: "hint",
|
|
2291
|
+
id: "sa-prompt-hint",
|
|
2292
|
+
className: "dsh-ui-help"
|
|
2293
|
+
}, t("promptHint", (values.reviewerPrompt || "").length))
|
|
2294
|
+
]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, h("span", { className: "dsh-ui-label" }, t("unableLabel")), t("unableValue")))), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [
|
|
2295
|
+
h("p", {
|
|
2296
|
+
key: "line",
|
|
2297
|
+
className: "dsh-ui-meta dsh-ui-wrap"
|
|
2298
|
+
}, t("limitsLine", settings.policyLimits)),
|
|
2299
|
+
h("p", {
|
|
2300
|
+
key: "fuses",
|
|
2301
|
+
className: "dsh-ui-meta dsh-ui-wrap"
|
|
2302
|
+
}, t("limitsFuses", settings.policyLimits)),
|
|
2303
|
+
h("p", {
|
|
2304
|
+
key: "help",
|
|
2305
|
+
className: "dsh-ui-help"
|
|
2306
|
+
}, t("limitsHelp"))
|
|
2307
|
+
]), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1090
2308
|
variant: "primary",
|
|
1091
|
-
disabled:
|
|
2309
|
+
disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
|
|
1092
2310
|
onClick: save
|
|
1093
2311
|
}, t("save")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1094
2312
|
variant: "ghost",
|
|
1095
2313
|
disabled: busy,
|
|
1096
2314
|
onClick: () => refresh(true)
|
|
1097
|
-
}, t("refreshDiscard")))), h(ErrorText, {
|
|
2315
|
+
}, t("refreshDiscard")))), h(ErrorText, {
|
|
2316
|
+
error,
|
|
2317
|
+
t
|
|
2318
|
+
}), h(ErrorText, {
|
|
2319
|
+
error: catalogError && {
|
|
2320
|
+
message: catalogError,
|
|
2321
|
+
context: "catalogFailed"
|
|
2322
|
+
},
|
|
2323
|
+
t
|
|
2324
|
+
}), externalChange && h("p", {
|
|
2325
|
+
role: "alert",
|
|
2326
|
+
className: "dsh-ui-error"
|
|
2327
|
+
}, t("externalChange")), syncError && h("p", {
|
|
2328
|
+
role: "status",
|
|
2329
|
+
className: "dsh-ui-hint"
|
|
2330
|
+
}, t("syncUnavailable")), saved && h("p", {
|
|
1098
2331
|
role: "status",
|
|
1099
2332
|
className: "dsh-ui-notice"
|
|
1100
|
-
}, t("saved"))
|
|
1101
|
-
api,
|
|
1102
|
-
dirty: dirty.current,
|
|
1103
|
-
locale
|
|
1104
|
-
}));
|
|
2333
|
+
}, t("saved")));
|
|
1105
2334
|
}
|
|
1106
2335
|
function apply(ctx) {
|
|
1107
|
-
const
|
|
2336
|
+
const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
|
|
2337
|
+
const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
|
|
1108
2338
|
const api = Object.freeze({
|
|
2339
|
+
subscribeSettings: (change, error) => settingsSubscriber.subscribe(change, error),
|
|
2340
|
+
subscribeGeneration: (listener) => ctx.connection.generation?.subscribe(listener) ?? (() => {}),
|
|
1109
2341
|
getSettings: () => call("settings.get", {}),
|
|
1110
2342
|
saveSettings: (payload) => call("settings.save", payload),
|
|
1111
2343
|
modelCatalog: () => ctx.remote.session.modelCatalog(),
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
2344
|
+
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)
|
|
1115
2348
|
});
|
|
2349
|
+
ctx.slots.inject("conversation.input.permission", () => ctx.slots.register({
|
|
2350
|
+
name: "conversation.input.permission",
|
|
2351
|
+
priority: -10,
|
|
2352
|
+
inject: () => ({
|
|
2353
|
+
api,
|
|
2354
|
+
locale: ctx.locale
|
|
2355
|
+
})
|
|
2356
|
+
}, PermissionMenu));
|
|
1116
2357
|
ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
|
|
1117
2358
|
name: "plugins.bundle.config",
|
|
1118
2359
|
key: BUNDLE,
|
|
@@ -1123,20 +2364,23 @@ function apply(ctx) {
|
|
|
1123
2364
|
}, SettingsPanel));
|
|
1124
2365
|
}
|
|
1125
2366
|
//#endregion
|
|
2367
|
+
exports.ApprovalDialog = ApprovalDialog;
|
|
1126
2368
|
exports.BUNDLE = BUNDLE;
|
|
1127
2369
|
exports.CHANNEL = CHANNEL;
|
|
1128
|
-
exports.
|
|
1129
|
-
exports.
|
|
2370
|
+
exports.PermissionMenu = PermissionMenu;
|
|
2371
|
+
exports.ReviewerFields = ReviewerFields;
|
|
1130
2372
|
exports.SettingsPanel = SettingsPanel;
|
|
1131
2373
|
exports.apply = apply;
|
|
2374
|
+
exports.approvalRemaining = approvalRemaining;
|
|
2375
|
+
exports.createApprovalController = createApprovalController;
|
|
1132
2376
|
exports.createRequestScope = createRequestScope;
|
|
2377
|
+
exports.errorText = errorText;
|
|
1133
2378
|
exports.inject = inject;
|
|
1134
2379
|
exports.localeLanguage = localeLanguage;
|
|
1135
|
-
exports.modelChoices = modelChoices;
|
|
1136
|
-
exports.modelKey = modelKey;
|
|
1137
2380
|
exports.name = name;
|
|
1138
|
-
exports.
|
|
1139
|
-
exports.
|
|
2381
|
+
exports.permissionLabel = permissionLabel;
|
|
2382
|
+
exports.pickReviewerModel = pickReviewerModel;
|
|
2383
|
+
exports.reviewerMenuState = reviewerMenuState;
|
|
1140
2384
|
exports.translator = translator;
|
|
1141
2385
|
exports.unwrapRpc = unwrapRpc;
|
|
1142
2386
|
|