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.
@@ -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
+ });