@xingjisoft/neutrino 0.3.0 → 0.4.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.
Files changed (56) hide show
  1. package/README.md +33 -549
  2. package/assets/neutrino.ico +0 -0
  3. package/assets/neutrino.svg +7 -0
  4. package/assets/pro/backend_output_model.txt +5 -0
  5. package/assets/pro/claude-module-prompt.txt +65 -0
  6. package/assets/pro/common.txt +9 -0
  7. package/assets/pro/confirm_model.txt +5 -0
  8. package/assets/pro/front_output_model.txt +9 -0
  9. package/assets/pro/genimage_model.txt +1 -0
  10. package/assets/pro/main_model.txt +5 -0
  11. package/assets/pro/model-template.json +376 -0
  12. package/assets/pro/modeling_model.txt +5 -0
  13. package/assets/pro/plan_model.txt +8 -0
  14. package/assets/pro/thinking_model.txt +3 -0
  15. package/dist/agents.js +22 -3
  16. package/dist/app.js +199 -26
  17. package/dist/background.js +358 -0
  18. package/dist/bin.js +184 -44
  19. package/dist/browser.js +57 -0
  20. package/dist/config.js +174 -26
  21. package/dist/header.js +2 -1
  22. package/dist/index.js +1 -1
  23. package/dist/llm/approval.js +6 -2
  24. package/dist/llm/client.js +197 -28
  25. package/dist/llm/subagent.js +37 -1
  26. package/dist/llm/tools.js +3 -1
  27. package/dist/migration.js +33 -0
  28. package/dist/pro/agents.js +104 -0
  29. package/dist/pro/config.js +88 -0
  30. package/dist/pro/preflight.js +172 -0
  31. package/dist/screens/agentConversation.js +48 -5
  32. package/dist/screens/agentHistory.js +4 -2
  33. package/dist/screens/chat.js +14 -8
  34. package/dist/screens/modelSelect.js +15 -4
  35. package/dist/screens/settings.js +52 -9
  36. package/dist/screens/start.js +48 -15
  37. package/dist/state.js +1 -0
  38. package/dist/terminal/restore.js +3 -12
  39. package/dist/tray.js +138 -0
  40. package/dist/ui.js +58 -0
  41. package/dist/web/hub.js +26 -4
  42. package/dist/web/server.js +495 -74
  43. package/dist/web/service.js +135 -0
  44. package/dist/web/session.js +278 -25
  45. package/dist/web/settings.js +97 -24
  46. package/dist/web/terminalBridge.js +250 -0
  47. package/package.json +37 -37
  48. package/web/README.md +150 -16
  49. package/web/app.js +490 -25
  50. package/web/i18n.js +396 -345
  51. package/web/index.html +120 -15
  52. package/web/neutrino-pro.gif +0 -0
  53. package/web/neutrino.png +0 -0
  54. package/web/pro.js +184 -0
  55. package/web/settings.js +373 -247
  56. package/web/style.css +398 -0
package/web/index.html CHANGED
@@ -5,6 +5,23 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
6
6
  <meta name="color-scheme" content="dark" />
7
7
  <meta name="theme-color" content="#151517" />
8
+ <script>
9
+ // Switching to the Pro workspace navigates only once the #151515 curtain
10
+ // is opaque. Cover the page here, before the first paint, so a slow
11
+ // module load cannot flash the freshly loaded /pro page before app.js
12
+ // takes the curtain over. app.js removes the class; the timer is a
13
+ // safety net for when the module cannot run at all.
14
+ try {
15
+ var proPath = location.pathname === "/pro" || location.pathname.startsWith("/pro/");
16
+ var proStart = Number(sessionStorage.getItem("neutrino.pro.transition")) || 0;
17
+ if (proPath && proStart && Date.now() - proStart < 8000) {
18
+ document.documentElement.classList.add("pro-transitioning");
19
+ setTimeout(function () {
20
+ document.documentElement.classList.remove("pro-transitioning");
21
+ }, 8000);
22
+ }
23
+ } catch (error) { /* Storage may be blocked; the page still works. */ }
24
+ </script>
8
25
  <title>Neutrino</title>
