@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/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 __nodeShims;
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, ui-monospace, 'Cascadia Mono', Consolas, monospace);
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
- chooseModel: "选择模型",
583
- savedUnavailable: (value) => `已保存,目录中不可用:${value}`,
584
- fast: "快速审核",
585
- deep: "深度审核",
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
- approveForMe: "替我审批",
598
- sessionHelp: "为指定的活跃会话启用。保留 Workspace Write + ask,不授予 Full Access;重启、切换权限或卸载后需重新启用。",
599
- targetSession: "目标会话",
600
- chooseSession: "选择活跃会话",
601
- enabledMark: "已启用",
602
- sessionEnabled: "本会话已启用",
603
- sessionDisabled: (current) => `未启用 · 当前权限:${current}`,
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
- reviewMode: "审核模式",
615
- modeRules: "精确规则候选(POSIX)",
616
- modeApproval: "独立模型替我审批(含 Windows 原生提权)",
617
- modeSummary: "仅放行低/中风险、用户直接授权、范围有界的单次提权,其余拒绝。",
618
- details: "安全与预算说明",
619
- safetyDetail: "仅处理真实原生 write/edit/bash/pwsh 的单次沙箱提权;高风险、不确定、证据不足、未知或未绑定请求、错误、超时、预算不足及授权失效均拒绝,明确拒绝不回退。不执行取证命令,也不放开整个会话。",
620
- outputBudget: "思考模型可能需要更高输出预算(默认快审 64 / 深审 256 tokens)。在 profile 检查 fastOutputTokens / deepOutputTokens;本面板不改预算。",
621
- unableLabel: "无法自动判断时",
622
- unableValue: "直接拒绝",
623
- unableHelp: "未核验 DSH 的仅人工审批通道,此策略固定且不保存;交给下游审批链可能被其他自动插件放行,不算人工回退。",
624
- windows: "Windows:精确规则模式不支持文件/PowerShell 自动提权,请选独立审批模式。",
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
- limits: "预算与安全边界(只读)",
628
- limitsTimes: (l) => `每阶段超时 ${l.timeoutMs} ms · 输入 ${l.maxInputBytes} bytes · 快审 ${l.fastCallsPerTask} 次 · 深审 ${l.deepCallsPerTask} 次`,
629
- limitsFuses: (l) => `会话预算 ${l.sessionBudgetUnits} · 连续拒绝熔断 ${l.consecutiveDenials} · 累计拒绝熔断 ${l.totalDenials}`,
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
- chooseModel: "Choose a model",
643
- savedUnavailable: (value) => `Saved, unavailable: ${value}`,
644
- fast: "Fast review",
645
- deep: "Deep review",
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 this stage’s reasoning effort.",
657
- approveForMe: "Approve for me",
658
- sessionHelp: "Enable for one live session. Keeps Workspace Write + ask; no standing permission grant. Re-enable after restart, a permission change or uninstall.",
659
- targetSession: "Target session",
660
- chooseSession: "Choose a live session",
661
- enabledMark: "enabled",
662
- sessionEnabled: "Enabled for this session",
663
- sessionDisabled: (current) => `Disabled · Permissions: ${current}`,
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: "Saving does not call a model; only explicitly enabled sessions use automatic review.",
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
- reviewMode: "Review mode",
675
- modeRules: "Exact rule candidates (POSIX)",
676
- modeApproval: "Independent approval reviewer (includes native Windows escalation)",
677
- modeSummary: "Approves only a single low- or medium-risk, user-authorized, bounded escalation; everything else is rejected.",
678
- details: "Safety and budget details",
679
- safetyDetail: "Only single sandbox escalations of real native write/edit/bash/pwsh calls. Rejected: high risk, uncertainty, missing evidence, unknown or unbound requests, errors, timeouts, exhausted budgets and invalidated grants. Explicit denials never fall back. No probing commands; the whole session is never opened.",
680
- outputBudget: "Reasoning models may need more output budget (default 64 fast / 256 deep tokens). Check fastOutputTokens / deepOutputTokens in the profile; this panel does not change budgets.",
681
- unableLabel: "When it cannot judge automatically",
682
- unableValue: "Reject",
683
- unableHelp: "No verified human-only DSH channel, so this policy is fixed and not saved. Another automatic answerer is not human fallback.",
684
- windows: "Windows: exact rule mode cannot auto-approve file or PowerShell escalation; choose independent approval.",
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
- limits: "Budget and safety limits (read-only)",
688
- limitsTimes: (l) => `Timeout ${l.timeoutMs} ms per stage · Input ${l.maxInputBytes} bytes · Fast calls ${l.fastCallsPerTask} · Deep calls ${l.deepCallsPerTask}`,
689
- limitsFuses: (l) => `Session units ${l.sessionBudgetUnits} · Consecutive denial fuse ${l.consecutiveDenials} · Total denial fuse ${l.totalDenials}`,
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 models, reasoning effort and additional review prompt.",
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 [language, setLanguage] = (0, react.useState)(() => localeLanguage(locale));
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
- function modelKey(provider, model) {
757
- return JSON.stringify([provider || "", model || ""]);
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
- function modelChoices(catalog, provider = "", model = "", t = translator("en")) {
760
- const choices = [{
761
- value: modelKey("", ""),
762
- name: t("follow"),
763
- provider: "",
764
- model: "",
765
- efforts: []
766
- }];
767
- for (const group of catalog?.groups || []) for (const entry of group.models || []) choices.push({
768
- value: modelKey(group.id, entry.id),
769
- name: `${group.name || group.id} / ${entry.name || entry.id}`,
770
- provider: group.id,
771
- model: entry.id,
772
- efforts: entry.reasoning?.efforts || []
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 current = modelKey(provider, model);
775
- if (!choices.some((choice) => choice.value === current)) choices.push({
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
- efforts: [],
781
- unknown: true
782
- });
783
- return choices;
1733
+ follow,
1734
+ known,
1735
+ choices,
1736
+ selected,
1737
+ efforts: follow ? [] : modelMenuEffortOptions(selected, values.reasoningEffort || "")
1738
+ };
784
1739
  }
785
- function selectModel(values, stage, choice) {
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
- [`${stage}Provider`]: choice.provider,
789
- [`${stage}Model`]: choice.model,
790
- [`${stage}ReasoningEffort`]: ""
1744
+ provider: route.provider,
1745
+ model: route.model,
1746
+ reasoningEffort: ""
791
1747
  };
792
1748
  }
793
- /** A short, recognizable session name: its title or folder, plus the id head. */
794
- function sessionLabel(item, t = translator("en")) {
795
- const cwd = item.header?.cwd || "";
796
- return `${item.header?.title || cwd.split(/[\\/]/).filter(Boolean).pop() || "—"} · ${String(item.sessionId).slice(0, 8)}${item.safeAuto ? ` · ${t("enabledMark")}` : ""}`;
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
- function ModelFields({ stage, values, catalog, disabled, onChange, locale }) {
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
- const deep = stage === "deep";
832
- const provider = values[`${stage}Provider`] || "", model = values[`${stage}Model`] || "";
833
- const effort = values[`${stage}ReasoningEffort`] || "";
834
- const follow = !provider && !model;
835
- const [wantDeep, setWantDeep] = (0, react.useState)(false);
836
- const on = !deep || !follow || wantDeep;
837
- const choices = modelChoices(catalog, provider, model, t);
838
- if (deep) choices[0] = {
839
- ...choices[0],
840
- name: t("chooseModel"),
841
- placeholder: true
842
- };
843
- const selected = choices.find((choice) => choice.value === modelKey(provider, model));
844
- const unknownEffort = effort && !selected.efforts.some((item) => item.id === effort);
845
- const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
846
- return h("fieldset", {
847
- disabled,
848
- className: "dsh-ui-card"
849
- }, h("legend", { className: "dsh-ui-heading" }, t(deep ? "deep" : "fast")), deep && h("div", { className: "dsh-ui-row" }, h(_deepseek_ai_dsh_client_ui_primitives.Switch, {
850
- checked: on,
851
- label: t("deepSwitch"),
852
- disabled,
853
- onChange: (next) => {
854
- setWantDeep(next);
855
- if (!next && !follow) onChange(selectModel(values, stage, choices[0]));
856
- }
857
- }), h("span", {
858
- className: "dsh-ui-label",
859
- "aria-hidden": "true"
860
- }, t("deepSwitch"))), !on && h("p", { className: "dsh-ui-help" }, t("deepOff"), retained), on && h("label", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("model")), h("select", {
861
- className: "dsh-ui-select",
862
- value: selected.value,
863
- onChange: (e) => {
864
- const choice = choices.find((item) => item.value === e.target.value);
865
- if (choice && !choice.placeholder) onChange(selectModel(values, stage, choice));
866
- }
867
- }, choices.map((choice) => h("option", {
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
- function SessionControls({ api, initialSessions = [], dirty = false, locale }) {
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 [rows, setRows] = (0, react.useState)(initialSessions);
886
- const [selected, setSelected] = (0, react.useState)("");
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 scope = (0, react.useRef)(null);
890
- const load = () => scope.current?.run(() => api.listSessions(), {
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
- setRows(value.sessions);
1884
+ setSnapshot(value);
893
1885
  setError("");
894
1886
  },
895
- error: (e) => setError(e.message)
896
- });
1887
+ error: (e) => setError(errorMessage(e))
1888
+ }, false, automatic);
1889
+ refreshRef.current = refresh;
897
1890
  (0, react.useEffect)(() => {
898
- const current = createRequestScope();
899
- scope.current = current;
900
- load();
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
- const payload = {
909
- sessionId: row.sessionId,
910
- expectedRevision: row.revision
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
- scope.current?.run(() => enabled ? api.enableSession({
913
- ...payload,
914
- value: "safe-auto"
915
- }) : api.disableSession(payload), {
916
- value: (next) => setRows((old) => old.map((item) => item.sessionId === row.sessionId ? {
917
- ...item,
918
- ...next
919
- } : item)),
920
- error: (e) => setError(t("refreshRetry", e.message)),
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: busy,
2053
+ disabled,
926
2054
  className: "dsh-ui-card"
927
- }, h("legend", { className: "dsh-ui-heading" }, t("approveForMe")), h("p", { className: "dsh-ui-help" }, t("sessionHelp")), h("label", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("targetSession")), h("select", {
928
- className: "dsh-ui-select",
929
- value: selected,
930
- onChange: (e) => setSelected(e.target.value)
931
- }, h("option", { value: "" }, t("chooseSession")), rows.map((item) => h("option", {
932
- key: item.sessionId,
933
- value: item.sessionId,
934
- title: [item.header?.cwd, item.sessionId].filter(Boolean).join(" · ")
935
- }, sessionLabel(item, t))))), row && h("p", {
936
- role: "status",
937
- className: "dsh-ui-compact dsh-ui-wrap"
938
- }, row.safeAuto ? t("sessionEnabled") : t("sessionDisabled", row.current)), dirty && h("p", {
939
- role: "status",
940
- className: "dsh-ui-help"
941
- }, t("unsaved")), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
942
- variant: "primary",
943
- disabled: !row || !row.available || row.safeAuto || dirty,
944
- onClick: () => change(true)
945
- }, t("enable")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
946
- variant: "outline",
947
- disabled: !row?.safeAuto,
948
- onClick: () => change(false)
949
- }, t("disable")), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
950
- variant: "ghost",
951
- onClick: load
952
- }, t("refreshSessions"))), !rows.length && h("p", { className: "dsh-ui-empty" }, t("noSessions")), h(ErrorText, { error }));
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 [detailsOpen, setDetailsOpen] = (0, react.useState)(false);
964
- const [limitsOpen, setLimitsOpen] = (0, react.useState)(false);
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) => scopeRef.current?.run(() => api.getSettings(), {
969
- value: (next) => {
970
- if (dirty.current && !discardDraft) return;
971
- setSettings(next);
972
- setValues(next.values);
973
- dirty.current = false;
974
- setError("");
975
- if (!automatic) setSaved(false);
976
- },
977
- error: (e) => setError(e.message)
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.message);
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 || settings.http || scopeRef.current?.writing || (values.reviewerPrompt || "").length > 4096) return;
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(t("saveFailed", e.message)),
1021
- settled: () => setBusy(false)
2223
+ error: (e) => setError({
2224
+ message: errorMessage(e),
2225
+ context: "saveFailed"
2226
+ }),
2227
+ settled: () => {
2228
+ setBusy(false);
2229
+ if (pending.current) refreshRef.current(false, true);
2230
+ }
1022
2231
  }, true);
