dsh-plugin-rollout-scout 1.3.1
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/LICENSE +21 -0
- package/README.en.md +134 -0
- package/README.md +132 -0
- package/_wrap-client.mjs +32 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1319 -0
- package/lib/fixtures.js +145 -0
- package/lib/index.js +1583 -0
- package/package.json +54 -0
- package/plugin.client.js +1295 -0
package/plugin.client.js
ADDED
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
// dsh-plugin-rollout-scout — client half.
|
|
2
|
+
//
|
|
3
|
+
// A full-frame console that drives the host's probe loop: enter a prompt,
|
|
4
|
+
// pick model / concurrency / folder, press Start, and watch probes stream.
|
|
5
|
+
// Each probe carries a live rollout confidence built from how its paragraphs
|
|
6
|
+
// open; probes that read as the old model are cancelled mid-thought, and
|
|
7
|
+
// confident catches are kept. Start toggles to Pause, which stops launching
|
|
8
|
+
// while letting live probes finish, and back to Resume.
|
|
9
|
+
|
|
10
|
+
const ROUTE = '/rollout-scout';
|
|
11
|
+
const POLL_MS = 800;
|
|
12
|
+
|
|
13
|
+
// The shell's own icon set, so the sidebar row reads as part of the app rather
|
|
14
|
+
// than as a plugin bolted on. Optional: an inline glyph stands in if the
|
|
15
|
+
// package is not resolvable from the plugin sandbox.
|
|
16
|
+
let primitives = null;
|
|
17
|
+
try {
|
|
18
|
+
if (typeof require === 'function') primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
19
|
+
} catch (e) {}
|
|
20
|
+
|
|
21
|
+
// The console covers the whole window, so it is mounted on <body> rather than
|
|
22
|
+
// left inside the shell's overlay layer. `position:fixed` resolves against the
|
|
23
|
+
// viewport only while no ancestor establishes a containing block — a single
|
|
24
|
+
// transform, filter, contain or container-type anywhere above it silently
|
|
25
|
+
// re-anchors the surface to that element's box and it stops short of the
|
|
26
|
+
// window edge. Portalling removes the dependency instead of tracking it.
|
|
27
|
+
let reactDom = null;
|
|
28
|
+
try {
|
|
29
|
+
if (typeof require === 'function') reactDom = require('react-dom');
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
|
|
32
|
+
function realGlobal() {
|
|
33
|
+
try { if (typeof window !== 'undefined' && window) return window; } catch (e) {}
|
|
34
|
+
try { if (typeof globalThis !== 'undefined' && globalThis) return globalThis; } catch (e) {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function api(method, body) {
|
|
39
|
+
const g = realGlobal();
|
|
40
|
+
const res = await g.fetch(ROUTE, method === 'GET' ? { cache: 'no-store' } : {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
const value = await res.json().catch(function () { return {}; });
|
|
46
|
+
if (!res.ok) throw new Error(value.error || ('HTTP ' + res.status));
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const FORM_KEY = 'dsh-plugin-rollout-scout:form';
|
|
51
|
+
// Rides in the same saved form blob as the rest of the console's preferences.
|
|
52
|
+
const QUIET_KEY = 'preflightDismissed';
|
|
53
|
+
|
|
54
|
+
function loadForm() {
|
|
55
|
+
const g = realGlobal();
|
|
56
|
+
try {
|
|
57
|
+
const raw = g && g.localStorage && g.localStorage.getItem(FORM_KEY);
|
|
58
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
59
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function saveForm(form) {
|
|
66
|
+
const g = realGlobal();
|
|
67
|
+
try { if (g && g.localStorage) g.localStorage.setItem(FORM_KEY, JSON.stringify(form)); } catch (e) {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Statuses where the probe can still be discarded, so hovering it is
|
|
71
|
+
// meaningful. Hover on any other card is just a mouse passing over a
|
|
72
|
+
// finished row and must not generate traffic.
|
|
73
|
+
const RESCUABLE = {
|
|
74
|
+
starting: true,
|
|
75
|
+
streaming: true,
|
|
76
|
+
'kept-streaming': true,
|
|
77
|
+
'pending-discard': true,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const STATUS_TONE = {
|
|
81
|
+
starting: 'wait',
|
|
82
|
+
streaming: 'wait',
|
|
83
|
+
'kept-streaming': 'good',
|
|
84
|
+
kept: 'good',
|
|
85
|
+
'pending-discard': 'bad',
|
|
86
|
+
pinned: 'wait',
|
|
87
|
+
discarding: 'bad',
|
|
88
|
+
discarded: 'bad',
|
|
89
|
+
finished: 'neutral',
|
|
90
|
+
stopped: 'neutral',
|
|
91
|
+
error: 'bad',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function pct(score) {
|
|
95
|
+
return Math.round((typeof score === 'number' ? score : 0.5) * 100);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const CSS = [
|
|
99
|
+
/* -- sidebar seat ------------------------------------------------------ *
|
|
100
|
+
* Geometry is copied from the shell's own Settings trigger row so the two
|
|
101
|
+
* sit flush at the sidebar foot: same height, radius, gap and negative
|
|
102
|
+
* margins in the wide column, same 36px circle in the 56px rail.
|
|
103
|
+
*
|
|
104
|
+
* Tracked against dsh 0.1.1-rc.2. That row was 34px tall with different
|
|
105
|
+
* insets in 0.1.0-rc.x, so on an older harness this sits a few pixels
|
|
106
|
+
* short of Settings — cosmetic, and it corrects itself on upgrade. */
|
|
107
|
+
'.rsc-seat{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:12px;flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}',
|
|
108
|
+
'.rsc-seat:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
109
|
+
'.rsc-seat[data-rail]{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}',
|
|
110
|
+
'.rsc-seat[data-open]{background:var(--dsw-specific-sidebar-nav-item-active,var(--dsw-alias-interactive-bg-hover))}',
|
|
111
|
+
'.rsc-seat-icon{flex:none;position:relative;display:inline-flex;align-items:center;justify-content:center}',
|
|
112
|
+
'.rsc-seat-label{flex:1;min-width:0;text-align:left;white-space:nowrap;overflow:hidden}',
|
|
113
|
+
'.rsc-seat-meta{flex:none;font-size:11.5px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
114
|
+
/* The rail hides the label, so the run has to read from the icon alone. */
|
|
115
|
+
'.rsc-pip{position:absolute;top:-2px;right:-3px;width:7px;height:7px;border-radius:50%;box-shadow:0 0 0 2px var(--dsw-specific-sidebar-fill,#1e1e22)}',
|
|
116
|
+
'.rsc-pip[data-tone=live]{background:var(--dsw-alias-accent-primary,#4b8dff);animation:rsc-pulse 1.4s ease-in-out infinite}',
|
|
117
|
+
'.rsc-pip[data-tone=paused]{background:var(--dsw-alias-label-secondary,#bbb)}',
|
|
118
|
+
'.rsc-pip[data-tone=caught]{background:#3fbf6f}',
|
|
119
|
+
'@keyframes rsc-pulse{0%,100%{opacity:1}50%{opacity:.35}}',
|
|
120
|
+
'.rsc-seat-badge{flex:none;min-width:17px;height:17px;padding:0 5px;border-radius:999px;background:#3fbf6f;color:#04210f;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center}',
|
|
121
|
+
|
|
122
|
+
/* -- full-frame surface ------------------------------------------------ */
|
|
123
|
+
'.rsc-full{position:fixed;inset:0;z-index:70;display:flex;flex-direction:column;background:color-mix(in srgb,var(--dsw-alias-bg-primary,#16161a) 94%,transparent);-webkit-backdrop-filter:blur(30px) saturate(1.4);backdrop-filter:blur(30px) saturate(1.4);color:var(--dsw-alias-label-primary);font-size:13px;animation:rsc-in 260ms cubic-bezier(.32,.72,0,1) both}',
|
|
124
|
+
'@keyframes rsc-in{from{opacity:0;transform:scale(.99)}to{opacity:1;transform:none}}',
|
|
125
|
+
/* DSH Desktop uses Electron's Windows title-bar overlay. The native window
|
|
126
|
+
* buttons therefore sit over the renderer instead of taking layout space.
|
|
127
|
+
* Electron exposes the unobstructed title-bar rectangle through these env()
|
|
128
|
+
* values; derive the occupied strip from it and keep the normal 22px inset
|
|
129
|
+
* as breathing room. Browsers and non-overlay shells take the fallbacks. */
|
|
130
|
+
'.rsc-top{display:flex;align-items:center;gap:14px;padding:16px max(22px,calc(100vw - env(titlebar-area-x,0px) - env(titlebar-area-width,100vw) + 22px)) 16px 22px;border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 22%,transparent)}',
|
|
131
|
+
'.rsc-h1{font-size:16px;font-weight:600}',
|
|
132
|
+
'.rsc-sub{font-size:12.5px;color:var(--dsw-alias-label-tertiary);flex:1}',
|
|
133
|
+
'.rsc-x{width:30px;height:30px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:15px}',
|
|
134
|
+
'.rsc-x:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
135
|
+
'.rsc-cols{flex:1;display:flex;min-height:0}',
|
|
136
|
+
'.rsc-leftwrap{width:340px;flex:none;min-height:0;display:flex;flex-direction:column;border-right:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 18%,transparent)}',
|
|
137
|
+
'.rsc-left{flex:1;min-height:0;overflow-y:auto;padding:18px 20px 30px;display:flex;flex-direction:column;gap:13px}',
|
|
138
|
+
'.rsc-right{flex:1;min-width:0;overflow-y:auto;padding:18px 22px 30px}',
|
|
139
|
+
|
|
140
|
+
/* -- form -------------------------------------------------------------- */
|
|
141
|
+
'.rsc-label{font-size:11.5px;color:var(--dsw-alias-label-tertiary);margin-bottom:4px;display:block}',
|
|
142
|
+
'.rsc-input,.rsc-select,.rsc-area{width:100%;box-sizing:border-box;border-radius:9px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 30%,transparent);background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 8%,transparent);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;padding:7px 9px;outline:none}',
|
|
143
|
+
'.rsc-input:focus,.rsc-select:focus,.rsc-area:focus{border-color:var(--dsw-alias-accent-primary,#4b8dff)}',
|
|
144
|
+
'.rsc-area{min-height:74px;resize:vertical;line-height:19px}',
|
|
145
|
+
'.rsc-row{display:flex;gap:9px}',
|
|
146
|
+
'.rsc-row>*{flex:1;min-width:0}',
|
|
147
|
+
'.rsc-check{display:flex;align-items:flex-start;gap:8px;cursor:pointer;font-size:12.5px;line-height:17px}',
|
|
148
|
+
'.rsc-check input{margin-top:1px;accent-color:var(--dsw-alias-accent-primary,#4b8dff)}',
|
|
149
|
+
'.rsc-check-input{display:flex;align-items:center;gap:8px;font-size:12.5px;line-height:17px}',
|
|
150
|
+
'.rsc-check-input .rsc-check{flex:1;min-width:0}',
|
|
151
|
+
'.rsc-check-input .rsc-input{width:68px;flex:none;padding:4px 7px;font-size:12px}',
|
|
152
|
+
'.rsc-unit{font-size:11.5px;color:var(--dsw-alias-label-tertiary);flex:none}',
|
|
153
|
+
/* Scoring takes four numbers; at 340px a single row wraps their labels and
|
|
154
|
+
* knocks the inputs out of alignment, so they pair off instead. */
|
|
155
|
+
'.rsc-grid2{display:grid;grid-template-columns:1fr 1fr;gap:9px}',
|
|
156
|
+
'.rsc-selfcheck{font-size:11.5px;padding:7px 10px;border-radius:9px;cursor:pointer;background:color-mix(in srgb,#3fbf6f 14%,transparent);color:#3fbf6f}',
|
|
157
|
+
'.rsc-selfcheck[data-bad]{background:color-mix(in srgb,#e5a23d 16%,transparent);color:#e5a23d}',
|
|
158
|
+
'.rsc-selfcheck-rows{margin-top:7px;display:flex;flex-direction:column;gap:3px;font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
159
|
+
'.rsc-selfcheck-row{display:flex;gap:7px;align-items:baseline}',
|
|
160
|
+
'.rsc-selfcheck-row>span:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
161
|
+
'.rsc-selfcheck-row[data-miss]{color:var(--dsw-alias-status-error,#e5484d)}',
|
|
162
|
+
'.rsc-selfcheck-row b{font-variant-numeric:tabular-nums;font-weight:600}',
|
|
163
|
+
/* Actions sit below the scroll, always reachable, ordered by weight: the
|
|
164
|
+
* primary action alone on top, the two run controls beside it, and the
|
|
165
|
+
* destructive one as text rather than a third slab competing for the eye. */
|
|
166
|
+
'.rsc-foot{flex:none;border-top:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 18%,transparent);padding:12px 20px 14px;display:flex;flex-direction:column;gap:8px;background:inherit}',
|
|
167
|
+
'.rsc-actions{display:flex;gap:9px;margin-top:3px}',
|
|
168
|
+
'.rsc-btn[data-wide]{width:100%;flex:none;padding:10px 0;font-size:13.5px;font-weight:600}',
|
|
169
|
+
'.rsc-btn[data-quiet]{border-color:transparent;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 12%,transparent)}',
|
|
170
|
+
'.rsc-btn[data-quiet]:hover{background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 22%,transparent)}',
|
|
171
|
+
'.rsc-textbtn{align-self:flex-start;border:0;background:0 0;padding:2px 0;font:inherit;font-size:11.5px;color:var(--dsw-alias-label-tertiary);cursor:pointer;text-decoration:underline;text-underline-offset:3px}',
|
|
172
|
+
'.rsc-textbtn:hover:not([disabled]){color:var(--dsw-alias-status-error,#e5484d)}',
|
|
173
|
+
'.rsc-textbtn[disabled]{opacity:.45;cursor:default}',
|
|
174
|
+
/* Long explanations and rarely-touched switches fold away instead of
|
|
175
|
+
* sitting between the controls they describe. */
|
|
176
|
+
'.rsc-fold{border-radius:9px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 7%,transparent)}',
|
|
177
|
+
'.rsc-fold>summary{cursor:pointer;list-style:none;padding:7px 10px;font-size:11.5px;color:var(--dsw-alias-label-secondary,#bbb)}',
|
|
178
|
+
'.rsc-fold>summary::-webkit-details-marker{display:none}',
|
|
179
|
+
'.rsc-fold>summary:before{content:"▸";display:inline-block;margin-right:6px;transition:transform 140ms ease}',
|
|
180
|
+
'.rsc-fold[open]>summary:before{transform:rotate(90deg)}',
|
|
181
|
+
'.rsc-fold-body{padding:0 10px 10px;display:flex;flex-direction:column;gap:10px}',
|
|
182
|
+
'.rsc-btn{flex:1;padding:9px 0;border-radius:999px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 34%,transparent);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;cursor:pointer;transition:background 140ms ease}',
|
|
183
|
+
'.rsc-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
184
|
+
'.rsc-btn[data-primary]{background:var(--dsw-alias-accent-primary,#4b8dff);border-color:transparent;color:#fff}',
|
|
185
|
+
'.rsc-btn[data-danger]{border-color:color-mix(in srgb,var(--dsw-alias-status-error,#e5484d) 55%,transparent);color:var(--dsw-alias-status-error,#e5484d)}',
|
|
186
|
+
'.rsc-btn[disabled]{opacity:.45;cursor:default}',
|
|
187
|
+
'.rsc-hint{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
|
|
188
|
+
'.rsc-note{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
189
|
+
'.rsc-error{font-size:12px;color:var(--dsw-alias-status-error,#e5484d)}',
|
|
190
|
+
'.rsc-sectionhead{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);margin-top:4px}',
|
|
191
|
+
|
|
192
|
+
/* -- probe cards ------------------------------------------------------- */
|
|
193
|
+
'.rsc-stats{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap}',
|
|
194
|
+
'.rsc-stat{padding:9px 14px;border-radius:12px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 10%,transparent);min-width:92px}',
|
|
195
|
+
'.rsc-stat-v{font-size:19px;font-weight:600;font-variant-numeric:tabular-nums}',
|
|
196
|
+
'.rsc-stat-k{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-top:1px}',
|
|
197
|
+
'.rsc-list{display:flex;flex-direction:column;gap:8px}',
|
|
198
|
+
'@keyframes rsc-row-in{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}',
|
|
199
|
+
'@keyframes rsc-row-out{from{opacity:1}to{opacity:.18}}',
|
|
200
|
+
'@keyframes rsc-leave-line{from{transform:scaleX(1)}to{transform:scaleX(0)}}',
|
|
201
|
+
'.rsc-item{position:relative;overflow:hidden;animation:rsc-row-in 300ms cubic-bezier(.32,.72,0,1) both;padding:11px 13px;border-radius:12px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 8%,transparent);border:1px solid transparent}',
|
|
202
|
+
'.rsc-item[data-leaving]{animation:rsc-row-out 3200ms linear forwards}',
|
|
203
|
+
'.rsc-item[data-leaving]::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--dsw-alias-status-error,#e5484d);transform-origin:left center;animation:rsc-leave-line 3200ms linear forwards}',
|
|
204
|
+
'.rsc-item[data-tone=good]{border-color:color-mix(in srgb,#3fbf6f 45%,transparent)}',
|
|
205
|
+
'.rsc-item[data-tone=bad]{opacity:.72}',
|
|
206
|
+
'.rsc-item[data-click]{cursor:pointer}',
|
|
207
|
+
'.rsc-item[data-click]:hover{background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 15%,transparent)}',
|
|
208
|
+
'.rsc-item-head{display:flex;align-items:center;gap:9px}',
|
|
209
|
+
'.rsc-item-dot{flex:none;width:8px;height:8px;border-radius:50%}',
|
|
210
|
+
'.rsc-item-dot[data-tone=wait]{background:var(--dsw-alias-accent-primary,#4b8dff);animation:rsc-pulse 1.4s ease-in-out infinite}',
|
|
211
|
+
'.rsc-item-dot[data-tone=good]{background:#3fbf6f}',
|
|
212
|
+
'.rsc-item-dot[data-tone=bad]{background:var(--dsw-alias-status-error,#e5484d)}',
|
|
213
|
+
'.rsc-item-dot[data-tone=neutral]{background:var(--dsw-alias-label-tertiary,#888)}',
|
|
214
|
+
'.rsc-item-name{font-size:13px;font-weight:600;flex:1}',
|
|
215
|
+
'.rsc-item-status{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}',
|
|
216
|
+
'.rsc-badge{font-size:10.5px;padding:2px 9px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 18%,transparent);color:var(--dsw-alias-label-secondary,#bbb)}',
|
|
217
|
+
'.rsc-badge[data-tone=good]{background:color-mix(in srgb,#3fbf6f 22%,transparent);color:#3fbf6f}',
|
|
218
|
+
'.rsc-badge[data-tone=bad]{background:color-mix(in srgb,var(--dsw-alias-status-error,#e5484d) 18%,transparent);color:var(--dsw-alias-status-error,#e5484d)}',
|
|
219
|
+
|
|
220
|
+
/* -- score meter ------------------------------------------------------- */
|
|
221
|
+
'.rsc-score{display:flex;align-items:center;gap:10px;margin-top:9px}',
|
|
222
|
+
'.rsc-meter{position:relative;flex:1;height:7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 20%,transparent);overflow:hidden}',
|
|
223
|
+
'.rsc-meter-fill{position:absolute;left:0;top:0;bottom:0;border-radius:999px;transition:width 260ms cubic-bezier(.32,.72,0,1),background 260ms ease}',
|
|
224
|
+
'.rsc-meter-mark{position:absolute;top:-2px;bottom:-2px;width:1px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#fff) 40%,transparent)}',
|
|
225
|
+
'.rsc-score-v{font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;min-width:38px;text-align:right}',
|
|
226
|
+
'.rsc-evidence{font-size:11px;color:var(--dsw-alias-label-tertiary);margin-top:5px;display:flex;gap:8px;flex-wrap:wrap}',
|
|
227
|
+
'.rsc-chip{padding:1px 7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 16%,transparent)}',
|
|
228
|
+
'.rsc-chip[data-sign=neg]{background:color-mix(in srgb,var(--dsw-alias-status-error,#e5484d) 16%,transparent);color:var(--dsw-alias-status-error,#e5484d)}',
|
|
229
|
+
'.rsc-chip[data-sign=pos]{background:color-mix(in srgb,#3fbf6f 16%,transparent);color:#3fbf6f}',
|
|
230
|
+
'.rsc-prev{font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-tertiary);margin-top:7px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}',
|
|
231
|
+
'.rsc-empty{color:var(--dsw-alias-label-tertiary);padding:40px 0;text-align:center;font-size:13px}',
|
|
232
|
+
|
|
233
|
+
/* -- protection + orphans ---------------------------------------------- */
|
|
234
|
+
'.rsc-lock{margin-left:auto;flex:none;border:0;border-radius:7px;padding:2px 7px;font:inherit;font-size:10.5px;cursor:pointer;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 16%,transparent);color:var(--dsw-alias-label-secondary,#bbb)}',
|
|
235
|
+
'.rsc-lock:hover{background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 28%,transparent)}',
|
|
236
|
+
'.rsc-lock[data-on]{background:color-mix(in srgb,#3fbf6f 22%,transparent);color:#3fbf6f}',
|
|
237
|
+
'.rsc-item[data-locked]{border-color:color-mix(in srgb,#3fbf6f 45%,transparent);opacity:1}',
|
|
238
|
+
'.rsc-banner{display:flex;align-items:center;gap:10px;padding:9px 12px;margin-bottom:12px;border-radius:10px;font-size:12px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 12%,transparent)}',
|
|
239
|
+
'.rsc-banner span{flex:1;min-width:0}',
|
|
240
|
+
'.rsc-banner button{flex:none;border:0;border-radius:7px;padding:4px 11px;font:inherit;font-size:11.5px;cursor:pointer;background:var(--dsw-alias-accent-primary,#4b8dff);color:#fff}',
|
|
241
|
+
'.rsc-banner button[disabled]{opacity:.45;cursor:default}',
|
|
242
|
+
'.rsc-rename{margin-top:8px}',
|
|
243
|
+
|
|
244
|
+
/* -- pre-flight dialog -------------------------------------------------- */
|
|
245
|
+
'.rsc-scrim{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,#000 45%,transparent);animation:rsc-in 160ms ease both}',
|
|
246
|
+
'.rsc-modal{width:440px;max-width:calc(100vw - 40px);border-radius:16px;padding:22px;background:var(--dsw-alias-bg-primary,#1e1e22);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 24%,transparent);box-shadow:0 24px 64px rgba(0,0,0,.45);display:flex;flex-direction:column;gap:13px}',
|
|
247
|
+
'.rsc-modal-h{font-size:15px;font-weight:600}',
|
|
248
|
+
'.rsc-modal-p{font-size:12.5px;line-height:19px;color:var(--dsw-alias-label-secondary,#bbb)}',
|
|
249
|
+
'.rsc-modal-state{display:flex;align-items:center;gap:9px;padding:10px 12px;border-radius:10px;font-size:12px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary,#888) 10%,transparent)}',
|
|
250
|
+
'.rsc-modal-state[data-tone=warn]{background:color-mix(in srgb,#e5a23d 16%,transparent);color:#e5a23d}',
|
|
251
|
+
'.rsc-modal-state[data-tone=ok]{background:color-mix(in srgb,#3fbf6f 15%,transparent);color:#3fbf6f}',
|
|
252
|
+
'.rsc-modal-state span{flex:1;min-width:0}',
|
|
253
|
+
'.rsc-modal-state button{flex:none;border:0;border-radius:7px;padding:4px 11px;font:inherit;font-size:11.5px;cursor:pointer;background:currentColor;color:var(--dsw-alias-bg-primary,#1e1e22)}',
|
|
254
|
+
'.rsc-modal-foot{display:flex;align-items:center;gap:10px;margin-top:2px}',
|
|
255
|
+
'.rsc-modal-foot .rsc-check{flex:1;min-width:0}',
|
|
256
|
+
'.rsc-modal-foot .rsc-btn{flex:none;padding:8px 18px}',
|
|
257
|
+
'.rsc-link{font-size:11.5px;color:var(--dsw-alias-label-tertiary);text-decoration:none}',
|
|
258
|
+
'.rsc-link:hover{color:var(--dsw-alias-label-primary)}',
|
|
259
|
+
].join('');
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
apply(ctx) {
|
|
263
|
+
const slots = ctx.get('slots');
|
|
264
|
+
if (slots === undefined) return;
|
|
265
|
+
ctx.effect(function () { return styles.insert(CSS); });
|
|
266
|
+
|
|
267
|
+
let sessions = null;
|
|
268
|
+
try { sessions = ctx.get('sessions'); } catch (e) {}
|
|
269
|
+
|
|
270
|
+
const I18N_NS = 'dsh-plugin-rollout-scout';
|
|
271
|
+
const I18N = {
|
|
272
|
+
en: {
|
|
273
|
+
title: 'Rollout Scout',
|
|
274
|
+
tagline: 'Start throwaway conversations and score their chain-of-thought to find a limited-rollout model.',
|
|
275
|
+
launcher: 'Rollout Scout',
|
|
276
|
+
pillRunning: 'Scouting {active} · {launched} tried',
|
|
277
|
+
pillPaused: 'Paused · {launched} tried',
|
|
278
|
+
pillDone: 'Idle · {launched} tried',
|
|
279
|
+
pillBest: 'Best rollout confidence so far: {score}%',
|
|
280
|
+
seatLive: '{active} live',
|
|
281
|
+
close: 'Close',
|
|
282
|
+
setup: 'Probe setup',
|
|
283
|
+
scoring: 'Scoring',
|
|
284
|
+
prompt: 'Probe prompt',
|
|
285
|
+
promptPlaceholder: 'Ask something that makes it reason at length',
|
|
286
|
+
model: 'Model',
|
|
287
|
+
effort: 'Reasoning effort',
|
|
288
|
+
concurrency: 'Concurrency',
|
|
289
|
+
folder: 'Folder for probe sessions',
|
|
290
|
+
discardBelow: 'Discard below',
|
|
291
|
+
keepAbove: 'Keep above',
|
|
292
|
+
minOpenings: 'Min. openings',
|
|
293
|
+
paragraphWindow: 'Give up after',
|
|
294
|
+
paragraphs: '{count} paragraphs',
|
|
295
|
+
paragraphs_one: '{count} paragraph',
|
|
296
|
+
forceStop: 'Force stop',
|
|
297
|
+
forceStopHint: 'Stop launching and abort every conversation still in flight.',
|
|
298
|
+
discardChinese: 'Discard when the chain-of-thought is mostly Chinese (80%+)',
|
|
299
|
+
chineseCot: 'Chinese CoT',
|
|
300
|
+
scoringHint: '“Let me” opening a paragraph is decisive against. “I’ll” opening the whole chain-of-thought is decisive for. “We need” at the start is only a negative opening — the summariser often restates the task that way, then writes I’ll / I’m in even paragraphs with pauses between bursts.',
|
|
301
|
+
shapeRegular: 'even paragraphs',
|
|
302
|
+
shapeBurst: '{count} pauses',
|
|
303
|
+
shapeBurst_one: '{count} pause',
|
|
304
|
+
reason_shape: 'summariser shape',
|
|
305
|
+
reason_decisive: 'decisive opening',
|
|
306
|
+
reason_score: 'opening score',
|
|
307
|
+
reason_window: 'no positive opening',
|
|
308
|
+
reason_chinese: 'Chinese CoT',
|
|
309
|
+
reason_ended: 'finished without a keep',
|
|
310
|
+
reason_tps: 'high TPS',
|
|
311
|
+
reason_ttft_fast: 'first token too fast',
|
|
312
|
+
discardAboveTps: 'Discard when TPS exceeds',
|
|
313
|
+
tpsUnit: 'chunks/s',
|
|
314
|
+
discardBelowTtft: 'Discard when first token <',
|
|
315
|
+
secUnit: 's',
|
|
316
|
+
timingHint: 'Rollout models generate at ~40–50 chunks/s with distinctive latency. Discarding out-of-range probes saves tokens early.',
|
|
317
|
+
tpsChip: '{tps} chunks/s',
|
|
318
|
+
ttftChip: 'TTFT {ttft}s',
|
|
319
|
+
autoPauseOnMatch: 'Auto-pause on a strong match',
|
|
320
|
+
autoDelete: 'Delete old-model probes from disk',
|
|
321
|
+
start: 'Start',
|
|
322
|
+
pause: 'Pause',
|
|
323
|
+
resume: 'Resume',
|
|
324
|
+
clear: 'Clear finished',
|
|
325
|
+
deleteAll: 'Delete all sessions',
|
|
326
|
+
deleteAllHint: 'Remove every probe conversation from disk — including ones already cleared from this list — and reset numbering to 1.',
|
|
327
|
+
deleteAllBlocked: 'Probes are still live. Force stop first — deleting a session log while its turn is running would corrupt it.',
|
|
328
|
+
effortDefault: 'Provider default',
|
|
329
|
+
statLaunched: 'Launched',
|
|
330
|
+
statActive: 'Live',
|
|
331
|
+
statKept: 'Kept',
|
|
332
|
+
statDiscarded: 'Discarded',
|
|
333
|
+
statBest: 'Best score',
|
|
334
|
+
running: 'Scouting — {active} live, {launched} launched',
|
|
335
|
+
idle: 'Idle',
|
|
336
|
+
noteHit: 'Caught one — launching paused. Press Resume to continue.',
|
|
337
|
+
noteForceStopped: 'Force stopped. Every probe in flight was aborted.',
|
|
338
|
+
notePaused: 'Paused. Live probes will finish on their own; press Resume to keep fishing.',
|
|
339
|
+
noteLaunchFailed: 'Three probes in a row failed to start, so launching stopped. Press Resume to try again. Last error: {error}',
|
|
340
|
+
probe: 'Probe {id}',
|
|
341
|
+
confidence: 'rollout confidence',
|
|
342
|
+
evidenceNone: 'no classified opening yet',
|
|
343
|
+
chars: '{count} chars',
|
|
344
|
+
deleted: 'deleted',
|
|
345
|
+
openSession: 'Click to open this conversation',
|
|
346
|
+
empty: 'No probes yet. Set a prompt and press Start.',
|
|
347
|
+
emptyAllDiscarded: 'All {count} probes so far were discarded. Still fishing.',
|
|
348
|
+
status_stopped: 'stopped',
|
|
349
|
+
verdict_rollout: 'ROLLOUT',
|
|
350
|
+
verdict_old: 'old model',
|
|
351
|
+
verdict_unknown: 'inconclusive',
|
|
352
|
+
status_starting: 'starting',
|
|
353
|
+
status_streaming: 'thinking',
|
|
354
|
+
'status_kept-streaming': 'finishing',
|
|
355
|
+
status_kept: 'kept',
|
|
356
|
+
status_discarding: 'cancelling',
|
|
357
|
+
status_discarded: 'discarded',
|
|
358
|
+
status_finished: 'finished',
|
|
359
|
+
status_error: 'error',
|
|
360
|
+
'status_pending-discard': 'thinking',
|
|
361
|
+
status_pinned: 'watching',
|
|
362
|
+
localeCode: 'en',
|
|
363
|
+
scoringHelp: 'How scoring works',
|
|
364
|
+
selfCheck: 'Self-check {agreed}/{total} · known rollout kept {kept}/{rollout}',
|
|
365
|
+
selfCheckBad: 'Self-check {agreed}/{total} — your thresholds disagree with {n} labelled samples',
|
|
366
|
+
selfCheckHint: 'Runs {total} hand-labelled chains-of-thought through the classifier under the settings above. No tokens, no probes — it is how you can tell "nothing found" apart from "nothing findable".',
|
|
367
|
+
selfCheckWant: 'want',
|
|
368
|
+
protect: 'Keep',
|
|
369
|
+
protectOn: 'Kept',
|
|
370
|
+
protectHint: 'Exempt this conversation from every stop and delete in this console.',
|
|
371
|
+
unprotectHint: 'Kept. Click to hand it back to the ordinary rules.',
|
|
372
|
+
notePausedCulled: 'Paused. {count} probes already judged as the old model were cancelled; the undecided ones run on.',
|
|
373
|
+
noteReaped: 'Swept {count} untracked probe conversations out of the folder.',
|
|
374
|
+
orphans: '{count} probe conversations in this folder are not tracked by this console.',
|
|
375
|
+
orphansLive: '{count} untracked ({live} still running).',
|
|
376
|
+
reap: 'Sweep them',
|
|
377
|
+
reapBusy: 'Stop the run first',
|
|
378
|
+
preflightTitle: 'Before you start',
|
|
379
|
+
preflightBody: 'A run opens one conversation per probe. Probe prompts are sent as plugin messages, so DSH Desktop will not raise a system notification for them — but anything else you have running still will.',
|
|
380
|
+
notifOn: 'Desktop notifications are on.',
|
|
381
|
+
notifOff: 'Desktop notifications are off.',
|
|
382
|
+
notifNone: 'This harness has no desktop notifications.',
|
|
383
|
+
notifMute: 'Turn off',
|
|
384
|
+
dontShowAgain: 'Do not show this again',
|
|
385
|
+
preflightGo: 'Start run',
|
|
386
|
+
cancel: 'Cancel',
|
|
387
|
+
rename: 'Rename',
|
|
388
|
+
renamePrompt: 'Name this conversation',
|
|
389
|
+
},
|
|
390
|
+
zh: {
|
|
391
|
+
title: '灰度侦察',
|
|
392
|
+
tagline: '开启一批临时会话,为它们的思维链打分,用来寻找灰度发布的模型。',
|
|
393
|
+
launcher: '灰度侦察',
|
|
394
|
+
pillRunning: '侦察中 {active} · 已试 {launched}',
|
|
395
|
+
pillPaused: '已暂停 · 已试 {launched}',
|
|
396
|
+
pillDone: '空闲 · 已试 {launched}',
|
|
397
|
+
pillBest: '目前最高灰度置信度:{score}%',
|
|
398
|
+
seatLive: '{active} 个进行中',
|
|
399
|
+
close: '关闭',
|
|
400
|
+
setup: '探测设置',
|
|
401
|
+
scoring: '评分',
|
|
402
|
+
prompt: '探测提示词',
|
|
403
|
+
promptPlaceholder: '写一个能让它充分推理的问题',
|
|
404
|
+
model: '模型',
|
|
405
|
+
effort: '思考强度',
|
|
406
|
+
concurrency: '并发数',
|
|
407
|
+
folder: '探测会话存放目录',
|
|
408
|
+
discardBelow: '低于此分即丢弃',
|
|
409
|
+
keepAbove: '高于此分即保留',
|
|
410
|
+
minOpenings: '最少开头数',
|
|
411
|
+
paragraphWindow: '放弃阈值',
|
|
412
|
+
paragraphs: '{count} 段',
|
|
413
|
+
paragraphs_one: '{count} 段',
|
|
414
|
+
forceStop: '强制停止',
|
|
415
|
+
forceStopHint: '停止发起,并中止所有进行中的会话。',
|
|
416
|
+
discardChinese: '思维链以中文为主(80% 以上)时丢弃',
|
|
417
|
+
chineseCot: '中文思维链',
|
|
418
|
+
scoringHint: '段落以「Let me」开头即判定为旧模型;整条思维链以「I’ll」开头即判定为灰度。开头的「We need」只记负分——总结模型常这样复述任务,随后用规整的 I’ll / I’m 段落、一阵一阵地输出。',
|
|
419
|
+
shapeRegular: '规整段落',
|
|
420
|
+
shapeBurst: '{count} 次停顿',
|
|
421
|
+
shapeBurst_one: '{count} 次停顿',
|
|
422
|
+
reason_shape: '总结链形态',
|
|
423
|
+
reason_decisive: '决定性开头',
|
|
424
|
+
reason_score: '开头评分',
|
|
425
|
+
reason_window: '无正向开头',
|
|
426
|
+
reason_chinese: '中文思维链',
|
|
427
|
+
reason_ended: '结束时未命中',
|
|
428
|
+
reason_tps: 'TPS 过高',
|
|
429
|
+
reason_ttft_fast: '首字过快',
|
|
430
|
+
discardAboveTps: '生成速度 (TPS) 超过上限时丢弃',
|
|
431
|
+
tpsUnit: '字/秒',
|
|
432
|
+
discardBelowTtft: '首字延迟低于下限时丢弃',
|
|
433
|
+
secUnit: '秒',
|
|
434
|
+
timingHint: '灰度模型吐字速度常在 40~50 字/秒且首字延迟有特征,超出范围及早丢弃可大幅节省 Token。',
|
|
435
|
+
tpsChip: '{tps} 字/秒',
|
|
436
|
+
ttftChip: '首字 {ttft}秒',
|
|
437
|
+
autoPauseOnMatch: '命中强匹配时自动暂停',
|
|
438
|
+
autoDelete: '从磁盘删除判为旧模型的会话',
|
|
439
|
+
start: '开始',
|
|
440
|
+
pause: '暂停',
|
|
441
|
+
resume: '继续',
|
|
442
|
+
clear: '清空已结束',
|
|
443
|
+
deleteAll: '删除全部会话',
|
|
444
|
+
deleteAllHint: '从磁盘删除所有探测会话(包括已经从列表清掉的),并把编号从 1 重新计。',
|
|
445
|
+
deleteAllBlocked: '仍有探测在进行中,请先强制停止——删除正在写入的会话文件会损坏它。',
|
|
446
|
+
effortDefault: '服务商默认',
|
|
447
|
+
statLaunched: '已发起',
|
|
448
|
+
statActive: '进行中',
|
|
449
|
+
statKept: '已保留',
|
|
450
|
+
statDiscarded: '已丢弃',
|
|
451
|
+
statBest: '最高分',
|
|
452
|
+
running: '侦察中 — {active} 进行中,已发起 {launched}',
|
|
453
|
+
idle: '空闲',
|
|
454
|
+
noteHit: '已命中——发起已暂停,点击「继续」继续。',
|
|
455
|
+
noteForceStopped: '已强制停止,所有进行中的探测均已中止。',
|
|
456
|
+
notePaused: '已暂停,进行中的探测会自行结束;点击「继续」可继续钓。',
|
|
457
|
+
noteLaunchFailed: '连续三个探测启动失败,已停止发起。点击「继续」重试。最后的错误:{error}',
|
|
458
|
+
probe: '探测 {id}',
|
|
459
|
+
confidence: '灰度置信度',
|
|
460
|
+
evidenceNone: '暂无可分类开头',
|
|
461
|
+
chars: '{count} 字',
|
|
462
|
+
deleted: '已删除',
|
|
463
|
+
openSession: '点击打开该会话',
|
|
464
|
+
empty: '还没有探测。填写提示词后点击「开始」。',
|
|
465
|
+
emptyAllDiscarded: '目前 {count} 个探测全部被丢弃,仍在继续。',
|
|
466
|
+
status_stopped: '已停止',
|
|
467
|
+
verdict_rollout: '灰度',
|
|
468
|
+
verdict_old: '旧模型',
|
|
469
|
+
verdict_unknown: '无法判定',
|
|
470
|
+
status_starting: '启动中',
|
|
471
|
+
status_streaming: '思考中',
|
|
472
|
+
'status_kept-streaming': '收尾中',
|
|
473
|
+
status_kept: '已保留',
|
|
474
|
+
status_discarding: '中止中',
|
|
475
|
+
status_discarded: '已丢弃',
|
|
476
|
+
status_finished: '已结束',
|
|
477
|
+
status_error: '出错',
|
|
478
|
+
'status_pending-discard': '思考中',
|
|
479
|
+
status_pinned: '看着',
|
|
480
|
+
localeCode: 'zh',
|
|
481
|
+
scoringHelp: '评分是怎么算的',
|
|
482
|
+
selfCheck: '自检 {agreed}/{total} · 已知灰度样本保留 {kept}/{rollout}',
|
|
483
|
+
selfCheckBad: '自检 {agreed}/{total} —— 当前阈值与 {n} 条标注样本不一致',
|
|
484
|
+
selfCheckHint: '用上面的设置,把 {total} 条人工标注的思维链跑一遍分类器。不消耗 Token、不发起探测——这是分辨「没找到」和「根本找不到」的办法。',
|
|
485
|
+
selfCheckWant: '应为',
|
|
486
|
+
protect: '保留',
|
|
487
|
+
protectOn: '已保留',
|
|
488
|
+
protectHint: '让这个会话不受本控制台任何停止与删除操作的影响。',
|
|
489
|
+
unprotectHint: '已保留。点击可交回常规规则处理。',
|
|
490
|
+
notePausedCulled: '已暂停。已判定为旧模型的 {count} 个探测已中止;尚未判定的继续跑完。',
|
|
491
|
+
noteReaped: '已清理该目录下 {count} 个未被跟踪的探测会话。',
|
|
492
|
+
orphans: '该目录下有 {count} 个探测会话不在本控制台的跟踪范围内。',
|
|
493
|
+
orphansLive: '{count} 个未跟踪(其中 {live} 个仍在运行)。',
|
|
494
|
+
reap: '清理',
|
|
495
|
+
reapBusy: '请先停止运行',
|
|
496
|
+
preflightTitle: '开始之前',
|
|
497
|
+
preflightBody: '每个探测都会开启一个会话。探测提示词以插件消息发送,因此 DSH Desktop 不会为它们弹出系统通知——但你其它正在跑的会话仍然会。',
|
|
498
|
+
notifOn: '桌面通知已开启。',
|
|
499
|
+
notifOff: '桌面通知已关闭。',
|
|
500
|
+
notifNone: '当前环境没有桌面通知。',
|
|
501
|
+
notifMute: '关闭通知',
|
|
502
|
+
dontShowAgain: '不再提示',
|
|
503
|
+
preflightGo: '开始',
|
|
504
|
+
cancel: '取消',
|
|
505
|
+
rename: '重命名',
|
|
506
|
+
renamePrompt: '为该会话命名',
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
let t = function (key, params) {
|
|
510
|
+
let out = I18N.en[key] !== undefined ? I18N.en[key] : key;
|
|
511
|
+
if (params) for (const k in params) out = out.replace('{' + k + '}', String(params[k]));
|
|
512
|
+
return out;
|
|
513
|
+
};
|
|
514
|
+
try {
|
|
515
|
+
const locale = ctx.get('locale');
|
|
516
|
+
if (locale && typeof locale.register === 'function' && typeof locale.bind === 'function') {
|
|
517
|
+
ctx.effect(function () { return locale.register(I18N_NS, I18N); });
|
|
518
|
+
t = locale.bind(I18N_NS);
|
|
519
|
+
}
|
|
520
|
+
} catch (e) {}
|
|
521
|
+
|
|
522
|
+
/** English pluralises, Chinese does not; both go through the same key. */
|
|
523
|
+
function n(key, count) {
|
|
524
|
+
return t(count === 1 ? key + '_one' : key, { count: count });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function Field(props) {
|
|
528
|
+
return React.createElement('div', null,
|
|
529
|
+
React.createElement('span', { className: 'rsc-label' }, props.label),
|
|
530
|
+
props.children
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function scoreColor(score, config) {
|
|
535
|
+
if (config && score >= config.keepAbove) return '#3fbf6f';
|
|
536
|
+
if (config && score <= config.discardBelow) return 'var(--dsw-alias-status-error,#e5484d)';
|
|
537
|
+
return 'var(--dsw-alias-accent-primary,#4b8dff)';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The score meter: fill plus the two threshold marks, so the number is
|
|
541
|
+
* readable against the rules that will act on it. */
|
|
542
|
+
function ScoreMeter(props) {
|
|
543
|
+
const config = props.config;
|
|
544
|
+
return React.createElement('div', { className: 'rsc-score' },
|
|
545
|
+
React.createElement('div', { className: 'rsc-meter' },
|
|
546
|
+
React.createElement('div', {
|
|
547
|
+
className: 'rsc-meter-fill',
|
|
548
|
+
style: { width: pct(props.score) + '%', background: scoreColor(props.score, config) },
|
|
549
|
+
}),
|
|
550
|
+
config ? React.createElement('div', {
|
|
551
|
+
className: 'rsc-meter-mark', style: { left: pct(config.discardBelow) + '%' },
|
|
552
|
+
}) : null,
|
|
553
|
+
config ? React.createElement('div', {
|
|
554
|
+
className: 'rsc-meter-mark', style: { left: pct(config.keepAbove) + '%' },
|
|
555
|
+
}) : null
|
|
556
|
+
),
|
|
557
|
+
React.createElement('span', {
|
|
558
|
+
className: 'rsc-score-v',
|
|
559
|
+
style: { color: scoreColor(props.score, config) },
|
|
560
|
+
}, pct(props.score) + '%')
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function AttemptCard(props) {
|
|
565
|
+
const a = props.attempt;
|
|
566
|
+
// Electron's renderer does not implement window.prompt, so naming a
|
|
567
|
+
// catch happens in place on the card.
|
|
568
|
+
const [naming, setNaming] = React.useState(null);
|
|
569
|
+
const tone = STATUS_TONE[a.status] || 'neutral';
|
|
570
|
+
const clickable = !!a.sessionId && !a.deleted && sessions;
|
|
571
|
+
const hits = a.hits || {};
|
|
572
|
+
const hitKeys = Object.keys(hits);
|
|
573
|
+
const leaving = a.status === 'pending-discard' && !a.pinned && !a.protected;
|
|
574
|
+
// Rescue only applies while the probe is still cancellable. Wiring the
|
|
575
|
+
// handlers on finished rows sent a write per mouse-over as the pointer
|
|
576
|
+
// crossed the list, and pinned a conversation just for being opened —
|
|
577
|
+
// which then exempted it from "clear finished".
|
|
578
|
+
const rescuable = !!RESCUABLE[a.status] && !a.pinned;
|
|
579
|
+
return React.createElement('div', {
|
|
580
|
+
className: 'rsc-item',
|
|
581
|
+
'data-id': a.id,
|
|
582
|
+
'data-leaving': leaving ? '' : undefined,
|
|
583
|
+
'data-locked': a.protected ? '' : undefined,
|
|
584
|
+
'data-tone': tone,
|
|
585
|
+
'data-click': clickable || undefined,
|
|
586
|
+
title: clickable ? t('openSession') : undefined,
|
|
587
|
+
onMouseEnter: rescuable && props.onHold ? function () { props.onHold(a.id); } : undefined,
|
|
588
|
+
// Also released when the card is no longer rescuable but the host
|
|
589
|
+
// still has it held: a probe that finishes under the pointer would
|
|
590
|
+
// otherwise never see the mouse leave.
|
|
591
|
+
onMouseLeave: (rescuable || a.held) && props.onRelease
|
|
592
|
+
? function () { props.onRelease(a.id); }
|
|
593
|
+
: undefined,
|
|
594
|
+
onClick: clickable ? function () {
|
|
595
|
+
if (rescuable && props.onPin) props.onPin(a.id);
|
|
596
|
+
sessions.open(a.sessionId);
|
|
597
|
+
openStore.set(false);
|
|
598
|
+
} : undefined,
|
|
599
|
+
},
|
|
600
|
+
React.createElement('div', { className: 'rsc-item-head' },
|
|
601
|
+
React.createElement('span', { className: 'rsc-item-dot', 'data-tone': tone }),
|
|
602
|
+
React.createElement('span', { className: 'rsc-item-name' }, a.title || t('probe', { id: a.id })),
|
|
603
|
+
React.createElement('span', { className: 'rsc-item-status' },
|
|
604
|
+
t('status_' + a.status) + ' · ' + t('chars', { count: a.chars })
|
|
605
|
+
+ (a.deleted ? ' · ' + t('deleted') : '')),
|
|
606
|
+
a.verdict ? React.createElement('span', {
|
|
607
|
+
className: 'rsc-badge',
|
|
608
|
+
'data-tone': a.verdict === 'rollout' ? 'good' : (a.verdict === 'old' ? 'bad' : undefined),
|
|
609
|
+
}, t('verdict_' + a.verdict)) : null,
|
|
610
|
+
a.protected && props.onRename && naming === null ? React.createElement('button', {
|
|
611
|
+
type: 'button',
|
|
612
|
+
className: 'rsc-lock',
|
|
613
|
+
title: t('renamePrompt'),
|
|
614
|
+
onClick: function (event) {
|
|
615
|
+
event.stopPropagation();
|
|
616
|
+
setNaming(a.title || '');
|
|
617
|
+
},
|
|
618
|
+
}, t('rename')) : null,
|
|
619
|
+
React.createElement('button', {
|
|
620
|
+
type: 'button',
|
|
621
|
+
className: 'rsc-lock',
|
|
622
|
+
'data-on': a.protected ? '' : undefined,
|
|
623
|
+
title: a.protected ? t('unprotectHint') : t('protectHint'),
|
|
624
|
+
onClick: function (event) {
|
|
625
|
+
event.stopPropagation();
|
|
626
|
+
if (props.onProtect) props.onProtect(a.id, !a.protected);
|
|
627
|
+
},
|
|
628
|
+
}, a.protected ? t('protectOn') : t('protect'))
|
|
629
|
+
),
|
|
630
|
+
naming !== null ? React.createElement('input', {
|
|
631
|
+
className: 'rsc-input rsc-rename',
|
|
632
|
+
value: naming,
|
|
633
|
+
autoFocus: true,
|
|
634
|
+
placeholder: t('renamePrompt'),
|
|
635
|
+
onClick: function (event) { event.stopPropagation(); },
|
|
636
|
+
onChange: function (event) { setNaming(event.target.value); },
|
|
637
|
+
onKeyDown: function (event) {
|
|
638
|
+
if (event.key === 'Escape') { setNaming(null); return; }
|
|
639
|
+
if (event.key !== 'Enter') return;
|
|
640
|
+
const text = String(naming).trim();
|
|
641
|
+
setNaming(null);
|
|
642
|
+
if (text !== '') props.onRename(a.id, text);
|
|
643
|
+
},
|
|
644
|
+
onBlur: function () { setNaming(null); },
|
|
645
|
+
}) : null,
|
|
646
|
+
React.createElement(ScoreMeter, { score: a.score, config: props.config }),
|
|
647
|
+
React.createElement('div', { className: 'rsc-evidence' },
|
|
648
|
+
React.createElement('span', null, t('confidence')),
|
|
649
|
+
React.createElement('span', { className: 'rsc-chip' }, n('paragraphs', a.paragraphs || 0)),
|
|
650
|
+
a.ttft !== null && a.ttft !== undefined ? React.createElement('span', {
|
|
651
|
+
className: 'rsc-chip',
|
|
652
|
+
'data-sign': a.reason === 'ttft_fast' ? 'neg' : undefined,
|
|
653
|
+
}, t('ttftChip', { ttft: a.ttft })) : null,
|
|
654
|
+
a.tps !== null && a.tps !== undefined ? React.createElement('span', {
|
|
655
|
+
className: 'rsc-chip',
|
|
656
|
+
'data-sign': a.reason === 'tps' ? 'neg' : undefined,
|
|
657
|
+
}, t('tpsChip', { tps: a.tps })) : null,
|
|
658
|
+
a.reason ? React.createElement('span', {
|
|
659
|
+
className: 'rsc-chip',
|
|
660
|
+
'data-sign': a.reason === 'tps' || a.reason === 'ttft_fast' ? 'neg' : undefined,
|
|
661
|
+
}, t('reason_' + a.reason)) : null,
|
|
662
|
+
a.chinese ? React.createElement('span', { className: 'rsc-chip', 'data-sign': 'neg' }, t('chineseCot')) : null,
|
|
663
|
+
a.regular ? React.createElement('span', { className: 'rsc-chip', 'data-sign': 'pos' }, t('shapeRegular')) : null,
|
|
664
|
+
a.pauses ? React.createElement('span', { className: 'rsc-chip', 'data-sign': 'pos' }, n('shapeBurst', a.pauses)) : null,
|
|
665
|
+
hitKeys.length === 0 && !a.chinese && a.tps === null && a.ttft === null
|
|
666
|
+
? React.createElement('span', { className: 'rsc-chip' }, t('evidenceNone'))
|
|
667
|
+
: hitKeys.map(function (k) {
|
|
668
|
+
const hit = hits[k];
|
|
669
|
+
const count = hit && typeof hit === 'object' ? hit.count : hit;
|
|
670
|
+
const tagged = hit && typeof hit === 'object' ? hit.sign : null;
|
|
671
|
+
const negPhrase = /^(let me|let us|let's|we)\b/i.test(String(k));
|
|
672
|
+
const sign = tagged === 'neg' || negPhrase ? 'neg' : 'pos';
|
|
673
|
+
return React.createElement('span', {
|
|
674
|
+
key: k, className: 'rsc-chip', 'data-sign': sign,
|
|
675
|
+
}, k + ' ×' + count);
|
|
676
|
+
})
|
|
677
|
+
),
|
|
678
|
+
a.error ? React.createElement('div', { className: 'rsc-error' }, a.error) : null,
|
|
679
|
+
a.preview ? React.createElement('div', { className: 'rsc-prev' }, a.preview) : null
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Newest first, launch order. Never resorted, so a card stays put. */
|
|
684
|
+
function ProbeQueue(props) {
|
|
685
|
+
return React.createElement('div', { className: 'rsc-list' },
|
|
686
|
+
props.attempts.map(function (a) {
|
|
687
|
+
return React.createElement(AttemptCard, {
|
|
688
|
+
key: a.id, attempt: a, config: props.config,
|
|
689
|
+
onHold: props.onHold, onRelease: props.onRelease, onPin: props.onPin,
|
|
690
|
+
onProtect: props.onProtect, onRename: props.onRename,
|
|
691
|
+
});
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Shown before a run unless the user has dismissed it for good. The
|
|
698
|
+
* notification line reads the harness's own setting rather than guessing,
|
|
699
|
+
* so on a web build it says there is nothing to worry about instead of
|
|
700
|
+
* warning about a feature that does not exist.
|
|
701
|
+
*/
|
|
702
|
+
function Preflight(props) {
|
|
703
|
+
const [quiet, setQuiet] = React.useState(false);
|
|
704
|
+
const n = props.notifications || {};
|
|
705
|
+
const tone = !n.registered ? null : (n.enabled ? 'warn' : 'ok');
|
|
706
|
+
const line = !n.registered ? t('notifNone') : (n.enabled ? t('notifOn') : t('notifOff'));
|
|
707
|
+
return React.createElement('div', {
|
|
708
|
+
className: 'rsc-scrim',
|
|
709
|
+
onClick: function (e) { if (e.target === e.currentTarget) props.onCancel(); },
|
|
710
|
+
},
|
|
711
|
+
React.createElement('div', { className: 'rsc-modal', role: 'dialog', 'aria-modal': 'true' },
|
|
712
|
+
React.createElement('div', { className: 'rsc-modal-h' }, t('preflightTitle')),
|
|
713
|
+
React.createElement('div', { className: 'rsc-modal-p' }, t('preflightBody')),
|
|
714
|
+
React.createElement('div', { className: 'rsc-modal-state', 'data-tone': tone || undefined },
|
|
715
|
+
React.createElement('span', null, line),
|
|
716
|
+
n.registered && n.enabled
|
|
717
|
+
? React.createElement('button', {
|
|
718
|
+
type: 'button', onClick: props.onMute,
|
|
719
|
+
}, t('notifMute'))
|
|
720
|
+
: null
|
|
721
|
+
),
|
|
722
|
+
React.createElement('div', { className: 'rsc-modal-foot' },
|
|
723
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
724
|
+
React.createElement('input', {
|
|
725
|
+
type: 'checkbox', checked: quiet,
|
|
726
|
+
onChange: function (e) { setQuiet(e.target.checked); },
|
|
727
|
+
}),
|
|
728
|
+
React.createElement('span', null, t('dontShowAgain'))
|
|
729
|
+
),
|
|
730
|
+
React.createElement('button', {
|
|
731
|
+
type: 'button', className: 'rsc-btn', onClick: props.onCancel,
|
|
732
|
+
}, t('cancel')),
|
|
733
|
+
React.createElement('button', {
|
|
734
|
+
type: 'button', className: 'rsc-btn', 'data-primary': '',
|
|
735
|
+
onClick: function () { props.onConfirm(quiet); },
|
|
736
|
+
}, t('preflightGo'))
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Runs the labelled corpus through the host's real classifier under the
|
|
744
|
+
* settings currently in the form, so editing a threshold immediately shows
|
|
745
|
+
* whether it would still catch a known rollout sample.
|
|
746
|
+
*/
|
|
747
|
+
function SelfCheck(props) {
|
|
748
|
+
const [report, setReport] = React.useState(null);
|
|
749
|
+
const [open, setOpen] = React.useState(false);
|
|
750
|
+
const config = props.config;
|
|
751
|
+
React.useEffect(function () {
|
|
752
|
+
let alive = true;
|
|
753
|
+
const timer = setTimeout(function () {
|
|
754
|
+
api('POST', { action: 'self-check', config: config })
|
|
755
|
+
.then(function (v) { if (alive) setReport(v); })
|
|
756
|
+
.catch(function () { if (alive) setReport(null); });
|
|
757
|
+
}, 250);
|
|
758
|
+
return function () { alive = false; clearTimeout(timer); };
|
|
759
|
+
}, [JSON.stringify(config)]);
|
|
760
|
+
if (report === null) return null;
|
|
761
|
+
const bad = report.agreed < report.total;
|
|
762
|
+
return React.createElement('div', null,
|
|
763
|
+
React.createElement('div', {
|
|
764
|
+
className: 'rsc-selfcheck',
|
|
765
|
+
'data-bad': bad ? '' : undefined,
|
|
766
|
+
title: t('selfCheckHint', { total: report.total }),
|
|
767
|
+
onClick: function () { setOpen(!open); },
|
|
768
|
+
}, bad
|
|
769
|
+
? t('selfCheckBad', { agreed: report.agreed, total: report.total, n: report.total - report.agreed })
|
|
770
|
+
: t('selfCheck', {
|
|
771
|
+
agreed: report.agreed, total: report.total,
|
|
772
|
+
kept: report.rolloutKept, rollout: report.rolloutTotal,
|
|
773
|
+
})),
|
|
774
|
+
open ? React.createElement('div', { className: 'rsc-selfcheck-rows' },
|
|
775
|
+
report.results.map(function (x) {
|
|
776
|
+
return React.createElement('div', {
|
|
777
|
+
key: x.id, className: 'rsc-selfcheck-row', 'data-miss': x.agrees ? undefined : '',
|
|
778
|
+
},
|
|
779
|
+
React.createElement('span', null, x.title),
|
|
780
|
+
React.createElement('b', null, x.score + '%'),
|
|
781
|
+
React.createElement('span', null, t('verdict_' + x.verdict)),
|
|
782
|
+
x.agrees ? null : React.createElement('span', null,
|
|
783
|
+
t('selfCheckWant') + ' ' + t('verdict_' + x.label))
|
|
784
|
+
);
|
|
785
|
+
})
|
|
786
|
+
) : null
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function Stat(props) {
|
|
791
|
+
return React.createElement('div', { className: 'rsc-stat' },
|
|
792
|
+
React.createElement('div', { className: 'rsc-stat-v' }, props.value),
|
|
793
|
+
React.createElement('div', { className: 'rsc-stat-k' }, props.label)
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function ScoutView(props) {
|
|
798
|
+
const [remote, setRemote] = React.useState(null);
|
|
799
|
+
const [error, setError] = React.useState(null);
|
|
800
|
+
const [form, setForm] = React.useState(function () { return loadForm(); });
|
|
801
|
+
const [preflight, setPreflight] = React.useState(false);
|
|
802
|
+
|
|
803
|
+
// Every action returns the whole state, and so does the poll. Without
|
|
804
|
+
// ordering, a poll issued before a hover write could land after it and
|
|
805
|
+
// paint the pre-write state back over the card. Tickets are handed out
|
|
806
|
+
// in request order and a reply older than the newest applied is dropped.
|
|
807
|
+
const seq = React.useRef({ issued: 0, applied: 0 });
|
|
808
|
+
function ticket() {
|
|
809
|
+
seq.current.issued += 1;
|
|
810
|
+
return seq.current.issued;
|
|
811
|
+
}
|
|
812
|
+
function applyState(at, value) {
|
|
813
|
+
if (at < seq.current.applied) return;
|
|
814
|
+
seq.current.applied = at;
|
|
815
|
+
setRemote(value);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
React.useEffect(function () {
|
|
819
|
+
let alive = true;
|
|
820
|
+
const tick = function () {
|
|
821
|
+
const at = ticket();
|
|
822
|
+
api('GET').then(function (value) {
|
|
823
|
+
if (alive) { applyState(at, value); setError(null); }
|
|
824
|
+
}).catch(function (e) {
|
|
825
|
+
if (alive) setError(String(e.message || e));
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
tick();
|
|
829
|
+
const timer = setInterval(tick, POLL_MS);
|
|
830
|
+
return function () { alive = false; clearInterval(timer); };
|
|
831
|
+
}, []);
|
|
832
|
+
|
|
833
|
+
const config = remote ? remote.config : null;
|
|
834
|
+
function val(key) {
|
|
835
|
+
if (form[key] !== undefined) return form[key];
|
|
836
|
+
if (config) return config[key];
|
|
837
|
+
return '';
|
|
838
|
+
}
|
|
839
|
+
function patch(key, value) {
|
|
840
|
+
const next = Object.assign({}, form);
|
|
841
|
+
next[key] = value;
|
|
842
|
+
setForm(next);
|
|
843
|
+
saveForm(next);
|
|
844
|
+
}
|
|
845
|
+
function num(key, value, float) {
|
|
846
|
+
const n = float ? parseFloat(value) : parseInt(value, 10);
|
|
847
|
+
patch(key, Number.isFinite(n) ? n : undefined);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const running = !!(remote && remote.running);
|
|
851
|
+
const paused = !!(remote && remote.paused);
|
|
852
|
+
const attempts = remote ? remote.attempts : [];
|
|
853
|
+
|
|
854
|
+
async function call(action, extra) {
|
|
855
|
+
setError(null);
|
|
856
|
+
const at = ticket();
|
|
857
|
+
try {
|
|
858
|
+
applyState(at, await api('POST', Object.assign({ action: action }, extra)));
|
|
859
|
+
} catch (e) { setError(String(e.message || e)); }
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const note = remote && remote.note === 'hit' ? t('noteHit')
|
|
863
|
+
: remote && remote.note === 'force-stopped' ? t('noteForceStopped')
|
|
864
|
+
: remote && remote.note === 'paused' ? t('notePaused')
|
|
865
|
+
: remote && remote.note === 'paused-culled'
|
|
866
|
+
? t('notePausedCulled', { count: remote.culled || 0 })
|
|
867
|
+
: remote && remote.note === 'reaped'
|
|
868
|
+
? t('noteReaped', { count: remote.reaped || 0 })
|
|
869
|
+
: remote && remote.note === 'launch-failed'
|
|
870
|
+
? t('noteLaunchFailed', { error: remote.lastError || '' })
|
|
871
|
+
: null;
|
|
872
|
+
|
|
873
|
+
// Start opens the pre-flight unless it has been dismissed for good.
|
|
874
|
+
function beginRun() {
|
|
875
|
+
if (loadForm()[QUIET_KEY]) startRun();
|
|
876
|
+
else setPreflight(true);
|
|
877
|
+
}
|
|
878
|
+
function startRun() {
|
|
879
|
+
setPreflight(false);
|
|
880
|
+
const locale = t('localeCode') === 'zh' ? 'zh' : 'en';
|
|
881
|
+
call('start', { config: Object.assign({}, config, form, { locale: locale }) });
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const orphans = (remote && remote.orphans) || { live: 0, cold: 0 };
|
|
885
|
+
const orphanTotal = orphans.live + orphans.cold;
|
|
886
|
+
|
|
887
|
+
const kept = attempts.filter(function (a) { return a.verdict === 'rollout'; }).length;
|
|
888
|
+
const discarded = attempts.filter(function (a) { return a.status === 'discarded'; }).length;
|
|
889
|
+
const best = attempts.reduce(function (m, a) {
|
|
890
|
+
return typeof a.score === 'number' && a.score > m ? a.score : m;
|
|
891
|
+
}, 0);
|
|
892
|
+
const queue = attempts
|
|
893
|
+
.filter(function (a) {
|
|
894
|
+
if (a.pinned || a.status === 'pending-discard') return true;
|
|
895
|
+
return a.status !== 'discarded';
|
|
896
|
+
});
|
|
897
|
+
const liveCount = remote ? remote.active : 0;
|
|
898
|
+
// A kept probe stays live on purpose, so it never blocks a delete.
|
|
899
|
+
const blocking = remote ? remote.blocking : 0;
|
|
900
|
+
|
|
901
|
+
return React.createElement('div', { className: 'rsc-full', onClick: props.onSurfaceClick },
|
|
902
|
+
React.createElement('div', { className: 'rsc-top' },
|
|
903
|
+
React.createElement('span', { className: 'rsc-h1' }, t('title')),
|
|
904
|
+
React.createElement('span', { className: 'rsc-sub' },
|
|
905
|
+
running ? t('running', { active: remote.active, launched: remote.launched }) : t('idle')),
|
|
906
|
+
React.createElement('a', {
|
|
907
|
+
className: 'rsc-link',
|
|
908
|
+
href: 'https://github.com/SpookySandwich/dsh-plugin-rollout-scout',
|
|
909
|
+
target: '_blank', rel: 'noreferrer',
|
|
910
|
+
}, 'GitHub ↗'),
|
|
911
|
+
React.createElement('button', {
|
|
912
|
+
type: 'button', className: 'rsc-x', title: t('close'), onClick: props.onClose,
|
|
913
|
+
}, '✕')
|
|
914
|
+
),
|
|
915
|
+
React.createElement('div', { className: 'rsc-cols' },
|
|
916
|
+
React.createElement('div', { className: 'rsc-leftwrap' },
|
|
917
|
+
React.createElement('div', { className: 'rsc-left' },
|
|
918
|
+
React.createElement('div', { className: 'rsc-hint' }, t('tagline')),
|
|
919
|
+
React.createElement('div', { className: 'rsc-sectionhead' }, t('setup')),
|
|
920
|
+
React.createElement(Field, { label: t('prompt') },
|
|
921
|
+
React.createElement('textarea', {
|
|
922
|
+
className: 'rsc-area', value: val('prompt'), disabled: running,
|
|
923
|
+
placeholder: t('promptPlaceholder'),
|
|
924
|
+
onChange: function (e) { patch('prompt', e.target.value); },
|
|
925
|
+
})
|
|
926
|
+
),
|
|
927
|
+
React.createElement('div', { className: 'rsc-row' },
|
|
928
|
+
React.createElement(Field, { label: t('model') },
|
|
929
|
+
React.createElement('select', {
|
|
930
|
+
className: 'rsc-select', value: val('model'), disabled: running,
|
|
931
|
+
onChange: function (e) { patch('model', e.target.value); },
|
|
932
|
+
},
|
|
933
|
+
React.createElement('option', { value: 'deepseek-v4-pro' }, 'DeepSeek-V4-Pro'),
|
|
934
|
+
React.createElement('option', { value: 'deepseek-v4-flash' }, 'DeepSeek-V4-Flash')
|
|
935
|
+
)
|
|
936
|
+
),
|
|
937
|
+
React.createElement(Field, { label: t('effort') },
|
|
938
|
+
React.createElement('select', {
|
|
939
|
+
className: 'rsc-select', value: val('reasoningEffort'), disabled: running,
|
|
940
|
+
onChange: function (e) { patch('reasoningEffort', e.target.value); },
|
|
941
|
+
},
|
|
942
|
+
React.createElement('option', { value: 'high' }, 'High'),
|
|
943
|
+
React.createElement('option', { value: 'max' }, 'Max'),
|
|
944
|
+
React.createElement('option', { value: 'off' }, 'Off'),
|
|
945
|
+
React.createElement('option', { value: 'default' }, t('effortDefault'))
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
),
|
|
949
|
+
React.createElement(Field, { label: t('concurrency') },
|
|
950
|
+
React.createElement('input', {
|
|
951
|
+
className: 'rsc-input', type: 'number', min: 1, max: 6,
|
|
952
|
+
value: val('concurrency'), disabled: running,
|
|
953
|
+
onChange: function (e) { num('concurrency', e.target.value); },
|
|
954
|
+
})
|
|
955
|
+
),
|
|
956
|
+
React.createElement(Field, { label: t('folder') },
|
|
957
|
+
React.createElement('input', {
|
|
958
|
+
className: 'rsc-input', value: val('folder'), disabled: running,
|
|
959
|
+
onChange: function (e) { patch('folder', e.target.value); },
|
|
960
|
+
})
|
|
961
|
+
),
|
|
962
|
+
React.createElement('div', { className: 'rsc-sectionhead' }, t('scoring')),
|
|
963
|
+
React.createElement('div', { className: 'rsc-grid2' },
|
|
964
|
+
React.createElement(Field, { label: t('discardBelow') },
|
|
965
|
+
React.createElement('input', {
|
|
966
|
+
className: 'rsc-input', type: 'number', step: 0.05, min: 0.05, max: 0.9,
|
|
967
|
+
value: val('discardBelow'), disabled: running,
|
|
968
|
+
onChange: function (e) { num('discardBelow', e.target.value, true); },
|
|
969
|
+
})
|
|
970
|
+
),
|
|
971
|
+
React.createElement(Field, { label: t('keepAbove') },
|
|
972
|
+
React.createElement('input', {
|
|
973
|
+
className: 'rsc-input', type: 'number', step: 0.05, min: 0.5, max: 0.99,
|
|
974
|
+
value: val('keepAbove'), disabled: running,
|
|
975
|
+
onChange: function (e) { num('keepAbove', e.target.value, true); },
|
|
976
|
+
})
|
|
977
|
+
),
|
|
978
|
+
React.createElement(Field, { label: t('minOpenings') },
|
|
979
|
+
React.createElement('input', {
|
|
980
|
+
className: 'rsc-input', type: 'number', min: 1, max: 40,
|
|
981
|
+
value: val('minOpenings'), disabled: running,
|
|
982
|
+
onChange: function (e) { num('minOpenings', e.target.value); },
|
|
983
|
+
})
|
|
984
|
+
),
|
|
985
|
+
React.createElement(Field, { label: t('paragraphWindow') },
|
|
986
|
+
React.createElement('input', {
|
|
987
|
+
className: 'rsc-input', type: 'number', min: 2, max: 200,
|
|
988
|
+
value: val('paragraphWindow'), disabled: running,
|
|
989
|
+
onChange: function (e) { num('paragraphWindow', e.target.value); },
|
|
990
|
+
})
|
|
991
|
+
)
|
|
992
|
+
),
|
|
993
|
+
React.createElement(SelfCheck, { config: Object.assign({}, config, form) }),
|
|
994
|
+
React.createElement('details', { className: 'rsc-fold' },
|
|
995
|
+
React.createElement('summary', null, t('scoringHelp')),
|
|
996
|
+
React.createElement('div', { className: 'rsc-fold-body' },
|
|
997
|
+
React.createElement('div', { className: 'rsc-hint' }, t('scoringHint'))
|
|
998
|
+
)
|
|
999
|
+
),
|
|
1000
|
+
React.createElement('div', { className: 'rsc-check-input' },
|
|
1001
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
1002
|
+
React.createElement('input', {
|
|
1003
|
+
type: 'checkbox', checked: !!val('discardAboveTps'), disabled: running,
|
|
1004
|
+
onChange: function (e) { patch('discardAboveTps', e.target.checked); },
|
|
1005
|
+
}),
|
|
1006
|
+
React.createElement('span', null, t('discardAboveTps'))
|
|
1007
|
+
),
|
|
1008
|
+
React.createElement('input', {
|
|
1009
|
+
className: 'rsc-input', type: 'number', min: 5, max: 300, step: 5,
|
|
1010
|
+
value: val('maxTps'), disabled: running || !val('discardAboveTps'),
|
|
1011
|
+
onChange: function (e) { num('maxTps', e.target.value); },
|
|
1012
|
+
}),
|
|
1013
|
+
React.createElement('span', { className: 'rsc-unit' }, t('tpsUnit'))
|
|
1014
|
+
),
|
|
1015
|
+
React.createElement('div', { className: 'rsc-check-input' },
|
|
1016
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
1017
|
+
React.createElement('input', {
|
|
1018
|
+
type: 'checkbox', checked: !!val('discardBelowTtft'), disabled: running,
|
|
1019
|
+
onChange: function (e) { patch('discardBelowTtft', e.target.checked); },
|
|
1020
|
+
}),
|
|
1021
|
+
React.createElement('span', null, t('discardBelowTtft'))
|
|
1022
|
+
),
|
|
1023
|
+
React.createElement('input', {
|
|
1024
|
+
className: 'rsc-input', type: 'number', min: 0.1, max: 60, step: 0.1,
|
|
1025
|
+
value: val('minTtft'), disabled: running || !val('discardBelowTtft'),
|
|
1026
|
+
onChange: function (e) { num('minTtft', e.target.value, true); },
|
|
1027
|
+
}),
|
|
1028
|
+
React.createElement('span', { className: 'rsc-unit' }, t('secUnit'))
|
|
1029
|
+
),
|
|
1030
|
+
React.createElement('div', { className: 'rsc-hint' }, t('timingHint')),
|
|
1031
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
1032
|
+
React.createElement('input', {
|
|
1033
|
+
type: 'checkbox', checked: !!val('autoPauseOnMatch'), disabled: running,
|
|
1034
|
+
onChange: function (e) { patch('autoPauseOnMatch', e.target.checked); },
|
|
1035
|
+
}),
|
|
1036
|
+
React.createElement('span', null, t('autoPauseOnMatch'))
|
|
1037
|
+
),
|
|
1038
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
1039
|
+
React.createElement('input', {
|
|
1040
|
+
type: 'checkbox', checked: !!val('discardChinese'), disabled: running,
|
|
1041
|
+
onChange: function (e) { patch('discardChinese', e.target.checked); },
|
|
1042
|
+
}),
|
|
1043
|
+
React.createElement('span', null, t('discardChinese'))
|
|
1044
|
+
),
|
|
1045
|
+
React.createElement('label', { className: 'rsc-check' },
|
|
1046
|
+
React.createElement('input', {
|
|
1047
|
+
type: 'checkbox', checked: !!val('autoDelete'), disabled: running,
|
|
1048
|
+
onChange: function (e) { patch('autoDelete', e.target.checked); },
|
|
1049
|
+
}),
|
|
1050
|
+
React.createElement('span', null, t('autoDelete'))
|
|
1051
|
+
),
|
|
1052
|
+
note ? React.createElement('div', {
|
|
1053
|
+
className: remote.note === 'launch-failed' ? 'rsc-error' : 'rsc-note',
|
|
1054
|
+
}, note) : null,
|
|
1055
|
+
error ? React.createElement('div', { className: 'rsc-error' }, error) : null
|
|
1056
|
+
),
|
|
1057
|
+
React.createElement('div', { className: 'rsc-foot' },
|
|
1058
|
+
running
|
|
1059
|
+
? React.createElement('button', {
|
|
1060
|
+
type: 'button', className: 'rsc-btn', 'data-wide': '', 'data-quiet': '',
|
|
1061
|
+
onClick: function () { call('pause'); },
|
|
1062
|
+
}, t('pause'))
|
|
1063
|
+
: React.createElement('button', {
|
|
1064
|
+
type: 'button', className: 'rsc-btn', 'data-wide': '', 'data-primary': '',
|
|
1065
|
+
disabled: !remote || String(val('prompt') || '').trim() === '',
|
|
1066
|
+
onClick: function () {
|
|
1067
|
+
// Resume keeps the run and its config; Start begins a new one.
|
|
1068
|
+
if (paused) call('resume');
|
|
1069
|
+
else beginRun();
|
|
1070
|
+
},
|
|
1071
|
+
}, paused ? t('resume') : t('start')),
|
|
1072
|
+
React.createElement('div', { className: 'rsc-actions' },
|
|
1073
|
+
React.createElement('button', {
|
|
1074
|
+
type: 'button', className: 'rsc-btn', 'data-danger': '',
|
|
1075
|
+
title: t('forceStopHint'), disabled: blocking === 0,
|
|
1076
|
+
onClick: function () { call('force-stop'); },
|
|
1077
|
+
}, t('forceStop')),
|
|
1078
|
+
React.createElement('button', {
|
|
1079
|
+
type: 'button', className: 'rsc-btn', disabled: running,
|
|
1080
|
+
onClick: function () { call('clear'); },
|
|
1081
|
+
}, t('clear'))
|
|
1082
|
+
),
|
|
1083
|
+
React.createElement('button', {
|
|
1084
|
+
type: 'button', className: 'rsc-textbtn',
|
|
1085
|
+
// Live probes are still writing their session logs, so the host
|
|
1086
|
+
// refuses this. Show it as disabled rather than as an error
|
|
1087
|
+
// after the click.
|
|
1088
|
+
disabled: running || blocking > 0,
|
|
1089
|
+
title: blocking > 0 ? t('deleteAllBlocked') : t('deleteAllHint'),
|
|
1090
|
+
onClick: function () { call('delete-all'); },
|
|
1091
|
+
}, t('deleteAll'))
|
|
1092
|
+
)
|
|
1093
|
+
),
|
|
1094
|
+
React.createElement('div', { className: 'rsc-right' },
|
|
1095
|
+
React.createElement('div', { className: 'rsc-stats' },
|
|
1096
|
+
React.createElement(Stat, { value: remote ? remote.launched : 0, label: t('statLaunched') }),
|
|
1097
|
+
React.createElement(Stat, { value: remote ? remote.active : 0, label: t('statActive') }),
|
|
1098
|
+
React.createElement(Stat, { value: kept, label: t('statKept') }),
|
|
1099
|
+
React.createElement(Stat, { value: discarded, label: t('statDiscarded') }),
|
|
1100
|
+
React.createElement(Stat, { value: pct(best) + '%', label: t('statBest') })
|
|
1101
|
+
),
|
|
1102
|
+
orphanTotal > 0
|
|
1103
|
+
? React.createElement('div', { className: 'rsc-banner' },
|
|
1104
|
+
React.createElement('span', null, orphans.live > 0
|
|
1105
|
+
? t('orphansLive', { count: orphanTotal, live: orphans.live })
|
|
1106
|
+
: t('orphans', { count: orphanTotal })),
|
|
1107
|
+
React.createElement('button', {
|
|
1108
|
+
type: 'button', disabled: running,
|
|
1109
|
+
title: running ? t('reapBusy') : undefined,
|
|
1110
|
+
onClick: function () { call('reap'); },
|
|
1111
|
+
}, t('reap'))
|
|
1112
|
+
)
|
|
1113
|
+
: null,
|
|
1114
|
+
queue.length === 0
|
|
1115
|
+
? React.createElement('div', { className: 'rsc-empty' },
|
|
1116
|
+
discarded > 0 ? t('emptyAllDiscarded', { count: discarded }) : t('empty'))
|
|
1117
|
+
: React.createElement(ProbeQueue, {
|
|
1118
|
+
attempts: queue, config: config,
|
|
1119
|
+
onHold: function (id) { call('hold', { id: id }); },
|
|
1120
|
+
onRelease: function (id) { call('release', { id: id }); },
|
|
1121
|
+
onPin: function (id) { call('pin', { id: id }); },
|
|
1122
|
+
onProtect: function (id, on) { call(on ? 'protect' : 'unprotect', { id: id }); },
|
|
1123
|
+
onRename: function (id, title) { call('rename', { id: id, title: title }); },
|
|
1124
|
+
})
|
|
1125
|
+
)
|
|
1126
|
+
),
|
|
1127
|
+
preflight ? React.createElement(Preflight, {
|
|
1128
|
+
notifications: remote ? remote.notifications : null,
|
|
1129
|
+
onMute: function () { call('mute-notifications'); },
|
|
1130
|
+
onCancel: function () { setPreflight(false); },
|
|
1131
|
+
onConfirm: function (quiet) {
|
|
1132
|
+
if (quiet) patch(QUIET_KEY, true);
|
|
1133
|
+
startRun();
|
|
1134
|
+
},
|
|
1135
|
+
}) : null
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Open state is shared between the launcher and the full view.
|
|
1140
|
+
const openStore = {
|
|
1141
|
+
value: false,
|
|
1142
|
+
listeners: [],
|
|
1143
|
+
set(next) {
|
|
1144
|
+
this.value = next;
|
|
1145
|
+
for (let i = 0; i < this.listeners.length; i++) {
|
|
1146
|
+
try { this.listeners[i](); } catch (e) {}
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
subscribe(fn) {
|
|
1150
|
+
const listeners = this.listeners;
|
|
1151
|
+
listeners.push(fn);
|
|
1152
|
+
return function () {
|
|
1153
|
+
const at = listeners.indexOf(fn);
|
|
1154
|
+
if (at !== -1) listeners.splice(at, 1);
|
|
1155
|
+
};
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
function useOpen() {
|
|
1160
|
+
const [, force] = React.useReducer(function (x) { return x + 1; }, 0);
|
|
1161
|
+
React.useEffect(function () { return openStore.subscribe(force); }, []);
|
|
1162
|
+
return openStore.value;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* The run lives on the host, so it continues while the console is closed.
|
|
1167
|
+
* The launcher keeps polling a summary of it either way — closed, it is
|
|
1168
|
+
* the only thing telling you the run is still going.
|
|
1169
|
+
*/
|
|
1170
|
+
function useSummary(open) {
|
|
1171
|
+
const [summary, setSummary] = React.useState(null);
|
|
1172
|
+
React.useEffect(function () {
|
|
1173
|
+
let alive = true;
|
|
1174
|
+
const tick = function () {
|
|
1175
|
+
api('GET').then(function (v) {
|
|
1176
|
+
if (!alive) return;
|
|
1177
|
+
setSummary({
|
|
1178
|
+
running: !!v.running,
|
|
1179
|
+
paused: !!v.paused,
|
|
1180
|
+
active: v.active || 0,
|
|
1181
|
+
launched: v.launched || 0,
|
|
1182
|
+
kept: (v.attempts || []).filter(function (a) { return a.verdict === 'rollout'; }).length,
|
|
1183
|
+
best: (v.attempts || []).reduce(function (m, a) {
|
|
1184
|
+
return typeof a.score === 'number' && a.score > m ? a.score : m;
|
|
1185
|
+
}, 0),
|
|
1186
|
+
});
|
|
1187
|
+
}).catch(function () {});
|
|
1188
|
+
};
|
|
1189
|
+
tick();
|
|
1190
|
+
const timer = setInterval(tick, open ? 4000 : 2000);
|
|
1191
|
+
return function () { alive = false; clearInterval(timer); };
|
|
1192
|
+
}, [open]);
|
|
1193
|
+
return summary;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* The shell's Think glyph — the same mark the chat puts beside a
|
|
1198
|
+
* chain-of-thought, which is exactly what this plugin reads. Sized the way
|
|
1199
|
+
* the Settings row sizes its own icon: the 16 variant at 16 in the wide
|
|
1200
|
+
* column, the 14 variant at 18 in the rail.
|
|
1201
|
+
*/
|
|
1202
|
+
function ScoutIcon(props) {
|
|
1203
|
+
const rail = props.rail;
|
|
1204
|
+
const Icon = primitives
|
|
1205
|
+
&& (rail ? primitives.IconThinkOutline14 : primitives.IconThinkOutline16);
|
|
1206
|
+
if (Icon) return React.createElement(Icon, { size: rail ? 18 : 16 });
|
|
1207
|
+
// Concentric sweep, in case the primitives package is not resolvable.
|
|
1208
|
+
const size = rail ? 18 : 16;
|
|
1209
|
+
return React.createElement('svg', {
|
|
1210
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true',
|
|
1211
|
+
},
|
|
1212
|
+
React.createElement('circle', {
|
|
1213
|
+
cx: 8, cy: 8, r: 6.25, stroke: 'currentColor', strokeWidth: 1.3, opacity: 0.55,
|
|
1214
|
+
}),
|
|
1215
|
+
React.createElement('circle', { cx: 8, cy: 8, r: 2, fill: 'currentColor' })
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* The launcher, seated at the sidebar foot beside Settings. `wide` is the
|
|
1221
|
+
* shell's fold state: false means the 56px rail, where the label is gone
|
|
1222
|
+
* and the run has to be legible from the icon and its status pip alone.
|
|
1223
|
+
*/
|
|
1224
|
+
function SidebarSeat(props) {
|
|
1225
|
+
const wide = props.wide !== false;
|
|
1226
|
+
const open = useOpen();
|
|
1227
|
+
const s = useSummary(open);
|
|
1228
|
+
const running = !!(s && s.running);
|
|
1229
|
+
const paused = !!(s && s.paused);
|
|
1230
|
+
const caught = !!(s && s.kept > 0);
|
|
1231
|
+
const tone = caught ? 'caught' : running ? 'live' : paused ? 'paused' : null;
|
|
1232
|
+
const title = !s || (!running && !paused && s.launched === 0) ? t('launcher')
|
|
1233
|
+
: running ? t('pillRunning', { active: s.active, launched: s.launched })
|
|
1234
|
+
: paused ? t('pillPaused', { launched: s.launched })
|
|
1235
|
+
: t('pillDone', { launched: s.launched });
|
|
1236
|
+
return React.createElement('button', {
|
|
1237
|
+
type: 'button',
|
|
1238
|
+
className: 'rsc-seat',
|
|
1239
|
+
'data-rail': wide ? undefined : '',
|
|
1240
|
+
'data-open': open ? '' : undefined,
|
|
1241
|
+
'aria-haspopup': 'dialog',
|
|
1242
|
+
'aria-expanded': open,
|
|
1243
|
+
'aria-label': t('launcher'),
|
|
1244
|
+
title: s && s.launched > 0 ? title + ' · ' + t('pillBest', { score: pct(s.best) }) : title,
|
|
1245
|
+
onClick: function () { openStore.set(!open); },
|
|
1246
|
+
},
|
|
1247
|
+
React.createElement('span', { className: 'rsc-seat-icon' },
|
|
1248
|
+
React.createElement(ScoutIcon, { rail: !wide }),
|
|
1249
|
+
tone ? React.createElement('span', { className: 'rsc-pip', 'data-tone': tone }) : null
|
|
1250
|
+
),
|
|
1251
|
+
wide ? React.createElement('span', { className: 'rsc-seat-label' }, t('launcher')) : null,
|
|
1252
|
+
wide && caught
|
|
1253
|
+
? React.createElement('span', { className: 'rsc-seat-badge' }, s.kept)
|
|
1254
|
+
: wide && running
|
|
1255
|
+
? React.createElement('span', { className: 'rsc-seat-meta' }, t('seatLive', { active: s.active }))
|
|
1256
|
+
: null
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** The full-frame console. Opened from the seat, closed from its own ✕. */
|
|
1261
|
+
function ConsoleSurface() {
|
|
1262
|
+
const open = useOpen();
|
|
1263
|
+
if (!open) return null;
|
|
1264
|
+
const view = React.createElement(ScoutView, {
|
|
1265
|
+
onClose: function () { openStore.set(false); },
|
|
1266
|
+
onSurfaceClick: function (event) {
|
|
1267
|
+
const g = realGlobal();
|
|
1268
|
+
const seat = g && g.document && g.document.querySelector('.rsc-seat');
|
|
1269
|
+
if (!seat || typeof seat.getBoundingClientRect !== 'function') return;
|
|
1270
|
+
const rect = seat.getBoundingClientRect();
|
|
1271
|
+
if (event.clientX >= rect.left && event.clientX <= rect.right
|
|
1272
|
+
&& event.clientY >= rect.top && event.clientY <= rect.bottom) {
|
|
1273
|
+
openStore.set(false);
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
});
|
|
1277
|
+
const g = realGlobal();
|
|
1278
|
+
const body = g && g.document && g.document.body;
|
|
1279
|
+
if (!reactDom || typeof reactDom.createPortal !== 'function' || !body) return view;
|
|
1280
|
+
return reactDom.createPortal(view, body);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// The launcher is a sidebar footer action — a declared seat beside
|
|
1284
|
+
// Settings, rather than a pill floating over the composer's send button.
|
|
1285
|
+
slots.inject('sidebar.footer.action', function () {
|
|
1286
|
+
return slots.register({ name: 'sidebar.footer.action', id: 'rollout-scout', order: 120 }, SidebarSeat);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// The console itself still needs the frame-wide layer: it covers the whole
|
|
1290
|
+
// window, which nothing inside the sidebar column could do.
|
|
1291
|
+
slots.inject('shell.overlay', function () {
|
|
1292
|
+
return slots.register({ name: 'shell.overlay', id: 'rollout-scout', order: 120 }, ConsoleSurface);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
};
|