9
26
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f9fafb' stroke-width='1.7'%3E%3Cellipse cx='12' cy='12' rx='9.4' ry='3.8' transform='rotate(-30 12 12)'/%3E%3Ccircle cx='12' cy='12' r='3.1'/%3E%3C/svg%3E" />
10
27
  <link rel="stylesheet" href="style.css" />
@@ -13,14 +30,14 @@
13
30
  <div id="app">
14
31
  <aside id="sidebar">
15
32
  <header class="brand">
16
- <div class="logo" aria-hidden="true">
33
+ <button id="brand-logo" class="logo brand-logo" type="button" title="Switch to Pro mode" aria-label="Switch to Pro mode" data-i18n-title="Switch to Pro mode" data-i18n-aria-label="Switch to Pro mode">
17
34
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
18
35
  <ellipse cx="12" cy="12" rx="9.4" ry="3.8" transform="rotate(-30 12 12)" stroke="currentColor" stroke-width="1.7" />
19
36
  <circle cx="12" cy="12" r="3.1" stroke="currentColor" stroke-width="1.7" />
20
37
  </svg>
21
- </div>
38
+ </button>
22
39
  <div class="brand-text">
23
- <strong>Neutrino</strong>
40
+ <strong id="brand-name">Neutrino</strong>
24
41
  <span id="status" class="status">connecting…</span>
25
42
  </div>
26
43
  <div class="brand-actions">
@@ -32,6 +49,12 @@
32
49
  </button>
33
50
  </div>
34
51
  </header>
52
+ <div id="pro-tip" class="pro-tip hidden" role="note">
53
+ <span data-i18n="Click the badge to switch to Pro mode">Click the badge to switch to Pro mode</span>
54
+ <button id="pro-tip-close" class="pro-tip-close" type="button" title="Dismiss" aria-label="Dismiss" data-i18n-title="Dismiss" data-i18n-aria-label="Dismiss">
55
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
56
+ </button>
57
+ </div>
35
58
 
36
59
  <div class="sidebar-tools">
37
60
  <button id="new-chat" class="new-chat" type="button" title="New conversation" data-i18n-title="New conversation">
@@ -73,6 +96,10 @@
73
96
  <div id="session-sub" class="sub"></div>
74
97
  </div>
75
98
  <div id="controls" class="controls"></div>
99
+ <button id="pro-exit" class="control pro-exit hidden" type="button" title="Exit Pro mode" aria-label="Exit Pro mode" data-i18n-title="Exit Pro mode" data-i18n-aria-label="Exit Pro mode">
100
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 5-7 7 7 7M5 12h14" /></svg>
101
+ <span data-i18n="Exit Pro mode">Exit Pro mode</span>
102
+ </button>
76
103
  </header>
77
104
 
78
105
  <form id="model-picker" class="model-picker hidden" aria-label="Conversation model">
@@ -101,12 +128,41 @@
101
128
 
102
129
  <div id="approval" class="approval hidden" role="alert" aria-live="assertive"></div>
103
130
 
131
+ <footer id="agent-actions" class="agent-actions hidden">
132
+ <p id="agent-migrate-feedback" class="form-feedback error" role="status" aria-live="polite"></p>
133
+ <button id="agent-migrate" class="control primary" type="button" data-i18n="Migrate session" title="Copy this history into a new Neutrino conversation in the same folder" data-i18n-title="Copy this history into a new Neutrino conversation in the same folder">Migrate session</button>
134
+ </footer>
135
+
104
136
  <footer id="composer" class="composer hidden">
105
137
  <div class="composer-inner">
106
138
  <div class="composer-card">
