dsh-yolo-mode 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/lib/bridge-entry.js +51 -0
- package/lib/client/index.js +1268 -0
- package/lib/index.js +306 -0
- package/lib/judge.js +240 -0
- package/lib/policy.js +363 -0
- package/lib/remote.js +489 -0
- package/lib/settings.js +102 -0
- package/lib/state.js +92 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-yolo-mode",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
|
|
9
|
+
//#region src/client/locales.js
|
|
10
|
+
/**
|
|
11
|
+
* Bilingual dictionaries for the YOLO mode `settings.yoloMode` namespace.
|
|
12
|
+
* Both locales must carry every key so a locale switch never leaves a blank
|
|
13
|
+
* label.
|
|
14
|
+
*/
|
|
15
|
+
/** Dictionary namespace owned by YOLO mode (registered by src/client/index.js). */
|
|
16
|
+
const NS = "settings.yoloMode";
|
|
17
|
+
/** English dictionary. */
|
|
18
|
+
const en = {
|
|
19
|
+
nav: "YOLO Approval",
|
|
20
|
+
chip: "YOLO status",
|
|
21
|
+
sectionIntro: "Configure how YOLO mode auto-judges sandbox escalation approval requests.",
|
|
22
|
+
loadError: "Could not load YOLO mode settings.",
|
|
23
|
+
conflict: "The settings changed on the server. Reloading your edits — please review and retry.",
|
|
24
|
+
rejected: "The change was rejected by the settings provider.",
|
|
25
|
+
fatal: "Could not save: {message}",
|
|
26
|
+
preset: "Preset",
|
|
27
|
+
presetHint: "off | strict | balanced | permissive | yolo | custom",
|
|
28
|
+
modes: "Modes",
|
|
29
|
+
modesHint: "Valid sandbox modes YOLO judges (workspace-write, danger-full-access…)",
|
|
30
|
+
workspaceWrite: "workspace-write",
|
|
31
|
+
dangerFullAccess: "danger-full-access",
|
|
32
|
+
judge: "Judge",
|
|
33
|
+
provider: "Provider",
|
|
34
|
+
model: "Model",
|
|
35
|
+
systemPrompt: "System prompt",
|
|
36
|
+
timeoutMs: "Timeout (ms)",
|
|
37
|
+
maxTokens: "Max tokens",
|
|
38
|
+
concurrency: "Concurrency",
|
|
39
|
+
levels: "Levels (JSON)",
|
|
40
|
+
levelsHint: "{\"tools\": {…}, \"<mode>\": \"allow|judge|delegate|deny\"}",
|
|
41
|
+
invalidLevelsJson: "levels is not valid JSON",
|
|
42
|
+
providerEmpty: "Not configured",
|
|
43
|
+
modelEmpty: "Not configured",
|
|
44
|
+
presetLevels: "Levels table",
|
|
45
|
+
presetPrompt: "Default prompt",
|
|
46
|
+
presetErrorFallback: "Error fallback",
|
|
47
|
+
presetUnsureFallback: "Unsure fallback",
|
|
48
|
+
presetCustom: "per your levels table",
|
|
49
|
+
save: "Save",
|
|
50
|
+
saved: "Saved",
|
|
51
|
+
saveFailed: "Save failed",
|
|
52
|
+
statsTotal: "Total",
|
|
53
|
+
statsAllowed: "Allowed",
|
|
54
|
+
statsRejected: "Rejected",
|
|
55
|
+
statsDelegated: "Delegated",
|
|
56
|
+
refresh: "Refresh",
|
|
57
|
+
close: "Close",
|
|
58
|
+
tableTime: "Time",
|
|
59
|
+
tableTool: "Tool",
|
|
60
|
+
tableTarget: "Target",
|
|
61
|
+
tableDecision: "Decision",
|
|
62
|
+
tableOutcome: "Outcome",
|
|
63
|
+
tableReason: "Reason",
|
|
64
|
+
recentEmpty: "No decisions yet."
|
|
65
|
+
};
|
|
66
|
+
/** Chinese dictionary. */
|
|
67
|
+
const zh = {
|
|
68
|
+
nav: "YOLO 审批",
|
|
69
|
+
chip: "YOLO 状态",
|
|
70
|
+
sectionIntro: "配置 YOLO 模式如何自动裁决沙箱升权审批申请。",
|
|
71
|
+
loadError: "无法加载 YOLO 模式设置。",
|
|
72
|
+
conflict: "服务端设置已变更。已重新加载你的编辑——请复核并重试。",
|
|
73
|
+
rejected: "更改被设置提供方拒绝。",
|
|
74
|
+
fatal: "无法保存:{message}",
|
|
75
|
+
preset: "预设",
|
|
76
|
+
presetHint: "off | strict | balanced | permissive | yolo | custom",
|
|
77
|
+
modes: "模式",
|
|
78
|
+
modesHint: "由 YOLO 裁决的有效沙箱模式(workspace-write、danger-full-access…)",
|
|
79
|
+
workspaceWrite: "workspace-write",
|
|
80
|
+
dangerFullAccess: "danger-full-access",
|
|
81
|
+
judge: "裁判",
|
|
82
|
+
provider: "供应商",
|
|
83
|
+
model: "模型",
|
|
84
|
+
systemPrompt: "系统提示词",
|
|
85
|
+
timeoutMs: "超时(毫秒)",
|
|
86
|
+
maxTokens: "最大令牌",
|
|
87
|
+
concurrency: "并发",
|
|
88
|
+
levels: "层级表(JSON)",
|
|
89
|
+
levelsHint: "{\"tools\": {…}, \"<mode>\": \"allow|judge|delegate|deny\"}",
|
|
90
|
+
invalidLevelsJson: "levels 不是合法 JSON",
|
|
91
|
+
providerEmpty: "未配置",
|
|
92
|
+
modelEmpty: "未配置",
|
|
93
|
+
presetLevels: "层级表",
|
|
94
|
+
presetPrompt: "默认提示词",
|
|
95
|
+
presetErrorFallback: "错误回退",
|
|
96
|
+
presetUnsureFallback: "不确定回退",
|
|
97
|
+
presetCustom: "按你的层级表",
|
|
98
|
+
save: "保存",
|
|
99
|
+
saved: "已保存",
|
|
100
|
+
saveFailed: "保存失败",
|
|
101
|
+
statsTotal: "总数",
|
|
102
|
+
statsAllowed: "放行",
|
|
103
|
+
statsRejected: "拒绝",
|
|
104
|
+
statsDelegated: "转人工",
|
|
105
|
+
refresh: "刷新",
|
|
106
|
+
close: "关闭",
|
|
107
|
+
tableTime: "时间",
|
|
108
|
+
tableTool: "工具",
|
|
109
|
+
tableTarget: "目标模式",
|
|
110
|
+
tableDecision: "决策",
|
|
111
|
+
tableOutcome: "结果",
|
|
112
|
+
tableReason: "理由",
|
|
113
|
+
recentEmpty: "暂无裁决记录。"
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/client/store-logic.js
|
|
117
|
+
/**
|
|
118
|
+
* Pure settings-write logic for the YOLO mode client (no React, no wire).
|
|
119
|
+
* Everything here is a function of its inputs so the revision state machine,
|
|
120
|
+
* path-op construction, and conflict handling are unit-testable in a plain
|
|
121
|
+
* node environment.
|
|
122
|
+
*
|
|
123
|
+
* The persisted settings section mirrors the Host schema (lib/settings.js):
|
|
124
|
+
* {
|
|
125
|
+
* preset?, modes?, levels?, judge?: { provider?, model?, systemPrompt?,
|
|
126
|
+
* timeoutMs?, maxTokens?, concurrency? }, includeSubagents?, auditFile?
|
|
127
|
+
* }
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Normalize an optional string: blank/whitespace becomes undefined (so an
|
|
131
|
+
* empty field clears the user override back to the composition base).
|
|
132
|
+
*/
|
|
133
|
+
function optional(value) {
|
|
134
|
+
if (value === void 0) return void 0;
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build the path ops that (re)set one scalar configuration field. `set` writes
|
|
140
|
+
* the value at the path, creating intermediate objects; `unset` removes the
|
|
141
|
+
* user override so the field falls back to the composition base.
|
|
142
|
+
*/
|
|
143
|
+
function setFieldOps(path, value) {
|
|
144
|
+
return [{
|
|
145
|
+
op: "set",
|
|
146
|
+
path: [...path],
|
|
147
|
+
value
|
|
148
|
+
}];
|
|
149
|
+
}
|
|
150
|
+
function unsetFieldOps(path) {
|
|
151
|
+
return [{
|
|
152
|
+
op: "unset",
|
|
153
|
+
path: [...path]
|
|
154
|
+
}];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Ops to save the whole preset selection. Always writes the preset so a "save"
|
|
158
|
+
* on an unmodified form is idempotent with the host.
|
|
159
|
+
*/
|
|
160
|
+
function presetOps(preset) {
|
|
161
|
+
return setFieldOps(["preset"], preset);
|
|
162
|
+
}
|
|
163
|
+
/** Ops to save the modes list (workspace-write / danger-full-access …). */
|
|
164
|
+
function modesOps(modes) {
|
|
165
|
+
return setFieldOps(["modes"], modes);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Ops to save the whole levels table. levels.tools can override any preset per
|
|
169
|
+
* tool; levels[targetMode] only takes effect when preset === 'custom'. The
|
|
170
|
+
* parsed object replaces the section wholesale at ['levels'].
|
|
171
|
+
*/
|
|
172
|
+
function levelsOps(levels) {
|
|
173
|
+
return setFieldOps(["levels"], levels);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Ops to save one judge field. An empty string (or undefined resolved value)
|
|
177
|
+
* becomes an unset so the judge override is cleared back to defaults.
|
|
178
|
+
*/
|
|
179
|
+
function judgeFieldOps(stored, path, value) {
|
|
180
|
+
const normalized = optional(value);
|
|
181
|
+
if (normalized === stored) return [];
|
|
182
|
+
if (normalized === void 0) return unsetFieldOps(path);
|
|
183
|
+
return setFieldOps(path, normalized);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The revision state machine. A successful mutate returns the next revision;
|
|
187
|
+
* a conflict keeps the stale revision and marks `conflicted` so the store
|
|
188
|
+
* reloads before the user retries.
|
|
189
|
+
*/
|
|
190
|
+
function advanceRevision(state, serverRevision) {
|
|
191
|
+
return {
|
|
192
|
+
revision: serverRevision,
|
|
193
|
+
conflicted: false
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Mark a conflict: keep the stale revision (the editor must reload). */
|
|
197
|
+
function markConflict(state) {
|
|
198
|
+
return {
|
|
199
|
+
revision: state.revision,
|
|
200
|
+
conflicted: true
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Rebase after a reload picked up the fresh namespace view. */
|
|
204
|
+
function adoptRevision(_state, freshRevision) {
|
|
205
|
+
return {
|
|
206
|
+
revision: freshRevision,
|
|
207
|
+
conflicted: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Classification of a settings.mutate failure for the UI's conflict handling.
|
|
212
|
+
* `settings-conflict` → 'conflict' (reload and review); `settings-rejected` or
|
|
213
|
+
* `schema-validation` → 'rejected' (show the message); everything else fatal.
|
|
214
|
+
*/
|
|
215
|
+
function classifyMutateError(code, _message) {
|
|
216
|
+
if (code === "settings-conflict") return "conflict";
|
|
217
|
+
if (code === "settings-rejected" || code === "schema-validation") return "rejected";
|
|
218
|
+
return "fatal";
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/client/store.js
|
|
222
|
+
/**
|
|
223
|
+
* YOLO mode client page store: one snapshot joining the plugin's settings
|
|
224
|
+
* namespace (`settingsView`, resolved `view`) and its live status view
|
|
225
|
+
* (`statusView`: preset, judgeConfigured, stats, recent). The Host stays the
|
|
226
|
+
* single fact source: every write travels as path ops through the bridge's
|
|
227
|
+
* settingsMutate endpoint with an expectedRevision optimistic lock, and pushed
|
|
228
|
+
* invalidations (settings/document-updated, connection/reset) refresh the page.
|
|
229
|
+
*
|
|
230
|
+
* The store is a bare observable (implements subscribe/getSnapshot) so it can
|
|
231
|
+
* be bound directly by `bindSnapshotSelector` (dsh-client-web-react); the
|
|
232
|
+
* popup open/closed flag is also held here so the chip and the overlay share
|
|
233
|
+
* one source of truth.
|
|
234
|
+
*/
|
|
235
|
+
/** Channel + endpoints under the plugin's self-published bridge. */
|
|
236
|
+
const YOLO_RPC_CHANNEL = "/yolo-mode";
|
|
237
|
+
const YOLO_RPC_VIEW = "settingsView";
|
|
238
|
+
const YOLO_RPC_STATUS = "statusView";
|
|
239
|
+
const YOLO_RPC_MUTATE = "settingsMutate";
|
|
240
|
+
/** Initial snapshot returned by a freshly constructed store. */
|
|
241
|
+
function initialYoloState() {
|
|
242
|
+
return {
|
|
243
|
+
status: "idle",
|
|
244
|
+
view: void 0,
|
|
245
|
+
statusInfo: void 0,
|
|
246
|
+
revision: 0,
|
|
247
|
+
conflicted: false,
|
|
248
|
+
writable: true,
|
|
249
|
+
error: void 0,
|
|
250
|
+
open: false,
|
|
251
|
+
/** Configurable provider directory (llm.providers). */
|
|
252
|
+
providers: [],
|
|
253
|
+
/** Model catalog groups (llm.models). */
|
|
254
|
+
models: []
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The settings/status page controller (one per client surface).
|
|
259
|
+
*
|
|
260
|
+
* @param {object} options
|
|
261
|
+
* @param {{ call: (channel: string, endpoint: string, payload: any) => Promise<any> }} options.rpc
|
|
262
|
+
* rpc.call(channel, endpoint, payload) resolves to { ok: true, value } |
|
|
263
|
+
* { ok: false, error }.
|
|
264
|
+
* @param {object|null} [options.llm] - optional `connection.api.llm` face
|
|
265
|
+
* ({ providers(), models() } returning { result: { ok, value } }); when null
|
|
266
|
+
* the directory stays empty and the judge fields fall back to free text.
|
|
267
|
+
*/
|
|
268
|
+
var YoloStore = class {
|
|
269
|
+
constructor({ rpc, llm }) {
|
|
270
|
+
this.rpc = rpc;
|
|
271
|
+
this.llm = llm == null ? null : llm;
|
|
272
|
+
this._state = initialYoloState();
|
|
273
|
+
this._listeners = /* @__PURE__ */ new Set();
|
|
274
|
+
this._generation = 0;
|
|
275
|
+
}
|
|
276
|
+
/** The current immutable snapshot (stable reference until the next change). */
|
|
277
|
+
getSnapshot() {
|
|
278
|
+
return this._state;
|
|
279
|
+
}
|
|
280
|
+
/** Subscribe to snapshot changes; returns an unsubscribe. */
|
|
281
|
+
subscribe(listener) {
|
|
282
|
+
this._listeners.add(listener);
|
|
283
|
+
return () => {
|
|
284
|
+
this._listeners.delete(listener);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Apply a partial patch to the snapshot and synchronously notify listeners. */
|
|
288
|
+
set(partial) {
|
|
289
|
+
this._state = Object.assign({}, this._state, partial);
|
|
290
|
+
for (const listener of [...this._listeners]) listener();
|
|
291
|
+
}
|
|
292
|
+
/** Toggle the popup open/closed flag (shared by chip and overlay). */
|
|
293
|
+
togglePopup() {
|
|
294
|
+
this.set({ open: !this._state.open });
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Call one bridge endpoint over the connection's generic RPC channel.
|
|
298
|
+
* Returns the RpcResult ({ ok: true, value } | { ok: false, error }).
|
|
299
|
+
*/
|
|
300
|
+
async _call(endpoint, payload) {
|
|
301
|
+
const result = await this.rpc.call(YOLO_RPC_CHANNEL, endpoint, payload);
|
|
302
|
+
if (result === null || typeof result !== "object" || !("ok" in result)) return {
|
|
303
|
+
ok: false,
|
|
304
|
+
error: {
|
|
305
|
+
code: "internal",
|
|
306
|
+
message: "bridge returned a malformed result"
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
/** Error code of an RPC error branch, when present. */
|
|
312
|
+
_errorCode(error) {
|
|
313
|
+
if (error !== null && typeof error === "object" && typeof error.code === "string") return error.code;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Fetch the optional LLM directory (provider list + model catalog) when the
|
|
317
|
+
* store was constructed with an llm face. Directory failures are NOT fatal:
|
|
318
|
+
* they leave the snapshot's providers/models empty without flipping status
|
|
319
|
+
* to 'error' — only the settings/status bridge calls can do that.
|
|
320
|
+
*/
|
|
321
|
+
async _fetchLlmDirectory() {
|
|
322
|
+
if (this.llm === null || this.llm === void 0) return {
|
|
323
|
+
providers: [],
|
|
324
|
+
models: []
|
|
325
|
+
};
|
|
326
|
+
let providers = [];
|
|
327
|
+
let models = [];
|
|
328
|
+
try {
|
|
329
|
+
const response = await this.llm.providers({});
|
|
330
|
+
const value = response && response.result && response.result.ok ? response.result.value : void 0;
|
|
331
|
+
if (value && Array.isArray(value.providers)) providers = value.providers;
|
|
332
|
+
} catch {
|
|
333
|
+
providers = [];
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const response = await this.llm.models({});
|
|
337
|
+
const value = response && response.result && response.result.ok ? response.result.value : void 0;
|
|
338
|
+
if (value && Array.isArray(value.groups)) models = value.groups;
|
|
339
|
+
} catch {
|
|
340
|
+
models = [];
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
providers,
|
|
344
|
+
models
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Refresh the whole page snapshot: settings view + live status view + the
|
|
349
|
+
* optional LLM provider/model directory. On a success whose view is present,
|
|
350
|
+
* adopt its revision and go ready; a settings/status failure flips status to
|
|
351
|
+
* 'error' recording the first error code (the directory never does).
|
|
352
|
+
*/
|
|
353
|
+
async load() {
|
|
354
|
+
const generation = ++this._generation;
|
|
355
|
+
this.set({
|
|
356
|
+
status: "loading",
|
|
357
|
+
error: void 0
|
|
358
|
+
});
|
|
359
|
+
const [viewResult, statusResult, directory] = await Promise.all([
|
|
360
|
+
this._call(YOLO_RPC_VIEW, {}),
|
|
361
|
+
this._call(YOLO_RPC_STATUS, {}),
|
|
362
|
+
this._fetchLlmDirectory()
|
|
363
|
+
]);
|
|
364
|
+
if (generation !== this._generation) return;
|
|
365
|
+
if (!viewResult.ok || !statusResult.ok) {
|
|
366
|
+
const code = this._errorCode(!viewResult.ok ? viewResult.error : statusResult.error);
|
|
367
|
+
this.set({
|
|
368
|
+
status: "error",
|
|
369
|
+
error: code === void 0 ? true : code,
|
|
370
|
+
conflicted: false,
|
|
371
|
+
providers: directory.providers,
|
|
372
|
+
models: directory.models
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const view = viewResult.value.view;
|
|
377
|
+
const next = {
|
|
378
|
+
status: "ready",
|
|
379
|
+
view,
|
|
380
|
+
statusInfo: statusResult.value,
|
|
381
|
+
conflict: false,
|
|
382
|
+
error: void 0,
|
|
383
|
+
providers: directory.providers,
|
|
384
|
+
models: directory.models
|
|
385
|
+
};
|
|
386
|
+
if (viewResult.value.writable !== void 0) next.writable = viewResult.value.writable;
|
|
387
|
+
if (view !== void 0 && view !== null) next.revision = typeof view.revision === "number" ? view.revision : 0;
|
|
388
|
+
this.set(Object.assign({ conflicted: false }, next));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Run one mutate and update the snapshot's revision. Returns the failure
|
|
392
|
+
* kind ('conflict' | 'rejected' | 'fatal') plus a message, or undefined on
|
|
393
|
+
* success.
|
|
394
|
+
*
|
|
395
|
+
* - ok → advanceRevision + reload
|
|
396
|
+
* - conflict → markConflict + reload (fresh view, user reviews)
|
|
397
|
+
* - rejected/fatal→ record error, do NOT reload
|
|
398
|
+
*
|
|
399
|
+
* @param {import('./store-logic.js').SettingsPathOpViewLike[]} ops
|
|
400
|
+
* @returns {Promise<{ ok: true } | { ok: false, kind: string, code?: string }>}
|
|
401
|
+
*/
|
|
402
|
+
async mutate(ops) {
|
|
403
|
+
const state = this._state;
|
|
404
|
+
const result = await this._call(YOLO_RPC_MUTATE, {
|
|
405
|
+
ns: "yolo-mode",
|
|
406
|
+
ops,
|
|
407
|
+
expectedRevision: state.revision
|
|
408
|
+
});
|
|
409
|
+
if (result.ok) {
|
|
410
|
+
const value = result.value;
|
|
411
|
+
const serverRevision = value && typeof value.revision === "number" ? value.revision : state.revision;
|
|
412
|
+
this._state = Object.assign({}, this._state, advanceRevision({
|
|
413
|
+
revision: state.revision,
|
|
414
|
+
conflicted: state.conflicted
|
|
415
|
+
}, serverRevision));
|
|
416
|
+
for (const listener of [...this._listeners]) listener();
|
|
417
|
+
await this.load();
|
|
418
|
+
return { ok: true };
|
|
419
|
+
}
|
|
420
|
+
const code = this._errorCode(result.error);
|
|
421
|
+
const kind = classifyMutateError(code, void 0);
|
|
422
|
+
if (kind === "conflict") {
|
|
423
|
+
this._state = Object.assign({}, this._state, markConflict({
|
|
424
|
+
revision: state.revision,
|
|
425
|
+
conflicted: state.conflicted
|
|
426
|
+
}));
|
|
427
|
+
for (const listener of [...this._listeners]) listener();
|
|
428
|
+
await this.load();
|
|
429
|
+
} else this.set({
|
|
430
|
+
conflicted: false,
|
|
431
|
+
error: code === void 0 ? "settings-rejected" : code
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
kind,
|
|
436
|
+
code
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/** Rebase the revision from a freshly loaded view without a full load. */
|
|
440
|
+
adoptRevision(freshRevision) {
|
|
441
|
+
this.set(adoptRevision({
|
|
442
|
+
revision: this._state.revision,
|
|
443
|
+
conflicted: this._state.conflicted
|
|
444
|
+
}, freshRevision));
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/client/presets.js
|
|
449
|
+
/**
|
|
450
|
+
* Static per-preset display data for the YOLO settings page: the level table
|
|
451
|
+
* (workspace-write / danger-full-access policies), the judge error/unsure
|
|
452
|
+
* fallbacks, and a one-sentence summary of the preset's default judge prompt.
|
|
453
|
+
*
|
|
454
|
+
* The level/fallback numbers mirror the Host policy table (docs/design.md §4
|
|
455
|
+
* and lib/policy.js): only the non-custom presets carry fixed policies, while
|
|
456
|
+
* `custom` reads everything from the user's levels table (marker 'custom'
|
|
457
|
+
* tells the UI to render "per your levels table").
|
|
458
|
+
*
|
|
459
|
+
* This module is pure display data — nothing here participates in saving.
|
|
460
|
+
*/
|
|
461
|
+
/** Six presets in directory order, with their level table + prompt summary. */
|
|
462
|
+
const PRESETS_INFO = [
|
|
463
|
+
{
|
|
464
|
+
id: "off",
|
|
465
|
+
levels: {
|
|
466
|
+
workspaceWrite: "delegate",
|
|
467
|
+
dangerFullAccess: "delegate"
|
|
468
|
+
},
|
|
469
|
+
fallbackError: "delegate",
|
|
470
|
+
fallbackUnsure: "delegate",
|
|
471
|
+
prompt: "关闭:不自动裁决,所有升权申请转人工。"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "strict",
|
|
475
|
+
levels: {
|
|
476
|
+
workspaceWrite: "judge",
|
|
477
|
+
dangerFullAccess: "delegate"
|
|
478
|
+
},
|
|
479
|
+
fallbackError: "rejected",
|
|
480
|
+
fallbackUnsure: "delegate",
|
|
481
|
+
prompt: "最保守审计:workspace-write 从严裁决、danger-full-access 转人工,裁判出错一律拒绝。"
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: "balanced",
|
|
485
|
+
levels: {
|
|
486
|
+
workspaceWrite: "judge",
|
|
487
|
+
dangerFullAccess: "judge"
|
|
488
|
+
},
|
|
489
|
+
fallbackError: "delegate",
|
|
490
|
+
fallbackUnsure: "delegate",
|
|
491
|
+
prompt: "平衡(默认):两档模式均由裁判依据事实裁决,存疑或出错转人工。"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "permissive",
|
|
495
|
+
levels: {
|
|
496
|
+
workspaceWrite: "judge",
|
|
497
|
+
dangerFullAccess: "judge"
|
|
498
|
+
},
|
|
499
|
+
fallbackError: "delegate",
|
|
500
|
+
fallbackUnsure: "allowed-once",
|
|
501
|
+
prompt: "宽松审计:理由合理且范围可接受即倾向放行,仅明显破坏性/供应链风险拒绝;不确定时放行。"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: "yolo",
|
|
505
|
+
levels: {
|
|
506
|
+
workspaceWrite: "allow",
|
|
507
|
+
dangerFullAccess: "allow"
|
|
508
|
+
},
|
|
509
|
+
fallbackError: "delegate",
|
|
510
|
+
fallbackUnsure: "delegate",
|
|
511
|
+
prompt: "全力放行:所有升权申请自动允许。"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "custom",
|
|
515
|
+
levels: {
|
|
516
|
+
workspaceWrite: "custom",
|
|
517
|
+
dangerFullAccess: "custom"
|
|
518
|
+
},
|
|
519
|
+
fallbackError: "custom",
|
|
520
|
+
fallbackUnsure: "custom",
|
|
521
|
+
prompt: "自定义:按你的层级表裁决该工具+目标模式的策略,存疑按层级表回退。"
|
|
522
|
+
}
|
|
523
|
+
];
|
|
524
|
+
/**
|
|
525
|
+
* Look up one preset's display info.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} id - preset id ('off' | 'strict' | 'balanced' | 'permissive' |
|
|
528
|
+
* 'yolo' | 'custom').
|
|
529
|
+
* @returns {object|null} the PRESETS_INFO entry, or null when unknown.
|
|
530
|
+
*/
|
|
531
|
+
function presetInfo(id) {
|
|
532
|
+
return PRESETS_INFO.find((preset) => preset.id === id) || null;
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/client/ui/SettingsSection.js
|
|
536
|
+
/**
|
|
537
|
+
* YOLO mode settings section (slot `settings.section`). Renders the resolved
|
|
538
|
+
* configuration (view.value) into a form and saves via path ops through
|
|
539
|
+
* store.mutate with an optimistic-revision lock. Everything is React.createElement
|
|
540
|
+
* + inline styles (no JSX, no CSS modules).
|
|
541
|
+
*
|
|
542
|
+
* Draft-sync pattern mirrors the reference DefaultModelRow
|
|
543
|
+
* (dsh-subagents-options): the draft initializes once from the view and is
|
|
544
|
+
* re-seeded only by a useEffect on the exact leaf values we edit, skipped while
|
|
545
|
+
* a save is in flight. No render-time patching, so a mid-edit is never
|
|
546
|
+
* clobbered and a save always reflects the server truth afterwards.
|
|
547
|
+
*/
|
|
548
|
+
const PRESETS = [
|
|
549
|
+
"off",
|
|
550
|
+
"strict",
|
|
551
|
+
"balanced",
|
|
552
|
+
"permissive",
|
|
553
|
+
"yolo",
|
|
554
|
+
"custom"
|
|
555
|
+
];
|
|
556
|
+
const MODE_OPTIONS = [{
|
|
557
|
+
id: "workspace-write",
|
|
558
|
+
key: "workspaceWrite"
|
|
559
|
+
}, {
|
|
560
|
+
id: "danger-full-access",
|
|
561
|
+
key: "dangerFullAccess"
|
|
562
|
+
}];
|
|
563
|
+
/** Form draft: strings for text/number fields, array for modes, string for levels. */
|
|
564
|
+
function draftFromView(view, t) {
|
|
565
|
+
const value = view && view.value != null ? view.value : {};
|
|
566
|
+
const judge = value.judge && typeof value.judge === "object" ? value.judge : {};
|
|
567
|
+
return {
|
|
568
|
+
preset: typeof value.preset === "string" ? value.preset : "balanced",
|
|
569
|
+
modes: Array.isArray(value.modes) ? value.modes.slice() : [],
|
|
570
|
+
judgeProvider: typeof judge.provider === "string" ? judge.provider : "",
|
|
571
|
+
judgeModel: typeof judge.model === "string" ? judge.model : "",
|
|
572
|
+
judgeSystemPrompt: typeof judge.systemPrompt === "string" ? judge.systemPrompt : "",
|
|
573
|
+
judgeTimeoutMs: typeof judge.timeoutMs === "number" ? String(judge.timeoutMs) : "",
|
|
574
|
+
judgeMaxTokens: typeof judge.maxTokens === "number" ? String(judge.maxTokens) : "",
|
|
575
|
+
judgeConcurrency: typeof judge.concurrency === "number" ? String(judge.concurrency) : "",
|
|
576
|
+
levels: serializeLevels(value.levels, t)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/** Pretty-print the levels object, or '' when absent. */
|
|
580
|
+
function serializeLevels(levels, t) {
|
|
581
|
+
if (levels === void 0 || levels === null) return "";
|
|
582
|
+
try {
|
|
583
|
+
return JSON.stringify(levels, null, 2);
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/** Styles (inline). */
|
|
589
|
+
const styles = {
|
|
590
|
+
root: {
|
|
591
|
+
display: "flex",
|
|
592
|
+
flexDirection: "column",
|
|
593
|
+
gap: 12
|
|
594
|
+
},
|
|
595
|
+
intro: {
|
|
596
|
+
margin: 0,
|
|
597
|
+
color: "#6b7280",
|
|
598
|
+
fontSize: 13,
|
|
599
|
+
lineHeight: "18px"
|
|
600
|
+
},
|
|
601
|
+
card: {
|
|
602
|
+
border: "1px solid #e5e7eb",
|
|
603
|
+
borderRadius: 10,
|
|
604
|
+
padding: 12,
|
|
605
|
+
display: "flex",
|
|
606
|
+
flexDirection: "column",
|
|
607
|
+
gap: 10,
|
|
608
|
+
backgroundColor: "#f8fafc"
|
|
609
|
+
},
|
|
610
|
+
row: {
|
|
611
|
+
display: "flex",
|
|
612
|
+
flexDirection: "column",
|
|
613
|
+
gap: 4
|
|
614
|
+
},
|
|
615
|
+
fieldLabel: {
|
|
616
|
+
color: "#374151",
|
|
617
|
+
fontSize: 12
|
|
618
|
+
},
|
|
619
|
+
select: {
|
|
620
|
+
backgroundColor: "#ffffff",
|
|
621
|
+
color: "#111827",
|
|
622
|
+
border: "1px solid #cbd5e1",
|
|
623
|
+
borderRadius: 6,
|
|
624
|
+
padding: "6px 8px",
|
|
625
|
+
fontSize: 13
|
|
626
|
+
},
|
|
627
|
+
input: {
|
|
628
|
+
backgroundColor: "#ffffff",
|
|
629
|
+
color: "#111827",
|
|
630
|
+
border: "1px solid #cbd5e1",
|
|
631
|
+
borderRadius: 6,
|
|
632
|
+
padding: "6px 8px",
|
|
633
|
+
fontSize: 13,
|
|
634
|
+
width: "100%",
|
|
635
|
+
boxSizing: "border-box"
|
|
636
|
+
},
|
|
637
|
+
textarea: {
|
|
638
|
+
backgroundColor: "#ffffff",
|
|
639
|
+
color: "#111827",
|
|
640
|
+
border: "1px solid #cbd5e1",
|
|
641
|
+
borderRadius: 6,
|
|
642
|
+
padding: "6px 8px",
|
|
643
|
+
fontSize: 13,
|
|
644
|
+
width: "100%",
|
|
645
|
+
boxSizing: "border-box",
|
|
646
|
+
minHeight: 90,
|
|
647
|
+
fontFamily: "monospace"
|
|
648
|
+
},
|
|
649
|
+
modesRow: {
|
|
650
|
+
display: "flex",
|
|
651
|
+
flexDirection: "column",
|
|
652
|
+
gap: 6
|
|
653
|
+
},
|
|
654
|
+
modeLabel: {
|
|
655
|
+
display: "flex",
|
|
656
|
+
alignItems: "center",
|
|
657
|
+
gap: 6,
|
|
658
|
+
color: "#374151",
|
|
659
|
+
fontSize: 13
|
|
660
|
+
},
|
|
661
|
+
grid: {
|
|
662
|
+
display: "grid",
|
|
663
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
664
|
+
gap: 8
|
|
665
|
+
},
|
|
666
|
+
primaryButton: {
|
|
667
|
+
backgroundColor: "#2563eb",
|
|
668
|
+
color: "#ffffff",
|
|
669
|
+
border: "none",
|
|
670
|
+
borderRadius: 6,
|
|
671
|
+
padding: "6px 14px",
|
|
672
|
+
fontSize: 13,
|
|
673
|
+
cursor: "pointer"
|
|
674
|
+
},
|
|
675
|
+
primaryDisabled: {
|
|
676
|
+
opacity: .5,
|
|
677
|
+
cursor: "not-allowed"
|
|
678
|
+
},
|
|
679
|
+
hint: {
|
|
680
|
+
margin: 0,
|
|
681
|
+
color: "#6b7280",
|
|
682
|
+
fontSize: 11
|
|
683
|
+
},
|
|
684
|
+
result: {
|
|
685
|
+
fontSize: 12,
|
|
686
|
+
lineHeight: "16px"
|
|
687
|
+
},
|
|
688
|
+
resultOk: { color: "#16a34a" },
|
|
689
|
+
resultErr: { color: "#dc2626" },
|
|
690
|
+
presetInfo: {
|
|
691
|
+
border: "1px solid #e5e7eb",
|
|
692
|
+
borderRadius: 8,
|
|
693
|
+
padding: "8px 10px",
|
|
694
|
+
backgroundColor: "#f1f5f9",
|
|
695
|
+
display: "flex",
|
|
696
|
+
flexDirection: "column",
|
|
697
|
+
gap: 6
|
|
698
|
+
},
|
|
699
|
+
presetTable: {
|
|
700
|
+
display: "flex",
|
|
701
|
+
flexDirection: "column",
|
|
702
|
+
gap: 2
|
|
703
|
+
},
|
|
704
|
+
presetRow: {
|
|
705
|
+
display: "flex",
|
|
706
|
+
justifyContent: "space-between",
|
|
707
|
+
gap: 8,
|
|
708
|
+
fontSize: 12,
|
|
709
|
+
color: "#374151"
|
|
710
|
+
},
|
|
711
|
+
presetValue: {
|
|
712
|
+
color: "#6b7280",
|
|
713
|
+
fontFamily: "monospace"
|
|
714
|
+
},
|
|
715
|
+
presetPromptText: {
|
|
716
|
+
margin: 0,
|
|
717
|
+
color: "#6b7280",
|
|
718
|
+
fontSize: 12,
|
|
719
|
+
lineHeight: "16px"
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
/**
|
|
723
|
+
* The YOLO settings section.
|
|
724
|
+
*
|
|
725
|
+
* @param {object} props - slot-delivered inject face: { store, useSnapshot, t }
|
|
726
|
+
* plus optional ownerProps.close.
|
|
727
|
+
*/
|
|
728
|
+
function SettingsSection(props) {
|
|
729
|
+
const { store, useSnapshot, t, close } = props;
|
|
730
|
+
if (store === void 0 || useSnapshot === void 0 || t === void 0) return null;
|
|
731
|
+
return ReactSection({
|
|
732
|
+
store,
|
|
733
|
+
useSnapshot,
|
|
734
|
+
t,
|
|
735
|
+
close
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
function ReactSection({ store, useSnapshot, t, close }) {
|
|
739
|
+
const state = useSnapshot((s) => s);
|
|
740
|
+
const statusInfo = state.statusInfo && typeof state.statusInfo === "object" ? state.statusInfo : {};
|
|
741
|
+
const presetDefaults = statusInfo.presetDefaults && typeof statusInfo.presetDefaults === "object" ? statusInfo.presetDefaults : {};
|
|
742
|
+
(0, react.useEffect)(() => {
|
|
743
|
+
store.load();
|
|
744
|
+
}, [store]);
|
|
745
|
+
const view = state.view;
|
|
746
|
+
const writable = state.writable;
|
|
747
|
+
const [draft, setDraft] = (0, react.useState)(() => draftFromView(view, t));
|
|
748
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
749
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
750
|
+
const [done, setDone] = (0, react.useState)(false);
|
|
751
|
+
const providerOptions = (Array.isArray(state.providers) ? state.providers : []).filter((p) => p && typeof p.provider === "string" && p.provider !== "").map((p) => ({
|
|
752
|
+
id: p.provider,
|
|
753
|
+
label: p.displayName && typeof p.displayName === "string" && p.displayName !== "" ? p.displayName : void 0
|
|
754
|
+
}));
|
|
755
|
+
const providerGroup = (Array.isArray(state.models) ? state.models : []).find((g) => g && g.id === draft.judgeProvider);
|
|
756
|
+
const modelOptions = providerGroup && Array.isArray(providerGroup.models) ? providerGroup.models.filter((m) => m && typeof m.id === "string" && m.id !== "").map((m) => ({
|
|
757
|
+
id: m.id,
|
|
758
|
+
label: m.name && typeof m.name === "string" && m.name !== "" ? m.name : void 0
|
|
759
|
+
})) : [];
|
|
760
|
+
/** Pick a provider; clear the model when it no longer belongs to it. */
|
|
761
|
+
const setProvider = (value) => {
|
|
762
|
+
const next = Object.assign({}, draft, { judgeProvider: value });
|
|
763
|
+
const model = draft.judgeModel;
|
|
764
|
+
if (model !== "" && !modelBelongsTo(value, model, state.models)) next.judgeModel = "";
|
|
765
|
+
setDraft(next);
|
|
766
|
+
setDone(false);
|
|
767
|
+
setFailure(void 0);
|
|
768
|
+
};
|
|
769
|
+
(0, react.useEffect)(() => {
|
|
770
|
+
if (busy) return;
|
|
771
|
+
setDraft(draftFromView(view, t));
|
|
772
|
+
}, [
|
|
773
|
+
view?.value?.preset,
|
|
774
|
+
view?.value?.modes,
|
|
775
|
+
view?.value?.levels,
|
|
776
|
+
view?.value?.judge?.provider,
|
|
777
|
+
view?.value?.judge?.model,
|
|
778
|
+
view?.value?.judge?.systemPrompt,
|
|
779
|
+
view?.value?.judge?.timeoutMs,
|
|
780
|
+
view?.value?.judge?.maxTokens,
|
|
781
|
+
view?.value?.judge?.concurrency
|
|
782
|
+
]);
|
|
783
|
+
const levelsInvalid = !parseLevels(draft.levels).ok && trimToNull(draft.levels) != null;
|
|
784
|
+
/** Map a store.mutate failure outcome to a user-facing message. */
|
|
785
|
+
const kindMessage = (out) => {
|
|
786
|
+
if (out.kind === "conflict") return t("conflict");
|
|
787
|
+
if (out.kind === "rejected") return t("rejected");
|
|
788
|
+
return t("fatal");
|
|
789
|
+
};
|
|
790
|
+
const save = async () => {
|
|
791
|
+
setBusy(true);
|
|
792
|
+
setFailure(void 0);
|
|
793
|
+
setDone(false);
|
|
794
|
+
try {
|
|
795
|
+
const ops = buildOps(view, draft, t);
|
|
796
|
+
const out = await store.mutate(ops);
|
|
797
|
+
if (!out.ok) setFailure(kindMessage(out));
|
|
798
|
+
else setDone(true);
|
|
799
|
+
} finally {
|
|
800
|
+
setBusy(false);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
const toggleMode = (id) => {
|
|
804
|
+
const next = draft.modes.includes(id) ? draft.modes.filter((m) => m !== id) : draft.modes.concat([id]);
|
|
805
|
+
setDraft(Object.assign({}, draft, { modes: next }));
|
|
806
|
+
setDone(false);
|
|
807
|
+
setFailure(void 0);
|
|
808
|
+
};
|
|
809
|
+
const setField = (name, value) => {
|
|
810
|
+
setDraft(Object.assign({}, draft, { [name]: value }));
|
|
811
|
+
setDone(false);
|
|
812
|
+
setFailure(void 0);
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Preset change: switch the preset and pre-fill the judge system prompt and
|
|
816
|
+
* the levels from the preset's defaults served in statusInfo (custom → clear
|
|
817
|
+
* both so the user writes their own). Implemented as one combined draft
|
|
818
|
+
* update: several setField calls computed from the same stale closure would
|
|
819
|
+
* clobber each other under React's batched state updates.
|
|
820
|
+
*/
|
|
821
|
+
const changePreset = (value) => {
|
|
822
|
+
const next = Object.assign({}, draft, { preset: value });
|
|
823
|
+
if (value === "custom") {
|
|
824
|
+
next.judgeSystemPrompt = "";
|
|
825
|
+
next.levels = "";
|
|
826
|
+
} else {
|
|
827
|
+
const pd = presetDefaults[value];
|
|
828
|
+
if (pd && typeof pd === "object") {
|
|
829
|
+
next.judgeSystemPrompt = typeof pd.systemPrompt === "string" ? pd.systemPrompt : "";
|
|
830
|
+
next.levels = pd.levels === void 0 || pd.levels === null ? "" : JSON.stringify(pd.levels, null, 2);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
setDraft(next);
|
|
834
|
+
setDone(false);
|
|
835
|
+
setFailure(void 0);
|
|
836
|
+
};
|
|
837
|
+
if (state.status === "error") return (0, react.createElement)("div", { style: styles.root }, (0, react.createElement)("p", { style: styles.intro }, t("loadError") + (typeof state.error === "string" ? ": " + state.error : "")), (0, react.createElement)("button", {
|
|
838
|
+
style: Object.assign({}, styles.primaryButton, styles.primaryDisabled),
|
|
839
|
+
onClick: () => void store.load()
|
|
840
|
+
}, t("refresh")));
|
|
841
|
+
if (state.status !== "ready") return (0, react.createElement)("div", { style: styles.root }, (0, react.createElement)("p", { style: styles.intro }, t("sectionIntro")));
|
|
842
|
+
const preset = presetInfo(draft.preset);
|
|
843
|
+
return (0, react.createElement)("div", { style: styles.root }, (0, react.createElement)("p", { style: styles.intro }, t("sectionIntro")), (0, react.createElement)("div", { style: styles.card }, (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("preset")), (0, react.createElement)("select", {
|
|
844
|
+
style: styles.select,
|
|
845
|
+
value: draft.preset,
|
|
846
|
+
onChange: (e) => changePreset(e.target.value)
|
|
847
|
+
}, PRESETS.map((p) => (0, react.createElement)("option", {
|
|
848
|
+
key: p,
|
|
849
|
+
value: p
|
|
850
|
+
}, p))), (0, react.createElement)("p", { style: styles.hint }, t("presetHint"))), preset !== null ? presetInfoBlock(styles, t, preset) : null, (0, react.createElement)("div", { style: styles.modesRow }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("modes")), MODE_OPTIONS.map((m) => (0, react.createElement)("label", {
|
|
851
|
+
key: m.id,
|
|
852
|
+
style: styles.modeLabel
|
|
853
|
+
}, (0, react.createElement)("input", {
|
|
854
|
+
type: "checkbox",
|
|
855
|
+
checked: draft.modes.includes(m.id),
|
|
856
|
+
onChange: () => toggleMode(m.id)
|
|
857
|
+
}), t(m.key))), (0, react.createElement)("p", { style: styles.hint }, t("modesHint"))), (0, react.createElement)("div", { style: styles.grid }, fieldSelect(styles, t, draft, setProvider, "judgeProvider", "provider", providerOptions, t("providerEmpty")), judgeModelField(styles, t, draft, setField, modelOptions, t("modelEmpty")), fieldInput(styles, t, draft, setField, "judgeTimeoutMs", "timeoutMs"), fieldInput(styles, t, draft, setField, "judgeMaxTokens", "maxTokens"), fieldInput(styles, t, draft, setField, "judgeConcurrency", "concurrency")), (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("systemPrompt")), (0, react.createElement)("textarea", {
|
|
858
|
+
style: styles.textarea,
|
|
859
|
+
value: draft.judgeSystemPrompt,
|
|
860
|
+
onChange: (e) => setField("judgeSystemPrompt", e.target.value)
|
|
861
|
+
})), (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("levels") + " · " + t("levelsHint")), (0, react.createElement)("textarea", {
|
|
862
|
+
style: styles.textarea,
|
|
863
|
+
value: draft.levels,
|
|
864
|
+
onChange: (e) => setField("levels", e.target.value)
|
|
865
|
+
}), levelsInvalid ? (0, react.createElement)("p", { style: Object.assign({}, styles.result, styles.resultErr) }, t("invalidLevelsJson")) : null), (0, react.createElement)("div", { style: {
|
|
866
|
+
display: "flex",
|
|
867
|
+
gap: 8,
|
|
868
|
+
alignItems: "center"
|
|
869
|
+
} }, (0, react.createElement)("button", {
|
|
870
|
+
style: Object.assign({}, styles.primaryButton, !writable || busy || levelsInvalid ? styles.primaryDisabled : {}),
|
|
871
|
+
disabled: !writable || busy || levelsInvalid,
|
|
872
|
+
onClick: () => void save()
|
|
873
|
+
}, t("save")), done ? (0, react.createElement)("span", { style: Object.assign({}, styles.result, styles.resultOk) }, t("saved")) : failure !== void 0 ? (0, react.createElement)("span", { style: Object.assign({}, styles.result, styles.resultErr) }, failure) : null, close !== void 0 ? (0, react.createElement)("button", {
|
|
874
|
+
style: styles.primaryButton,
|
|
875
|
+
onClick: () => close()
|
|
876
|
+
}, t("close")) : null)));
|
|
877
|
+
}
|
|
878
|
+
function fieldInput(styles, t, draft, setField, name, labelKey) {
|
|
879
|
+
return (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t(labelKey)), (0, react.createElement)("input", {
|
|
880
|
+
style: styles.input,
|
|
881
|
+
value: draft[name],
|
|
882
|
+
onChange: (e) => setField(name, e.target.value)
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* A labeled <select> whose first option is always the "unconfigured" empty
|
|
887
|
+
* value, followed by the catalog options ({ id, label? }). The empty option
|
|
888
|
+
* doubles as the "clear back to composition base" choice.
|
|
889
|
+
*/
|
|
890
|
+
function fieldSelect(styles, t, draft, onChange, name, labelKey, options, emptyLabel) {
|
|
891
|
+
return (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t(labelKey)), (0, react.createElement)("select", {
|
|
892
|
+
style: styles.select,
|
|
893
|
+
value: draft[name],
|
|
894
|
+
onChange: (e) => onChange(e.target.value)
|
|
895
|
+
}, (0, react.createElement)("option", {
|
|
896
|
+
key: "empty",
|
|
897
|
+
value: ""
|
|
898
|
+
}, emptyLabel), options.map((o) => (0, react.createElement)("option", {
|
|
899
|
+
key: o.id,
|
|
900
|
+
value: o.id
|
|
901
|
+
}, o.label != null && o.label !== "" ? o.label : o.id))));
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Judge model field: a catalog-driven <select> when the selected provider's
|
|
905
|
+
* group lists models (and the draft value is one of them, or empty); a plain
|
|
906
|
+
* text input when there is no directory (llm face absent / fetch failed /
|
|
907
|
+
* unknown provider) or the stored value is unknown.
|
|
908
|
+
*/
|
|
909
|
+
function judgeModelField(styles, t, draft, setField, modelOptions, emptyLabel) {
|
|
910
|
+
if (modelOptions.length === 0) return fieldInput(styles, t, draft, setField, "judgeModel", "model");
|
|
911
|
+
if (!(draft.judgeModel === "" || modelOptions.some((o) => o.id === draft.judgeModel))) return fieldInput(styles, t, draft, setField, "judgeModel", "model");
|
|
912
|
+
return fieldSelect(styles, t, draft, (v) => setField("judgeModel", v), "judgeModel", "model", modelOptions, emptyLabel);
|
|
913
|
+
}
|
|
914
|
+
/** Read-only per-preset hint block: level table + error/unsure fallbacks + prompt summary. */
|
|
915
|
+
function presetInfoBlock(styles, t, preset) {
|
|
916
|
+
return (0, react.createElement)("div", { style: styles.presetInfo }, (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("presetLevels")), (0, react.createElement)("div", { style: styles.presetTable }, presetRow(styles, t, t("workspaceWrite"), preset.levels.workspaceWrite), presetRow(styles, t, t("dangerFullAccess"), preset.levels.dangerFullAccess), presetRow(styles, t, t("presetErrorFallback"), preset.fallbackError), presetRow(styles, t, t("presetUnsureFallback"), preset.fallbackUnsure))), (0, react.createElement)("div", { style: styles.row }, (0, react.createElement)("label", { style: styles.fieldLabel }, t("presetPrompt")), (0, react.createElement)("p", { style: styles.presetPromptText }, preset.prompt)));
|
|
917
|
+
}
|
|
918
|
+
/** One row of the preset level table; the 'custom' marker reads the user's levels table. */
|
|
919
|
+
function presetRow(styles, t, label, value) {
|
|
920
|
+
return (0, react.createElement)("div", { style: styles.presetRow }, (0, react.createElement)("span", null, label), (0, react.createElement)("span", { style: styles.presetValue }, value === "custom" ? t("presetCustom") : value));
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* True when modelId belongs to the provider's catalog group. When the provider
|
|
924
|
+
* has no catalog group (unknown provider / directory missing) membership
|
|
925
|
+
* cannot be judged — keep the value rather than clobbering a typed model.
|
|
926
|
+
*/
|
|
927
|
+
function modelBelongsTo(providerId, modelId, groups) {
|
|
928
|
+
if (!providerId || !modelId) return true;
|
|
929
|
+
const group = (Array.isArray(groups) ? groups : []).find((g) => g && g.id === providerId);
|
|
930
|
+
if (!group || !Array.isArray(group.models)) return true;
|
|
931
|
+
return group.models.some((m) => m && m.id === modelId);
|
|
932
|
+
}
|
|
933
|
+
/** Parse the levels textarea, tolerating a blank/whitespace value (treated as none). */
|
|
934
|
+
function parseLevels(text) {
|
|
935
|
+
const trimmed = text == null ? "" : text.trim();
|
|
936
|
+
if (trimmed.length === 0) return {
|
|
937
|
+
ok: true,
|
|
938
|
+
value: {}
|
|
939
|
+
};
|
|
940
|
+
try {
|
|
941
|
+
return {
|
|
942
|
+
ok: true,
|
|
943
|
+
value: JSON.parse(trimmed)
|
|
944
|
+
};
|
|
945
|
+
} catch {
|
|
946
|
+
return { ok: false };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function trimToNull(text) {
|
|
950
|
+
if (text == null) return null;
|
|
951
|
+
const trimmed = text.trim();
|
|
952
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Build path ops for the whole form. Numerical judge fields are parsed to
|
|
956
|
+
* numbers (blank → unset); text judge fields judgeField-shaped; modes/levels
|
|
957
|
+
* set whole.
|
|
958
|
+
*/
|
|
959
|
+
function buildOps(view, draft, t) {
|
|
960
|
+
const value = view && view.value != null ? view.value : {};
|
|
961
|
+
const judge = value.judge && typeof value.judge === "object" ? value.judge : {};
|
|
962
|
+
const ops = [];
|
|
963
|
+
ops.push.apply(ops, presetOps(draft.preset));
|
|
964
|
+
ops.push.apply(ops, modesOps(draft.modes));
|
|
965
|
+
ops.push.apply(ops, judgeFieldOps(judge.provider, ["judge", "provider"], draft.judgeProvider));
|
|
966
|
+
ops.push.apply(ops, judgeFieldOps(judge.model, ["judge", "model"], draft.judgeModel));
|
|
967
|
+
ops.push.apply(ops, judgeFieldOps(judge.systemPrompt, ["judge", "systemPrompt"], draft.judgeSystemPrompt));
|
|
968
|
+
const num = (raw, stored) => {
|
|
969
|
+
const trimmed = trimToNull(raw);
|
|
970
|
+
if (trimmed == null) return stored === void 0 ? void 0 : null;
|
|
971
|
+
const n = Number(trimmed);
|
|
972
|
+
return Number.isFinite(n) ? n : null;
|
|
973
|
+
};
|
|
974
|
+
const replaceField = (stored, path, valueOrNull) => {
|
|
975
|
+
if (valueOrNull === null) {
|
|
976
|
+
if (stored === void 0) return;
|
|
977
|
+
ops.push.apply(ops, unsetFieldOps(path));
|
|
978
|
+
} else if (valueOrNull !== void 0 && valueOrNull !== stored) ops.push.apply(ops, setFieldOps(path, valueOrNull));
|
|
979
|
+
};
|
|
980
|
+
replaceField(judge.timeoutMs, ["judge", "timeoutMs"], num(draft.judgeTimeoutMs, judge.timeoutMs));
|
|
981
|
+
replaceField(judge.maxTokens, ["judge", "maxTokens"], num(draft.judgeMaxTokens, judge.maxTokens));
|
|
982
|
+
replaceField(judge.concurrency, ["judge", "concurrency"], num(draft.judgeConcurrency, judge.concurrency));
|
|
983
|
+
const levels = parseLevels(draft.levels).value;
|
|
984
|
+
ops.push.apply(ops, levelsOps(levels));
|
|
985
|
+
return ops;
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/client/ui/Chip.js
|
|
989
|
+
/**
|
|
990
|
+
* YOLO mode status chip (slot `conversation.input.left`). Shows "YOLO <preset>"
|
|
991
|
+
* and toggles the shared popup via store.togglePopup().
|
|
992
|
+
*/
|
|
993
|
+
const chipStyle = {
|
|
994
|
+
display: "inline-flex",
|
|
995
|
+
alignItems: "center",
|
|
996
|
+
gap: 6,
|
|
997
|
+
padding: "4px 10px",
|
|
998
|
+
borderRadius: 999,
|
|
999
|
+
backgroundColor: "#ffffff",
|
|
1000
|
+
color: "#1d4ed8",
|
|
1001
|
+
border: "1px solid #93c5fd",
|
|
1002
|
+
fontSize: 12,
|
|
1003
|
+
cursor: "pointer",
|
|
1004
|
+
whiteSpace: "nowrap",
|
|
1005
|
+
userSelect: "none"
|
|
1006
|
+
};
|
|
1007
|
+
/** Status colour for the leading dot based on judge readiness. */
|
|
1008
|
+
function dotColor(statusInfo) {
|
|
1009
|
+
return statusInfo && statusInfo.judgeConfigured ? "#16a34a" : "#d97706";
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* @param {object} props - slot-delivered { store, useSnapshot, t } (inject face).
|
|
1013
|
+
*/
|
|
1014
|
+
function Chip(props) {
|
|
1015
|
+
const store = props.store;
|
|
1016
|
+
const useSnapshot = props.useSnapshot;
|
|
1017
|
+
if (store === void 0 || useSnapshot === void 0) return null;
|
|
1018
|
+
return ChipLoaded({
|
|
1019
|
+
store,
|
|
1020
|
+
useSnapshot
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
function ChipLoaded({ store, useSnapshot }) {
|
|
1024
|
+
(0, react.useEffect)(() => {
|
|
1025
|
+
store.load();
|
|
1026
|
+
}, [store]);
|
|
1027
|
+
const statusInfo = useSnapshot((s) => s).statusInfo;
|
|
1028
|
+
const preset = statusInfo && typeof statusInfo.preset === "string" ? statusInfo.preset : "…";
|
|
1029
|
+
return (0, react.createElement)("button", {
|
|
1030
|
+
style: chipStyle,
|
|
1031
|
+
title: "YOLO",
|
|
1032
|
+
onClick: () => store.togglePopup()
|
|
1033
|
+
}, (0, react.createElement)("span", { style: {
|
|
1034
|
+
width: 8,
|
|
1035
|
+
height: 8,
|
|
1036
|
+
borderRadius: 4,
|
|
1037
|
+
backgroundColor: dotColor(statusInfo),
|
|
1038
|
+
display: "inline-block"
|
|
1039
|
+
} }), "YOLO " + preset);
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/client/ui/Popup.js
|
|
1043
|
+
/**
|
|
1044
|
+
* YOLO mode status popup (slot `shell.overlay`). Renders null while the store's
|
|
1045
|
+
* open flag is false; otherwise a stats card plus the recent decisions table
|
|
1046
|
+
* (≤20, newest first) and a refresh button.
|
|
1047
|
+
*/
|
|
1048
|
+
const popupStyle = {
|
|
1049
|
+
position: "absolute",
|
|
1050
|
+
top: 56,
|
|
1051
|
+
right: 16,
|
|
1052
|
+
width: 520,
|
|
1053
|
+
maxWidth: "90vw",
|
|
1054
|
+
maxHeight: "70vh",
|
|
1055
|
+
overflow: "auto",
|
|
1056
|
+
backgroundColor: "#ffffff",
|
|
1057
|
+
color: "#111827",
|
|
1058
|
+
border: "1px solid #e5e7eb",
|
|
1059
|
+
borderRadius: 12,
|
|
1060
|
+
boxShadow: "0 12px 40px rgba(15,23,42,0.4)",
|
|
1061
|
+
padding: 14,
|
|
1062
|
+
display: "flex",
|
|
1063
|
+
flexDirection: "column",
|
|
1064
|
+
gap: 12,
|
|
1065
|
+
zIndex: 1e3
|
|
1066
|
+
};
|
|
1067
|
+
const headerStyle = {
|
|
1068
|
+
display: "flex",
|
|
1069
|
+
alignItems: "center",
|
|
1070
|
+
justifyContent: "space-between"
|
|
1071
|
+
};
|
|
1072
|
+
const statGrid = {
|
|
1073
|
+
display: "grid",
|
|
1074
|
+
gridTemplateColumns: "repeat(4, 1fr)",
|
|
1075
|
+
gap: 8
|
|
1076
|
+
};
|
|
1077
|
+
const statCell = {
|
|
1078
|
+
border: "1px solid #e5e7eb",
|
|
1079
|
+
borderRadius: 8,
|
|
1080
|
+
padding: "8px 6px",
|
|
1081
|
+
textAlign: "center",
|
|
1082
|
+
backgroundColor: "#f8fafc"
|
|
1083
|
+
};
|
|
1084
|
+
const statValue = {
|
|
1085
|
+
fontSize: 18,
|
|
1086
|
+
fontWeight: 600,
|
|
1087
|
+
color: "#111827"
|
|
1088
|
+
};
|
|
1089
|
+
const statLabel = {
|
|
1090
|
+
fontSize: 11,
|
|
1091
|
+
color: "#6b7280",
|
|
1092
|
+
marginTop: 2
|
|
1093
|
+
};
|
|
1094
|
+
const tableStyle = {
|
|
1095
|
+
width: "100%",
|
|
1096
|
+
borderCollapse: "collapse",
|
|
1097
|
+
fontSize: 12
|
|
1098
|
+
};
|
|
1099
|
+
const thStyle = {
|
|
1100
|
+
color: "#374151",
|
|
1101
|
+
textAlign: "left",
|
|
1102
|
+
padding: "4px 6px",
|
|
1103
|
+
borderBottom: "1px solid #e5e7eb",
|
|
1104
|
+
backgroundColor: "#f1f5f9",
|
|
1105
|
+
fontWeight: 500
|
|
1106
|
+
};
|
|
1107
|
+
const tdStyle = {
|
|
1108
|
+
padding: "4px 6px",
|
|
1109
|
+
borderBottom: "1px solid #e5e7eb",
|
|
1110
|
+
verticalAlign: "top"
|
|
1111
|
+
};
|
|
1112
|
+
const codeStyle = {
|
|
1113
|
+
fontFamily: "monospace",
|
|
1114
|
+
fontSize: 11,
|
|
1115
|
+
color: "#374151"
|
|
1116
|
+
};
|
|
1117
|
+
const primaryButton = {
|
|
1118
|
+
backgroundColor: "#2563eb",
|
|
1119
|
+
color: "#ffffff",
|
|
1120
|
+
border: "none",
|
|
1121
|
+
borderRadius: 6,
|
|
1122
|
+
padding: "5px 12px",
|
|
1123
|
+
fontSize: 12,
|
|
1124
|
+
cursor: "pointer"
|
|
1125
|
+
};
|
|
1126
|
+
/** Truncate long reason text for display. */
|
|
1127
|
+
function truncate(text, max) {
|
|
1128
|
+
if (typeof text !== "string") return "";
|
|
1129
|
+
return text.length > max ? text.slice(0, max) + "…" : text;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* @param {object} props - slot-delivered { store, useSnapshot, t }.
|
|
1133
|
+
*/
|
|
1134
|
+
function Popup(props) {
|
|
1135
|
+
const store = props.store;
|
|
1136
|
+
const useSnapshot = props.useSnapshot;
|
|
1137
|
+
const t = props.t;
|
|
1138
|
+
if (store === void 0 || useSnapshot === void 0 || t === void 0) return null;
|
|
1139
|
+
const state = useSnapshot((s) => s);
|
|
1140
|
+
if (!state.open) return null;
|
|
1141
|
+
const info = state.statusInfo;
|
|
1142
|
+
const stats = info && info.stats ? info.stats : {};
|
|
1143
|
+
const recent = info && Array.isArray(info.recent) ? info.recent.slice(0, 20) : [];
|
|
1144
|
+
return (0, react.createElement)("div", { style: popupStyle }, (0, react.createElement)("div", { style: headerStyle }, (0, react.createElement)("strong", {}, t("chip")), (0, react.createElement)("button", {
|
|
1145
|
+
style: primaryButton,
|
|
1146
|
+
onClick: () => void store.load()
|
|
1147
|
+
}, t("refresh"))), (0, react.createElement)("div", { style: statGrid }, statCellOf(t, "statsTotal", stats.total), statCellOf(t, "statsAllowed", stats.allowed), statCellOf(t, "statsRejected", stats.rejected), statCellOf(t, "statsDelegated", stats.delegated)), recent.length === 0 ? (0, react.createElement)("p", { style: {
|
|
1148
|
+
color: "#6b7280",
|
|
1149
|
+
fontSize: 12,
|
|
1150
|
+
margin: 0
|
|
1151
|
+
} }, t("recentEmpty")) : (0, react.createElement)("table", { style: tableStyle }, (0, react.createElement)("thead", {}, (0, react.createElement)("tr", {}, (0, react.createElement)("th", { style: thStyle }, t("tableTime")), (0, react.createElement)("th", { style: thStyle }, t("tableTool")), (0, react.createElement)("th", { style: thStyle }, t("tableTarget")), (0, react.createElement)("th", { style: thStyle }, t("tableDecision")), (0, react.createElement)("th", { style: thStyle }, t("tableOutcome")), (0, react.createElement)("th", { style: thStyle }, t("tableReason")))), (0, react.createElement)("tbody", {}, recent.map((row, idx) => tableRow(t, row, idx)))), (0, react.createElement)("div", { style: {
|
|
1152
|
+
display: "flex",
|
|
1153
|
+
justifyContent: "flex-end"
|
|
1154
|
+
} }, (0, react.createElement)("button", {
|
|
1155
|
+
style: primaryButton,
|
|
1156
|
+
onClick: () => store.togglePopup()
|
|
1157
|
+
}, t("close"))));
|
|
1158
|
+
}
|
|
1159
|
+
function statCellOf(t, key, value) {
|
|
1160
|
+
return (0, react.createElement)("div", { style: statCell }, (0, react.createElement)("div", { style: statValue }, value === void 0 || value === null ? "–" : String(value)), (0, react.createElement)("div", { style: statLabel }, t(key)));
|
|
1161
|
+
}
|
|
1162
|
+
function tableRow(t, row, idx) {
|
|
1163
|
+
return (0, react.createElement)("tr", { key: idx }, (0, react.createElement)("td", { style: tdStyle }, timeText(row.time)), (0, react.createElement)("td", { style: tdStyle }, row.toolName || "–"), (0, react.createElement)("td", { style: tdStyle }, (0, react.createElement)("span", { style: codeStyle }, row.targetMode || "–")), (0, react.createElement)("td", { style: tdStyle }, row.decision || "–"), (0, react.createElement)("td", { style: tdStyle }, row.outcome || "–"), (0, react.createElement)("td", { style: tdStyle }, truncate(row.reason, 48) || "–"));
|
|
1164
|
+
}
|
|
1165
|
+
function timeText(value) {
|
|
1166
|
+
if (typeof value === "string") return value;
|
|
1167
|
+
if (typeof value === "number") {
|
|
1168
|
+
const d = new Date(value);
|
|
1169
|
+
if (!Number.isNaN(d.getTime())) return d.toLocaleTimeString();
|
|
1170
|
+
return String(value);
|
|
1171
|
+
}
|
|
1172
|
+
return "–";
|
|
1173
|
+
}
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/client/index.js
|
|
1176
|
+
/**
|
|
1177
|
+
* YOLO mode — browser half (DSH client plugin).
|
|
1178
|
+
*
|
|
1179
|
+
* Registers the `settings.section` page (id 'yolo-mode') that configures the
|
|
1180
|
+
* preset/modes/judge/levels, the `conversation.input.left` status chip, and the
|
|
1181
|
+
* `shell.overlay` stats/recent popup. Data flows through the connection's
|
|
1182
|
+
* generic RPC channel (/yolo-mode) into a snapshot store; writes travel as path
|
|
1183
|
+
* ops through settingsMutate with an optimistic-revision lock.
|
|
1184
|
+
*/
|
|
1185
|
+
/**
|
|
1186
|
+
* Services required by these slot registrations (dsh.client.inject is the same
|
|
1187
|
+
* short-name set): slots, locale, connection, remote.
|
|
1188
|
+
*/
|
|
1189
|
+
const inject = [
|
|
1190
|
+
"slots",
|
|
1191
|
+
"locale",
|
|
1192
|
+
"connection",
|
|
1193
|
+
"remote"
|
|
1194
|
+
];
|
|
1195
|
+
/**
|
|
1196
|
+
* Register the YOLO section/chip/popup once the slot declarations are on the
|
|
1197
|
+
* ledger, wire the store to the connection, and keep it fresh on every pushed
|
|
1198
|
+
* invalidation.
|
|
1199
|
+
*
|
|
1200
|
+
* @param {object} ctx - client cordis context.
|
|
1201
|
+
*/
|
|
1202
|
+
function apply(ctx) {
|
|
1203
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1204
|
+
zh,
|
|
1205
|
+
en
|
|
1206
|
+
}), "yolo-mode: copy dictionaries");
|
|
1207
|
+
const connection = ctx.get("connection");
|
|
1208
|
+
if (connection === void 0 || connection === null) return;
|
|
1209
|
+
const t = ctx.locale.bind(NS);
|
|
1210
|
+
const store = new YoloStore({
|
|
1211
|
+
rpc: connection.rpc,
|
|
1212
|
+
llm: connection.api ? connection.api.llm : null
|
|
1213
|
+
});
|
|
1214
|
+
const useSnapshot = (0, _deepseek_ai_dsh_client_web_react.bindSnapshotSelector)(store);
|
|
1215
|
+
ctx.effect(() => {
|
|
1216
|
+
const refresh = (ns) => {
|
|
1217
|
+
if (ns !== void 0 && ns !== "yolo-mode") return;
|
|
1218
|
+
store.load();
|
|
1219
|
+
};
|
|
1220
|
+
const disposers = [
|
|
1221
|
+
ctx.remote == null ? () => {} : ctx.remote.$on("settings/document-updated", refresh),
|
|
1222
|
+
ctx.remote == null ? () => {} : ctx.remote.$on("llm/adapters-updated", () => void store.load()),
|
|
1223
|
+
ctx.on("connection/reset", () => void store.load())
|
|
1224
|
+
];
|
|
1225
|
+
return () => {
|
|
1226
|
+
for (const dispose of disposers) if (dispose) dispose();
|
|
1227
|
+
};
|
|
1228
|
+
}, "yolo-mode: pushed invalidations");
|
|
1229
|
+
const injected = () => ({
|
|
1230
|
+
store,
|
|
1231
|
+
useSnapshot,
|
|
1232
|
+
t
|
|
1233
|
+
});
|
|
1234
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1235
|
+
name: "settings.section",
|
|
1236
|
+
id: "yolo-mode",
|
|
1237
|
+
order: 25,
|
|
1238
|
+
label: () => t("nav"),
|
|
1239
|
+
locale: NS,
|
|
1240
|
+
inject: injected
|
|
1241
|
+
}, SettingsSection));
|
|
1242
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
1243
|
+
name: "conversation.input.left",
|
|
1244
|
+
id: "yolo-mode-chip",
|
|
1245
|
+
order: 0,
|
|
1246
|
+
label: () => t("chip"),
|
|
1247
|
+
locale: NS,
|
|
1248
|
+
inject: injected
|
|
1249
|
+
}, Chip));
|
|
1250
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1251
|
+
name: "shell.overlay",
|
|
1252
|
+
id: "yolo-mode-popup",
|
|
1253
|
+
order: 0,
|
|
1254
|
+
label: () => t("chip"),
|
|
1255
|
+
locale: NS,
|
|
1256
|
+
inject: injected
|
|
1257
|
+
}, Popup));
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
exports.NS = NS;
|
|
1261
|
+
exports.apply = apply;
|
|
1262
|
+
exports.en = en;
|
|
1263
|
+
exports.inject = inject;
|
|
1264
|
+
exports.zh = zh;
|
|
1265
|
+
|
|
1266
|
+
return module.exports;
|
|
1267
|
+
}
|
|
1268
|
+
});
|