1023
2232
  };
1024
- if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(Style), h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), settings?.http && h("p", { className: "dsh-ui-help" }, t("httpSummary")), h(ErrorText, { error }));
1025
- const readOnly = busy || settings?.http;
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")), settings?.http && h("p", {
1031
- role: "status",
1032
- className: "dsh-ui-banner"
1033
- }, t("httpBanner")), !values && h("p", { className: "dsh-ui-loading" }, t("loading")), values && h(react.default.Fragment, null, h("label", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("reviewMode")), h("select", {
1034
- className: "dsh-ui-select",
1035
- disabled: readOnly,
1036
- value: values.approvalReview ? "approval" : "rules",
1037
- onChange: (e) => change({
1038
- ...values,
1039
- approvalReview: e.target.value === "approval"
1040
- })
1041
- }, h("option", { value: "rules" }, t("modeRules")), h("option", { value: "approval" }, t("modeApproval")))), h("p", { className: "dsh-ui-help" }, t("modeSummary")), values.approvalReview && h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("unableLabel")), h("p", { className: "dsh-ui-compact" }, t("unableValue")), h("p", {
1042
- id: "sa-human-unavailable",
1043
- className: "dsh-ui-help"
1044
- }, t("unableHelp"))), settings.platform === "win32" && h("p", {
1045
- role: "status",
1046
- className: "dsh-ui-compact dsh-ui-wrap"
1047
- }, t("windows")), h(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
1048
- icon: h(_deepseek_ai_dsh_client_ui_primitives.IconShieldOutlineRegular),
1049
- title: t("details"),
1050
- open: detailsOpen,
1051
- expandable: true,
1052
- expandOnRowClick: true,
1053
- onToggle: () => setDetailsOpen((value) => !value)
1054
- }, h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("outputBudget")))), h(ModelFields, {
1055
- stage: "fast",
1056
- values,
1057
- catalog,
1058
- disabled: readOnly,
1059
- onChange: change,
1060
- locale
1061
- }), h(ModelFields, {
1062
- stage: "deep",
1063
- values,
1064
- catalog,
1065
- disabled: readOnly,
1066
- onChange: change,
1067
- locale
1068
- }), h("label", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("prompt")), h("textarea", {
1069
- className: "dsh-safe-auto-input",
1070
- rows: 6,
1071
- maxLength: 4096,
1072
- disabled: readOnly,
1073
- "aria-describedby": "sa-prompt-hint",
1074
- value: values.reviewerPrompt || "",
1075
- onChange: (e) => change({
1076
- ...values,
1077
- reviewerPrompt: e.target.value
1078
- })
1079
- })), h("p", {
1080
- id: "sa-prompt-hint",
1081
- className: "dsh-ui-help"
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: readOnly || !dirty.current || (values.reviewerPrompt || "").length > 4096,
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, { error }), h(ErrorText, { error: catalogError && t("catalogFailed", catalogError) }), saved && h("p", {
2315
+ }, t("refreshDiscard")))), h(ErrorText, {
2316
+ error,
2317
+ t
2318
+ }), h(ErrorText, {
2319
+ error: catalogError && {
2320
+ message: catalogError,
2321
+ context: "catalogFailed"
2322
+ },
2323
+ t
2324
+ }), externalChange && h("p", {
2325
+ role: "alert",
2326
+ className: "dsh-ui-error"
2327
+ }, t("externalChange")), syncError && h("p", {
2328
+ role: "status",
2329
+ className: "dsh-ui-hint"
2330
+ }, t("syncUnavailable")), saved && h("p", {
1098
2331
  role: "status",
1099
2332
  className: "dsh-ui-notice"
1100
- }, t("saved")), h(SessionControls, {
1101
- api,
1102
- dirty: dirty.current,
1103
- locale
1104
- }));
2333
+ }, t("saved")));
1105
2334
  }
1106
2335
  function apply(ctx) {
1107
- const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload, translator(localeLanguage(ctx.locale))("requestFailed"));
2336
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
2337
+ const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
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
- listSessions: () => call("session.list", {}),
1113
- enableSession: (payload) => call("session.select", payload),
1114
- disableSession: (payload) => call("session.disable", payload)
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.ModelFields = ModelFields;
1129
- exports.SessionControls = SessionControls;
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.selectModel = selectModel;
1139
- exports.sessionLabel = sessionLabel;
2381
+ exports.permissionLabel = permissionLabel;
2382
+ exports.pickReviewerModel = pickReviewerModel;
2383
+ exports.reviewerMenuState = reviewerMenuState;
1140
2384
  exports.translator = translator;
1141
2385
  exports.unwrapRpc = unwrapRpc;
1142
2386