107
139
  <textarea id="input" rows="1" placeholder="Message Neutrino…" aria-label="Message" data-i18n-placeholder="Message Neutrino…"></textarea>
140
+ <details id="pro-preset-control" class="pro-preset-control hidden">
141
+ <summary aria-label="Pro preset" data-i18n-aria-label="Pro preset"><span id="pro-preset-label">Pro</span><svg class="pro-preset-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
142
+ <div class="pro-preset-card">
143
+ <div class="pro-preset-heading"><span data-i18n="Neutrino Pro">Neutrino Pro</span><strong id="pro-preset-name"></strong></div>
144
+ <label class="pro-slider-label" for="pro-preset-range" data-i18n="Preset">Preset</label>
145
+ <div id="pro-preset-slider" class="pro-preset-slider">
146
+ <div class="pro-slider-track" aria-hidden="true">
147
+ <div class="pro-slider-fill"></div>
148
+ <div class="pro-slider-meteors">
149
+ <span class="pro-slider-meteor"></span><span class="pro-slider-meteor"></span><span class="pro-slider-meteor"></span><span class="pro-slider-meteor"></span>
150
+ </div>
151
+ </div>
152
+ <div id="pro-slider-stops" class="pro-slider-stops" aria-hidden="true"></div>
153
+ <input id="pro-preset-range" type="range" min="0" max="0" step="1" value="0" aria-label="Pro preset" data-i18n-aria-label="Pro preset" />
154
+ </div>
155
+ <div id="pro-preset-ticks" class="pro-preset-ticks"></div>
156
+ <details class="pro-model-details" id="pro-model-details">
157
+ <summary><span data-i18n="Model details">Model details</span><svg class="pro-model-details-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
158
+ <div id="pro-preset-models" class="pro-preset-models"></div>
159
+ </details>
160
+ <p id="pro-preset-note" class="settings-note"></p>
161
+ <p id="pro-preset-feedback" class="form-feedback error" role="status" aria-live="polite"></p>
162
+ </div>
163
+ </details>
108
164
  <button id="send" class="primary" type="button" title="Send message" aria-label="Send message" data-i18n-title="Send message" data-i18n-aria-label="Send message">
109
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
165
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /></svg>
110
166
  </button>
111
167
  </div>
112
168
  <div class="composer-hint">
@@ -146,23 +202,37 @@
146
202
  </select>
147
203
  </label>
148
204
  </div>
149
- <label class="checkbox-label"><input id="settings-plan" type="checkbox" /> <span data-i18n="Plan-first turns">Plan-first turns</span></label>
150
- <p class="settings-note" data-i18n="Split multi-step requests into a plan before answering.">Split multi-step requests into a plan before answering.</p>
151
- <h3 data-i18n="Defaults for new conversations">Defaults for new conversations</h3>
152
- <div class="settings-grid">
205
+ <label class="checkbox-label standard-only"><input id="settings-plan" type="checkbox" /> <span data-i18n="Plan-first turns">Plan-first turns</span></label>
206
+ <p class="settings-note standard-only" data-i18n="Split multi-step requests into a plan before answering.">Split multi-step requests into a plan before answering.</p>
207
+ <h3 class="standard-only" data-i18n="Defaults for new conversations">Defaults for new conversations</h3>
208
+ <div class="settings-grid standard-only">
153
209
  <label><span data-i18n="Provider">Provider</span><select id="default-provider"></select></label>
154
210
  <label><span data-i18n="Model">Model</span><select id="default-model"></select></label>
155
211
  <label><span data-i18n="Thinking intensity">Thinking intensity</span><select id="default-intensity"></select></label>
156
212
  </div>
