@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.
- package/README.md +33 -549
- package/assets/neutrino.ico +0 -0
- package/assets/neutrino.svg +7 -0
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/agents.js +22 -3
- package/dist/app.js +199 -26
- package/dist/background.js +358 -0
- package/dist/bin.js +184 -44
- package/dist/browser.js +57 -0
- package/dist/config.js +174 -26
- package/dist/header.js +2 -1
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/llm/tools.js +3 -1
- package/dist/migration.js +33 -0
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/agentConversation.js +48 -5
- package/dist/screens/agentHistory.js +4 -2
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +52 -9
- package/dist/screens/start.js +48 -15
- package/dist/state.js +1 -0
- package/dist/terminal/restore.js +3 -12
- package/dist/tray.js +138 -0
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +26 -4
- package/dist/web/server.js +495 -74
- package/dist/web/service.js +135 -0
- package/dist/web/session.js +278 -25
- package/dist/web/settings.js +97 -24
- package/dist/web/terminalBridge.js +250 -0
- package/package.json +37 -37
- package/web/README.md +150 -16
- package/web/app.js +490 -25
- package/web/i18n.js +396 -345
- package/web/index.html +120 -15
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +373 -247
- package/web/style.css +398 -0
package/dist/config.js
CHANGED
|
@@ -11,6 +11,36 @@ export const NEUTRINO_DIR = path.join(os.homedir(), ".neutrino");
|
|
|
11
11
|
export const MODEL_FILE = path.join(NEUTRINO_DIR, "model.json");
|
|
12
12
|
export const SESSION_DIR = path.join(NEUTRINO_DIR, "session");
|
|
13
13
|
export const CONFIG_FILE = path.join(NEUTRINO_DIR, "config.json");
|
|
14
|
+
/**
|
|
15
|
+
* The browser console has two workspaces: the regular one and the isolated
|
|
16
|
+
* "Pro" workspace served at `/pro`. Everything a workspace owns — transcripts,
|
|
17
|
+
* model defaults, plan/compaction settings, reviewer model and interface
|
|
18
|
+
* language — is stored per mode; only the credentials and the approval policy
|
|
19
|
+
* below are shared, because they protect or gate the whole process.
|
|
20
|
+
*/
|
|
21
|
+
export const MODES = ["standard", "pro"];
|
|
22
|
+
export const PRO_DIR = path.join(NEUTRINO_DIR, "pro");
|
|
23
|
+
export const PRO_CONFIG_FILE = path.join(PRO_DIR, "config.json");
|
|
24
|
+
export const PRO_SESSION_DIR = path.join(PRO_DIR, "session");
|
|
25
|
+
/**
|
|
26
|
+
* Settings that intentionally apply to both workspaces. The Web password, the
|
|
27
|
+
* approval policy, the console's interface/LAN access and background residency
|
|
28
|
+
* are properties of this process, not of a conversation workspace.
|
|
29
|
+
*/
|
|
30
|
+
export const SHARED_CONFIG_KEYS = ["web_password_hash", "approval_mode", "auto_approve_extreme_risk", "web_lan", "background"];
|
|
31
|
+
export function normalizeMode(value) {
|
|
32
|
+
return typeof value === "string" && value.trim().toLowerCase() === "pro" ? "pro" : "standard";
|
|
33
|
+
}
|
|
34
|
+
export function configFile(mode = "standard") {
|
|
35
|
+
return mode === "pro" ? PRO_CONFIG_FILE : CONFIG_FILE;
|
|
36
|
+
}
|
|
37
|
+
export function sessionDir(mode = "standard") {
|
|
38
|
+
return mode === "pro" ? PRO_SESSION_DIR : SESSION_DIR;
|
|
39
|
+
}
|
|
40
|
+
/** Key used to remember a model's context capacity per workspace. */
|
|
41
|
+
export function modelContextWindowKey(provider, modelName) {
|
|
42
|
+
return `${provider}/${modelName}`;
|
|
43
|
+
}
|
|
14
44
|
/**
|
|
15
45
|
* Whether assistant messages must be sent back with a `reasoning_content`
|
|
16
46
|
* field. DeepSeek's thinking mode requires one on *every* assistant message of
|
|
@@ -32,12 +62,26 @@ export function contextWindow(value) {
|
|
|
32
62
|
return isContextWindow(value) ? value : DEFAULT_CONTEXT_WINDOW;
|
|
33
63
|
}
|
|
34
64
|
/** Reload the exact provider/model so settings also apply to active chats. */
|
|
35
|
-
export function modelContextWindow(provider, model) {
|
|
65
|
+
export function modelContextWindow(provider, model, mode = "standard") {
|
|
66
|
+
// The Pro workspace keeps its own context capacities: changing one there must
|
|
67
|
+
// not move the compression threshold of the regular workspace (and vice
|
|
68
|
+
// versa). The capacity is a per-workspace setting, not shared model metadata,
|
|
69
|
+
// so an unset Pro value starts from the documented default instead of
|
|
70
|
+
// inheriting whatever the regular workspace saved in model.json.
|
|
71
|
+
if (mode === "pro") {
|
|
72
|
+
const configured = loadConfig("pro").model_context_windows;
|
|
73
|
+
if (configured && typeof configured === "object" && !Array.isArray(configured)) {
|
|
74
|
+
const override = configured[modelContextWindowKey(provider, model.name)];
|
|
75
|
+
if (isContextWindow(override))
|
|
76
|
+
return override;
|
|
77
|
+
}
|
|
78
|
+
return contextWindow(undefined);
|
|
79
|
+
}
|
|
36
80
|
const saved = loadModels().providers.find((p) => p.name === provider)?.models.find((m) => m.name === model.name);
|
|
37
81
|
return contextWindow((saved ?? model).context_window);
|
|
38
82
|
}
|
|
39
|
-
export function contextCompressionThreshold(provider, model) {
|
|
40
|
-
return Math.max(1, Math.floor(modelContextWindow(provider, model) * CONTEXT_COMPRESSION_RATIO));
|
|
83
|
+
export function contextCompressionThreshold(provider, model, mode = "standard") {
|
|
84
|
+
return Math.max(1, Math.floor(modelContextWindow(provider, model, mode) * CONTEXT_COMPRESSION_RATIO));
|
|
41
85
|
}
|
|
42
86
|
export const DEFAULT_MODELS = {
|
|
43
87
|
providers: [
|
|
@@ -89,6 +133,13 @@ export const DEFAULT_CONFIG = {
|
|
|
89
133
|
// Mount the browser UI alongside the terminal UI unless it is turned off
|
|
90
134
|
// here or on the command line.
|
|
91
135
|
web_console: true,
|
|
136
|
+
// Listen on every interface (the equivalent of --lan) instead of loopback.
|
|
137
|
+
// The listener belongs to the process, so both workspaces share this value.
|
|
138
|
+
web_lan: false,
|
|
139
|
+
// Keep the Web console and the conversations it runs alive after the
|
|
140
|
+
// terminal UI exits. This is a property of the machine, not of a workspace,
|
|
141
|
+
// so the regular site and Pro share it.
|
|
142
|
+
background: false,
|
|
92
143
|
// Working directories opened from the browser, newest first. They remain
|
|
93
144
|
// listed even when they hold no conversations yet.
|
|
94
145
|
web_workspaces: [],
|
|
@@ -103,6 +154,54 @@ export const INTENSITY_PARAMS = {
|
|
|
103
154
|
Max: { temperature: 1.0, top_p: 1.0 },
|
|
104
155
|
Ultra: { temperature: 1.5, top_p: 1.0 },
|
|
105
156
|
};
|
|
157
|
+
/** Canonical spelling of a configured intensity name, or null when unknown. */
|
|
158
|
+
export function normalizeIntensity(value) {
|
|
159
|
+
if (typeof value !== "string")
|
|
160
|
+
return null;
|
|
161
|
+
const wanted = value.trim().toLowerCase();
|
|
162
|
+
return INTENSITY_LEVELS.find((level) => level.toLowerCase() === wanted) ?? null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The intensities a model accepts, in the canonical Low → Ultra order.
|
|
166
|
+
*
|
|
167
|
+
* An absent or empty list keeps the historical behaviour: every level is
|
|
168
|
+
* allowed. Unknown names are ignored rather than making the model unusable, so
|
|
169
|
+
* a typo in `model.json` falls back to the remaining valid entries (or to all
|
|
170
|
+
* levels when none is valid).
|
|
171
|
+
*/
|
|
172
|
+
export function modelIntensities(model) {
|
|
173
|
+
const raw = model?.intensity;
|
|
174
|
+
const entries = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
175
|
+
const allowed = new Set();
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const level = normalizeIntensity(entry);
|
|
178
|
+
if (level)
|
|
179
|
+
allowed.add(level);
|
|
180
|
+
}
|
|
181
|
+
if (allowed.size === 0)
|
|
182
|
+
return [...INTENSITY_LEVELS];
|
|
183
|
+
return INTENSITY_LEVELS.filter((level) => allowed.has(level));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Pick a usable intensity for a model. The preferred value wins when the model
|
|
187
|
+
* accepts it; otherwise the configured fallback, then High, then the first
|
|
188
|
+
* supported level. This is the one place stale UI state, a saved session or a
|
|
189
|
+
* hand-edited `config.json` is reconciled with the model's limits.
|
|
190
|
+
*/
|
|
191
|
+
export function intensityForModel(model, preferred, fallback) {
|
|
192
|
+
const allowed = modelIntensities(model);
|
|
193
|
+
for (const candidate of [preferred, fallback, "High"]) {
|
|
194
|
+
const level = normalizeIntensity(candidate);
|
|
195
|
+
if (level && allowed.includes(level))
|
|
196
|
+
return level;
|
|
197
|
+
}
|
|
198
|
+
return allowed[0] ?? "High";
|
|
199
|
+
}
|
|
200
|
+
/** Exact model lookup within a provider (names may repeat across providers). */
|
|
201
|
+
export function findConfiguredModel(providerName, modelName) {
|
|
202
|
+
return loadModels().providers.find((provider) => provider.name === providerName)
|
|
203
|
+
?.models.find((model) => model.name === modelName) ?? null;
|
|
204
|
+
}
|
|
106
205
|
export function normalizeApprovalMode(value) {
|
|
107
206
|
// Config files are hand-edited: "Auto" or " autO " must not silently
|
|
108
207
|
// downgrade the conversation to a prompt on every tool call.
|
|
@@ -110,9 +209,11 @@ export function normalizeApprovalMode(value) {
|
|
|
110
209
|
? "auto"
|
|
111
210
|
: "request";
|
|
112
211
|
}
|
|
113
|
-
export function ensureDirs() {
|
|
212
|
+
export function ensureDirs(mode = "standard") {
|
|
114
213
|
fs.mkdirSync(NEUTRINO_DIR, { recursive: true });
|
|
115
|
-
fs.mkdirSync(
|
|
214
|
+
fs.mkdirSync(sessionDir(mode), { recursive: true });
|
|
215
|
+
if (mode === "pro")
|
|
216
|
+
fs.mkdirSync(PRO_DIR, { recursive: true });
|
|
116
217
|
}
|
|
117
218
|
export function loadModels() {
|
|
118
219
|
try {
|
|
@@ -132,7 +233,26 @@ export function saveModels(data) {
|
|
|
132
233
|
ensureDirs();
|
|
133
234
|
fs.writeFileSync(MODEL_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
134
235
|
}
|
|
135
|
-
|
|
236
|
+
/** Read a config file without creating it; null when missing or unreadable. */
|
|
237
|
+
function readConfigFile(file) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export function loadConfig(mode = "standard") {
|
|
246
|
+
if (mode === "pro") {
|
|
247
|
+
// Pro keeps its own file, with credentials/approval policy layered on top
|
|
248
|
+
// from the main config so those stay shared by construction.
|
|
249
|
+
const own = readConfigFile(PRO_CONFIG_FILE) ?? {};
|
|
250
|
+
const shared = readConfigFile(CONFIG_FILE) ?? DEFAULT_CONFIG;
|
|
251
|
+
const merged = { ...DEFAULT_CONFIG, ...own };
|
|
252
|
+
for (const key of SHARED_CONFIG_KEYS)
|
|
253
|
+
merged[key] = shared[key] ?? DEFAULT_CONFIG[key];
|
|
254
|
+
return merged;
|
|
255
|
+
}
|
|
136
256
|
try {
|
|
137
257
|
ensureDirs();
|
|
138
258
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
@@ -147,8 +267,30 @@ export function loadConfig() {
|
|
|
147
267
|
return { ...DEFAULT_CONFIG };
|
|
148
268
|
}
|
|
149
269
|
}
|
|
150
|
-
export function saveConfig(data) {
|
|
151
|
-
ensureDirs();
|
|
270
|
+
export function saveConfig(data, mode = "standard") {
|
|
271
|
+
ensureDirs(mode);
|
|
272
|
+
if (mode === "pro") {
|
|
273
|
+
fs.mkdirSync(PRO_DIR, { recursive: true });
|
|
274
|
+
const own = { ...data };
|
|
275
|
+
for (const key of SHARED_CONFIG_KEYS)
|
|
276
|
+
delete own[key];
|
|
277
|
+
fs.writeFileSync(PRO_CONFIG_FILE, JSON.stringify(own, null, 2), "utf-8");
|
|
278
|
+
// A Pro settings save that touched a shared key (password, approval policy)
|
|
279
|
+
// writes it back to the main config, where the terminal and both pages read it.
|
|
280
|
+
const shared = readConfigFile(CONFIG_FILE) ?? { ...DEFAULT_CONFIG };
|
|
281
|
+
let changed = false;
|
|
282
|
+
for (const key of SHARED_CONFIG_KEYS) {
|
|
283
|
+
if (data[key] === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
if (JSON.stringify(data[key]) === JSON.stringify(shared[key]))
|
|
286
|
+
continue;
|
|
287
|
+
shared[key] = data[key];
|
|
288
|
+
changed = true;
|
|
289
|
+
}
|
|
290
|
+
if (changed)
|
|
291
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(shared, null, 2), "utf-8");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
152
294
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
153
295
|
}
|
|
154
296
|
/** Look a model up by name across every configured provider. */
|
|
@@ -179,7 +321,8 @@ export function resolveModelSelection(value, requireKey = true) {
|
|
|
179
321
|
if (typeof input.provider !== "string" || typeof input.model !== "string" ||
|
|
180
322
|
typeof input.intensity !== "string")
|
|
181
323
|
throw new Error("provider, model and intensity are required");
|
|
182
|
-
|
|
324
|
+
const intensity = normalizeIntensity(input.intensity);
|
|
325
|
+
if (!intensity)
|
|
183
326
|
throw new Error("invalid thinking intensity");
|
|
184
327
|
const provider = loadModels().providers.find((p) => p.name === input.provider);
|
|
185
328
|
if (!provider)
|
|
@@ -187,15 +330,17 @@ export function resolveModelSelection(value, requireKey = true) {
|
|
|
187
330
|
const model = provider.models.find((m) => m.name === input.model);
|
|
188
331
|
if (!model)
|
|
189
332
|
throw new Error("unknown model for this provider");
|
|
333
|
+
if (!modelIntensities(model).includes(intensity))
|
|
334
|
+
throw new Error("this model does not support that thinking intensity");
|
|
190
335
|
if (requireKey && !model.api_key?.trim())
|
|
191
336
|
throw new Error("this model has no API key configured");
|
|
192
|
-
return { provider: provider.name, model: { ...model }, intensity
|
|
337
|
+
return { provider: provider.name, model: { ...model }, intensity };
|
|
193
338
|
}
|
|
194
|
-
export function saveSession(messages, modelName, provider, intensity, cwd, sessionId, tokens, context, approvalMode, plan) {
|
|
195
|
-
ensureDirs();
|
|
339
|
+
export function saveSession(messages, modelName, provider, intensity, cwd, sessionId, tokens, context, approvalMode, plan, mode = "standard", proPreset) {
|
|
340
|
+
ensureDirs(mode);
|
|
196
341
|
const now = new Date();
|
|
197
342
|
const id = sessionId ?? `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
198
|
-
const existing = sessionId ? loadSession(sessionId) : null;
|
|
343
|
+
const existing = sessionId ? loadSession(sessionId, mode) : null;
|
|
199
344
|
const created_at = existing ? existing.created_at : now.toISOString();
|
|
200
345
|
let title = "";
|
|
201
346
|
for (const msg of messages) {
|
|
@@ -216,6 +361,8 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
216
361
|
messages,
|
|
217
362
|
tokens,
|
|
218
363
|
approval_mode: normalizeApprovalMode(approvalMode ?? existing?.approval_mode),
|
|
364
|
+
...(mode === "pro" && (proPreset ?? existing?.pro_preset)
|
|
365
|
+
? { pro_preset: proPreset ?? existing?.pro_preset } : {}),
|
|
219
366
|
};
|
|
220
367
|
const validContext = normalizeContext(context, messages.filter((m) => m.role !== "system").length);
|
|
221
368
|
if (validContext)
|
|
@@ -224,7 +371,7 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
224
371
|
// how far it got, and a stopped plan is still the record of what was agreed.
|
|
225
372
|
if (plan && plan.steps.length)
|
|
226
373
|
session.plan = plan;
|
|
227
|
-
const filePath = path.join(
|
|
374
|
+
const filePath = path.join(sessionDir(mode), `${id}.json`);
|
|
228
375
|
// Keep the last complete transcript intact if writing or replacing fails.
|
|
229
376
|
// The temporary file shares the destination directory so rename is atomic.
|
|
230
377
|
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -252,8 +399,8 @@ export function saveSession(messages, modelName, provider, intensity, cwd, sessi
|
|
|
252
399
|
}
|
|
253
400
|
return id;
|
|
254
401
|
}
|
|
255
|
-
export function loadSession(sessionId) {
|
|
256
|
-
const filePath = path.join(
|
|
402
|
+
export function loadSession(sessionId, mode = "standard") {
|
|
403
|
+
const filePath = path.join(sessionDir(mode), `${sessionId}.json`);
|
|
257
404
|
if (!fs.existsSync(filePath))
|
|
258
405
|
return null;
|
|
259
406
|
try {
|
|
@@ -263,27 +410,28 @@ export function loadSession(sessionId) {
|
|
|
263
410
|
return null;
|
|
264
411
|
}
|
|
265
412
|
}
|
|
266
|
-
export function deleteSession(sessionId) {
|
|
267
|
-
const filePath = path.join(
|
|
413
|
+
export function deleteSession(sessionId, mode = "standard") {
|
|
414
|
+
const filePath = path.join(sessionDir(mode), `${sessionId}.json`);
|
|
268
415
|
if (fs.existsSync(filePath)) {
|
|
269
416
|
fs.unlinkSync(filePath);
|
|
270
417
|
return true;
|
|
271
418
|
}
|
|
272
419
|
return false;
|
|
273
420
|
}
|
|
274
|
-
export function listSessions() {
|
|
275
|
-
ensureDirs();
|
|
421
|
+
export function listSessions(mode = "standard") {
|
|
422
|
+
ensureDirs(mode);
|
|
276
423
|
const sessions = [];
|
|
424
|
+
const dir = sessionDir(mode);
|
|
277
425
|
let files = [];
|
|
278
426
|
try {
|
|
279
|
-
files = fs.readdirSync(
|
|
427
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
280
428
|
}
|
|
281
429
|
catch {
|
|
282
430
|
return sessions;
|
|
283
431
|
}
|
|
284
432
|
for (const f of files) {
|
|
285
433
|
try {
|
|
286
|
-
const data = JSON.parse(fs.readFileSync(path.join(
|
|
434
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
|
|
287
435
|
sessions.push({
|
|
288
436
|
id: data.id ?? f.replace(/\.json$/, ""),
|
|
289
437
|
created_at: data.created_at ?? "",
|
|
@@ -308,8 +456,8 @@ export function listSessions() {
|
|
|
308
456
|
* The history menu is scoped to the current project so sessions started in
|
|
309
457
|
* other directories do not clutter it.
|
|
310
458
|
*/
|
|
311
|
-
export function listSessionsForCwd(cwd = process.cwd()) {
|
|
312
|
-
return listSessions().filter((s) => sameCwd(s.cwd, cwd));
|
|
459
|
+
export function listSessionsForCwd(cwd = process.cwd(), mode = "standard") {
|
|
460
|
+
return listSessions(mode).filter((s) => sameCwd(s.cwd, cwd));
|
|
313
461
|
}
|
|
314
462
|
function readAsset(...candidates) {
|
|
315
463
|
for (const candidate of candidates) {
|
|
@@ -360,13 +508,13 @@ export function loadSubAgentPrompt() {
|
|
|
360
508
|
* `NEUTRINO_PLAN=off` overrides both ways for a single run, the way the other
|
|
361
509
|
* environment variables override the saved configuration.
|
|
362
510
|
*/
|
|
363
|
-
export function planFirstEnabled() {
|
|
511
|
+
export function planFirstEnabled(mode = "standard") {
|
|
364
512
|
const override = String(process.env.NEUTRINO_PLAN ?? "").trim().toLowerCase();
|
|
365
513
|
if (override === "off" || override === "0" || override === "false" || override === "no")
|
|
366
514
|
return false;
|
|
367
515
|
if (override === "on" || override === "1" || override === "true" || override === "yes")
|
|
368
516
|
return true;
|
|
369
|
-
return loadConfig().plan_first !== false;
|
|
517
|
+
return loadConfig(mode).plan_first !== false;
|
|
370
518
|
}
|
|
371
519
|
let cachedVersion = null;
|
|
372
520
|
/**
|
package/dist/header.js
CHANGED
|
@@ -26,7 +26,8 @@ export function drawHeader(buf, state) {
|
|
|
26
26
|
if (state.webUrl) {
|
|
27
27
|
// A short reminder that the browser console is live; the startup message
|
|
28
28
|
// prints the full addresses when more than one works.
|
|
29
|
-
|
|
29
|
+
const label = state.webBackground ? "web (background)" : "web";
|
|
30
|
+
text += ` · ${label} ${state.webUrl.replace(/^https?:\/\//, "").replace(/\/$/, "")}`;
|
|
30
31
|
}
|
|
31
32
|
text = truncateByWidth(text, w);
|
|
32
33
|
buf.centerText(0, text, { fg: C.accent, bg: C.highlightBg, bold: true });
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { App } from "./app.js";
|
|
2
2
|
export { defaultState } from "./state.js";
|
|
3
|
-
export { INTENSITY_LEVELS, INTENSITY_PARAMS, DEFAULT_MODELS, loadModels, saveModels, loadConfig, saveConfig, listSessions, loadSession, deleteSession, saveSession, } from "./config.js";
|
|
3
|
+
export { INTENSITY_LEVELS, INTENSITY_PARAMS, DEFAULT_MODELS, MODES, normalizeMode, normalizeIntensity, modelIntensities, intensityForModel, findConfiguredModel, loadModels, saveModels, loadConfig, saveConfig, listSessions, loadSession, deleteSession, saveSession, } from "./config.js";
|
|
4
4
|
export { LLMClient } from "./llm/client.js";
|
|
5
5
|
export { executeTool, getToolDefinitions } from "./llm/tools.js";
|
package/dist/llm/approval.js
CHANGED
|
@@ -511,9 +511,13 @@ export class ApprovalGate {
|
|
|
511
511
|
// Resolved inside the try: an unresolvable reviewer model is a failed
|
|
512
512
|
// vote (and therefore a denial), not an exception that aborts the call.
|
|
513
513
|
const { model, provider, intensity } = this.options.reviewer();
|
|
514
|
-
const client = new LLMClient(model, provider, intensity);
|
|
514
|
+
const client = this.options.createReviewerClient?.() ?? new LLMClient(model, provider, intensity);
|
|
515
515
|
const timeout = this.options.reviewTimeoutMs?.() ?? REVIEW_TIMEOUT_MS;
|
|
516
|
-
const
|
|
516
|
+
const rolePrompt = this.options.reviewerSystemPrompt?.();
|
|
517
|
+
const system = rolePrompt ? `${rolePrompt}\n\n${REVIEW_SYSTEM_PROMPT}` : REVIEW_SYSTEM_PROMPT;
|
|
518
|
+
const controller = new AbortController();
|
|
519
|
+
const raw = await withTimeout(client.complete(system, prompt, { signal: controller.signal, cancelled: this.options.cancelled }), timeout)
|
|
520
|
+
.finally(() => controller.abort());
|
|
517
521
|
if (raw === null) {
|
|
518
522
|
// Fail closed: a reviewer that never answers must not stall the turn.
|
|
519
523
|
return {
|