213
+ <section id="settings-pro" class="hidden">
214
+ <h3 data-i18n="Pro orchestration">Pro orchestration</h3>
215
+ <div class="settings-grid">
216
+ <label><span data-i18n="Default Pro preset">Default Pro preset</span><select id="settings-pro-preset"></select></label>
217
+ <label><span data-i18n="Maximum delegation depth">Maximum delegation depth</span><input id="settings-pro-depth" type="number" min="1" max="100" step="1" value="10" /></label>
218
+ </div>
219
+ <p class="settings-note" data-i18n="Every Pro task is planned first. All roles share the regular provider configuration. Image generation is reserved.">Every Pro task is planned first. All roles share the regular provider configuration. Image generation is reserved.</p>
220
+ <p class="settings-note" data-i18n="Edit role models, intensity and context windows in ~/.neutrino/pro/model.json. Default delegation depth: 10.">Edit role models, intensity and context windows in ~/.neutrino/pro/model.json. Default delegation depth: 10.</p>
221
+ </section>
157
222
  <h3 data-i18n="Conversation and approvals">Conversation and approvals</h3>
158
- <label><span data-i18n="Model context window (tokens)">Model context window (tokens)</span><input id="settings-context" type="number" min="1" max="9007199254740991" step="1" required /></label>
159
- <p id="settings-compress-note" class="settings-note" data-i18n="Auto-compress at 80% of this model's context window. Default: 1,000,000 tokens.">Auto-compress at 80% of this model's context window. Default: 1,000,000 tokens.</p>
160
- <label><span data-i18n="Approval reviewer model">Approval reviewer model</span><select id="settings-reviewer"></select></label>
223
+ <label class="standard-only"><span data-i18n="Model context window (tokens)">Model context window (tokens)</span><input id="settings-context" type="number" min="1" max="9007199254740991" step="1" required /></label>
224
+ <p id="settings-compress-note" class="settings-note standard-only" data-i18n="Auto-compress at 80% of this model's context window. Default: 1,000,000 tokens.">Auto-compress at 80% of this model's context window. Default: 1,000,000 tokens.</p>
225
+ <label class="standard-only"><span data-i18n="Approval reviewer model">Approval reviewer model</span><select id="settings-reviewer"></select></label>
161
226
  <label class="checkbox-label"><input id="settings-extreme" type="checkbox" /> <span data-i18n="Extreme-risk auto-approval (del class, 3 separate review rounds)">Extreme-risk auto-approval (del class, 3 separate review rounds)</span></label>
162
227
  <label class="checkbox-label"><input id="settings-sound" type="checkbox" /> <span data-i18n="Sound alerts (turn finished, approval waiting)">Sound alerts (turn finished, approval waiting)</span></label>
163
228
  <label class="checkbox-label"><input id="settings-web-console" type="checkbox" /> <span data-i18n="Start the Web console with the terminal">Start the Web console with the terminal</span></label>
164
229
  <p class="settings-note" data-i18n="Safety determines automatic approval; task relevance only blocks unanimously confirmed, extreme mismatches. Delete-class automatic approval also requires this setting.">Safety determines automatic approval; task relevance only blocks unanimously confirmed, extreme mismatches. Delete-class automatic approval also requires this setting.</p>
230
+ <h3 data-i18n="Background residency">Background residency</h3>
231
+ <label class="checkbox-label"><input id="settings-background" type="checkbox" /> <span data-i18n="Run the Web console in the background and keep browser tasks alive after the CLI exits">Run the Web console in the background and keep browser tasks alive after the CLI exits</span></label>
232
+ <p class="settings-note" data-i18n="Off by default. Changes take effect immediately, including for running browser tasks. The regular and Pro workspaces share this setting. Terminal conversations still end when the terminal closes.">Off by default. Changes take effect immediately, including for running browser tasks. The regular and Pro workspaces share this setting. Terminal conversations still end when the terminal closes.</p>
165
233
  <h3 data-i18n="Web access">Web access</h3>
234
+ <label class="checkbox-label"><input id="settings-web-lan" type="checkbox" /> <span data-i18n="Allow access from other devices (LAN, 0.0.0.0)">Allow access from other devices (LAN, 0.0.0.0)</span></label>
235
+ <p class="settings-note" data-i18n="Binds the Web console to 0.0.0.0 so other devices on your network can open it, like --lan. Takes effect immediately; turning it off returns the console to 127.0.0.1. Keep the Web password on: anyone who can reach this port can drive the agent.">Binds the Web console to 0.0.0.0 so other devices on your network can open it, like --lan. Takes effect immediately; turning it off returns the console to 127.0.0.1. Keep the Web password on: anyone who can reach this port can drive the agent.</p>
166
236
  <div class="settings-grid">
167
237
  <label><span data-i18n="New Web password">New Web password</span><input id="settings-password" type="password" autocomplete="new-password" maxlength="256" placeholder="Leave blank to keep current password" data-i18n-placeholder="Leave blank to keep current password" /></label>
168
238
  <label><span data-i18n="Confirm password">Confirm password</span><input id="settings-password-confirm" type="password" autocomplete="new-password" maxlength="256" placeholder="Repeat new password" data-i18n-placeholder="Repeat new password" /></label>
@@ -177,12 +247,34 @@
177
247
  </form>
178
248
  </dialog>
179
249
 
250
+ <dialog id="background-dialog" class="sheet-dialog background-dialog" aria-labelledby="background-title">
251
+ <div class="sheet background-sheet">
252
+ <header class="sheet-header background-header">
253
+ <span class="background-mark" aria-hidden="true">
254
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
255
+ <ellipse cx="12" cy="12" rx="9.4" ry="3.8" transform="rotate(-30 12 12)" stroke="currentColor" stroke-width="1.7" />
256
+ <circle cx="12" cy="12" r="3.1" stroke="currentColor" stroke-width="1.7" />
257
+ </svg>
258
+ </span>
259
+ <div class="background-copy">
260
+ <h2 id="background-title" data-i18n="Enable background residency to avoid accidentally interrupting tasks">Enable background residency to avoid accidentally interrupting tasks</h2>
261
+ </div>
262
+ </header>
263
+ <p id="background-feedback" class="form-feedback background-feedback" role="status" aria-live="polite"></p>
264
+ <footer class="sheet-actions background-actions">
265
+ <button id="background-cancel" class="control" type="button" data-i18n="Cancel">Cancel</button>
266
+ <button id="background-enable" class="primary" type="button" data-i18n="Enable" autofocus>Enable</button>
267
+ </footer>
268
+ </div>
269
+ </dialog>
270
+
180
271
  <dialog id="new-chat-dialog" class="sheet-dialog" aria-labelledby="new-chat-title">
181
272
  <form id="new-chat-form" class="sheet">
182
273
  <header class="sheet-header">
183
274
  <div>
184
275
  <h2 id="new-chat-title" data-i18n="New conversation">New conversation</h2>
185
- <p class="sheet-sub" data-i18n="Choose a working directory and a model.">Choose a working directory and a model.</p>
276
+ <p class="sheet-sub standard-only" data-i18n="Choose a working directory and a model.">Choose a working directory and a model.</p>
277
+ <p class="sheet-sub pro-only" data-i18n="Choose a working directory and a professional team preset.">Choose a working directory and a professional team preset.</p>
186
278
  </div>
187
279
  <button id="new-chat-close" class="icon-btn" type="button" aria-label="Close" data-i18n-aria-label="Close">
188
280
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
@@ -200,19 +292,20 @@
200
292
  <div id="new-chat-recents" class="workspace-recents"></div>
201
293
  </section>
202
294
  <section class="field-group">
203
- <h3 data-i18n="Model">Model</h3>
204
- <div class="settings-grid">
295
+ <h3 class="standard-only" data-i18n="Model">Model</h3>
296
+ <div class="settings-grid standard-only">
205
297
  <label><span data-i18n="Provider">Provider</span><select id="new-chat-provider"></select></label>
206
298
  <label><span data-i18n="Model">Model</span><select id="new-chat-model"></select></label>
207
299
  <label><span data-i18n="Thinking">Thinking</span><select id="new-chat-intensity"></select></label>
208
300
  </div>
301
+ <label class="pro-only"><span data-i18n="Pro preset">Pro preset</span><select id="new-chat-pro-preset"></select></label>
209
302
  <label><span data-i18n="Default approval mode">Default approval mode</span>
210
303
  <select id="new-chat-approval">
211
304
  <option value="request" data-i18n="Request Approval">Request Approval</option>
212
305
  <option value="auto" data-i18n="Auto Approval">Auto Approval</option>
213
306
  </select>
214
307
  </label>
215
- <p id="new-chat-note" class="settings-note"></p>
308
+ <p id="new-chat-note" class="settings-note standard-only"></p>
216
309
  </section>
217
310
  <p id="new-chat-feedback" class="form-feedback" role="status" aria-live="polite"></p>
218
311
  </div>
@@ -299,6 +392,18 @@
299
392
  </div>
300
393
  </dialog>
301
394
 
395
+ <dialog id="pro-fallback-dialog" class="sheet-dialog pro-fallback-dialog" aria-labelledby="pro-fallback-title">
396
+ <div class="sheet">
397
+ <header class="sheet-header"><h2 id="pro-fallback-title" data-i18n="Professional model availability">Professional model availability</h2></header>
398
+ <div class="sheet-body">
399
+ <p class="settings-note" data-i18n="These model substitutions apply only to this task in this conversation. Your preset configuration is unchanged.">These model substitutions apply only to this task in this conversation. Your preset configuration is unchanged.</p>
400
+ <div id="pro-fallback-rows" class="pro-fallback-rows"></div>
401
+ <p id="pro-fallback-error" class="form-feedback error hidden" role="alert"></p>
402
+ </div>
403
+ <footer class="sheet-actions"><button id="pro-fallback-close" class="primary" type="button" data-i18n="Got it">Got it</button></footer>
404
+ </div>
405
+ </dialog>
406
+
302
407
  <script src="app.js" type="module"></script>
303
408
  </body>
304
409
  </html>
Binary file
Binary file
package/web/pro.js ADDED
@@ -0,0 +1,184 @@
1
+ import { t, getLanguage } from "./i18n.js";
2
+
3
+ const ROLES = {
4
+ main_model: "Main model", thinking_model: "Reasoning model",
5
+ front_output_model: "Frontend model", backend_output_model: "Backend model",
6
+ confirm_model: "Approval model", genimage_model: "Image model",
7
+ plan_model: "Planning model", modeling_model: "Modeling model",
8
+ };
9
+
10
+ /** The preset is a conversation choice; effective fallbacks belong to one task. */
11
+ export function createProUI({ changePreset }) {
12
+ const control = document.getElementById("pro-preset-control");
13
+ if (!control) return null;
14
+ const label = document.getElementById("pro-preset-label");
15
+ const slider = document.getElementById("pro-preset-slider");
16
+ const stops = document.getElementById("pro-slider-stops");
17
+ const range = document.getElementById("pro-preset-range");
18
+ const name = document.getElementById("pro-preset-name");
19
+ const ticks = document.getElementById("pro-preset-ticks");
20
+ const models = document.getElementById("pro-preset-models");
21
+ const note = document.getElementById("pro-preset-note");
22
+ const feedback = document.getElementById("pro-preset-feedback");
23
+ const fallbackDialog = document.getElementById("pro-fallback-dialog");
24
+ const fallbackRows = document.getElementById("pro-fallback-rows");
25
+ const fallbackError = document.getElementById("pro-fallback-error");
26
+ const seenChecks = new Set();
27
+ let presets = [];
28
+ let selected = "";
29
+ let committed = "";
30
+ let defaultPreset = "";
31
+ let target = null;
32
+ let snapshot = null;
33
+ let applying = false;
34
+ let pending = Promise.resolve();
35
+ let failure = null;
36
+ const title = preset => getLanguage() === "zh" ? preset.chinese_name || preset.name : preset.name;
37
+ const presetFor = value => presets.find(entry => entry.name === value);
38
+ const modelName = value => value == null || value === "null" || value === "" ? t("Reserved") : value;
39
+ const reference = value => value?.provider ? `${value.provider}/${modelName(value.model)}` : modelName(value?.model);
40
+
41
+ function render() {
42
+ const preset = presetFor(selected) || presets[0];
43
+ control.classList.toggle("hidden", !preset);
44
+ if (!preset) return;
45
+ selected = preset.name;
46
+ const busy = Boolean(snapshot?.busy || snapshot?.approval || applying);
47
+ label.textContent = `Pro · ${title(preset)}`;
48
+ name.textContent = title(preset);
49
+ const selectedIndex = presets.indexOf(preset);
50
+ const lastIndex = presets.length - 1;
51
+ const position = lastIndex > 0 ? selectedIndex / lastIndex : 0;
52
+ slider.setAttribute("style", `--pro-slider-position: ${position}`);
53
+ slider.classList.toggle("is-disabled", busy);
54
+ control.classList.toggle("is-max", lastIndex > 0 && selectedIndex === lastIndex);
55
+ stops.replaceChildren();
56
+ presets.forEach((_entry, index) => {
57
+ const stop = document.createElement("span");
58
+ stop.className = "pro-slider-stop";
59
+ stop.setAttribute("style", `left: ${lastIndex > 0 ? index / lastIndex * 100 : 0}%`);
60
+ stop.classList.toggle("is-filled", index <= selectedIndex);
61
+ stops.appendChild(stop);
62
+ });
63
+ range.max = String(Math.max(0, presets.length - 1));
64
+ range.value = String(selectedIndex);
65
+ range.disabled = busy;
66
+ range.setAttribute("aria-valuetext", title(preset));
67
+ ticks.replaceChildren();
68
+ presets.forEach((entry, index) => {
69
+ const tick = document.createElement("button");
70
+ tick.type = "button";
71
+ tick.textContent = title(entry);
72
+ tick.disabled = busy;
73
+ tick.setAttribute("style", `left: ${lastIndex > 0 ? index / lastIndex * 100 : 0}%`);
74
+ tick.setAttribute("aria-pressed", String(entry.name === selected));
75
+ tick.addEventListener("click", () => choose(entry.name));
76
+ ticks.appendChild(tick);
77
+ });
78
+ models.replaceChildren();
79
+ for (const role of Object.keys(ROLES)) {
80
+ const configured = preset["model-setting"]?.find(entry => entry.type === role);
81
+ const effective = snapshot?.pro?.preset === selected && !snapshot.pro.checking
82
+ ? snapshot.pro.models?.find(entry => entry.role === role) : null;
83
+ const row = document.createElement("div");
84
+ row.className = "pro-model-row";
85
+ const roleLabel = document.createElement("span");
86
+ roleLabel.textContent = t(ROLES[role]);
87
+ const value = document.createElement("span");
88
+ value.textContent = modelName(effective?.model ?? configured?.model);
89
+ value.title = [effective?.provider ?? configured?.provider, effective?.intensity ?? configured?.intensity].filter(Boolean).join(" · ");
90
+ if (effective && effective.tier !== selected) {
91
+ value.classList.add("pro-model-fallback");
92
+ value.title += ` · ${t("Temporary fallback")}: ${effective.tier}`;
93
+ }
94
+ row.append(roleLabel, value);
95
+ models.appendChild(row);
96
+ }
97
+ note.textContent = snapshot?.pro?.checking
98
+ ? t("Checking every professional model before this task…")
99
+ : busy ? t("Pause the current turn to change presets.")
100
+ : t("Models are checked before each task. Unavailable roles temporarily use a lower preset.");
101
+ }
102
+
103
+ function choose(value) {
104
+ if (snapshot?.busy || snapshot?.approval || applying || !presetFor(value)) return;
105
+ const previous = committed || selected;
106
+ const originalTarget = target;
107
+ selected = value;
108
+ failure = null;
109
+ feedback.textContent = "";
110
+ applying = true;
111
+ render();
112
+ pending = Promise.resolve().then(() => changePreset(value)).then(() => {
113
+ if (target === originalTarget) committed = value;
114
+ }).catch(error => {
115
+ if (target !== originalTarget) return;
116
+ failure = error;
117
+ selected = previous;
118
+ feedback.textContent = error.message;
119
+ }).finally(() => { applying = false; render(); });
120
+ }
121
+
122
+ function showFallbacks(pro, key) {
123
+ if (!pro || pro.checking || !pro.checkId) return;
124
+ const id = `${key || ""}:${pro.checkId}`;
125
+ if (seenChecks.has(id)) return;
126
+ seenChecks.add(id);
127
+ if (!(pro.fallbacks?.length) && !pro.error) return;
128
+ fallbackRows.replaceChildren();
129
+ for (const fallback of pro.fallbacks || []) {
130
+ const row = document.createElement("div");
131
+ row.className = "pro-fallback-row";
132
+ const role = document.createElement("strong");
133
+ role.textContent = t(ROLES[fallback.role] || fallback.role);
134
+ const from = document.createElement("span");
135
+ from.textContent = `${reference(fallback.requested)} → ${fallback.replacement ? reference(fallback.replacement) : t("No available model")}`;
136
+ const detail = document.createElement("small");
137
+ detail.textContent = [fallback.reason, fallback.replacement?.tier].filter(Boolean).join(" · ");
138
+ row.append(role, from, detail);
139
+ fallbackRows.appendChild(row);
140
+ }
141
+ fallbackError.textContent = pro.error || "";
142
+ fallbackError.classList.toggle("hidden", !pro.error);
143
+ if (!fallbackDialog.open) fallbackDialog.showModal();
144
+ }
145
+
146
+ range.addEventListener("input", () => {
147
+ if (snapshot?.busy || applying) return;
148
+ selected = presets[Number(range.value)]?.name || selected;
149
+ render();
150
+ });
151
+ range.addEventListener("change", () => choose(selected));
152
+ control.addEventListener("keydown", event => {
153
+ if (event.key === "Escape") { control.open = false; event.stopPropagation(); }
154
+ });
155
+ document.addEventListener("pointerdown", event => {
156
+ if (control.open && !control.contains(event.target)) control.open = false;
157
+ });
158
+ document.getElementById("pro-fallback-close").addEventListener("click", () => fallbackDialog.close());
159
+
160
+ return {
161
+ load(catalog) {
162
+ presets = Array.isArray(catalog?.pro?.presets) ? catalog.pro.presets : [];
163
+ defaultPreset = catalog?.pro?.preset || presets[0]?.name || "";
164
+ if (!presetFor(selected) || (!target && !applying)) selected = defaultPreset;
165
+ if (!committed) committed = selected;
166
+ render();
167
+ },
168
+ show(info, key) {
169
+ if (target !== key) {
170
+ selected = info?.pro?.preset || info?.pro_preset || defaultPreset;
171
+ committed = selected;
172
+ target = key;
173
+ failure = null;
174
+ feedback.textContent = "";
175
+ } else if (!applying && info?.pro?.preset) committed = selected = info.pro.preset;
176
+ snapshot = info;
177
+ render();
178
+ showFallbacks(info?.pro, key);
179
+ },
180
+ refresh: render,
181
+ preset: () => selected,
182
+ async flush() { await pending; if (failure) throw failure; },
183
+ };
184
+ }