@xingjisoft/neutrino 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +19 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/diagnostics.d.ts +91 -0
  43. package/dist/llm/diagnostics.js +242 -0
  44. package/dist/llm/plan.d.ts +148 -0
  45. package/dist/llm/retry.d.ts +80 -0
  46. package/dist/llm/risk.d.ts +21 -0
  47. package/dist/llm/risk.js +7 -0
  48. package/dist/llm/runtime.d.ts +189 -0
  49. package/dist/llm/runtime.js +77 -4
  50. package/dist/llm/search.d.ts +2 -0
  51. package/dist/llm/searchNetwork.d.ts +8 -0
  52. package/dist/llm/subagent.d.ts +112 -0
  53. package/dist/llm/subagent.js +4 -2
  54. package/dist/llm/tools.d.ts +20 -0
  55. package/dist/llm/tools.js +4 -1
  56. package/dist/llm/workspace.d.ts +6 -0
  57. package/dist/llm/workspace.js +271 -0
  58. package/dist/markdown.d.ts +58 -0
  59. package/dist/migration.d.ts +8 -0
  60. package/dist/persistence.d.ts +14 -0
  61. package/dist/plugins/catalog.d.ts +20 -0
  62. package/dist/plugins/catalog.js +45 -0
  63. package/dist/plugins/cli.d.ts +1 -0
  64. package/dist/plugins/cli.js +106 -0
  65. package/dist/plugins/host.d.ts +121 -0
  66. package/dist/plugins/host.js +450 -0
  67. package/dist/plugins/manager.d.ts +24 -0
  68. package/dist/plugins/manager.js +288 -0
  69. package/dist/plugins/package.d.ts +5 -0
  70. package/dist/plugins/package.js +53 -0
  71. package/dist/plugins/runner.d.ts +1 -0
  72. package/dist/plugins/runner.js +227 -0
  73. package/dist/plugins/session.d.ts +12 -0
  74. package/dist/plugins/session.js +97 -0
  75. package/dist/plugins/terminal.d.ts +22 -0
  76. package/dist/plugins/terminal.js +33 -0
  77. package/dist/plugins/types.d.ts +178 -0
  78. package/dist/plugins/types.js +6 -0
  79. package/dist/plugins/validation.d.ts +14 -0
  80. package/dist/plugins/validation.js +174 -0
  81. package/dist/plugins/web-sdk.d.ts +58 -0
  82. package/dist/plugins/web-sdk.js +1 -0
  83. package/dist/pro/agents.d.ts +34 -0
  84. package/dist/pro/config.d.ts +75 -0
  85. package/dist/pro/config.js +3 -2
  86. package/dist/pro/preflight.d.ts +21 -0
  87. package/dist/sanitize.d.ts +34 -0
  88. package/dist/screens/agentConversation.d.ts +28 -0
  89. package/dist/screens/agentHistory.d.ts +25 -0
  90. package/dist/screens/agentSelect.d.ts +16 -0
  91. package/dist/screens/chat.d.ts +292 -0
  92. package/dist/screens/chat.js +139 -21
  93. package/dist/screens/history.d.ts +23 -0
  94. package/dist/screens/modelSelect.d.ts +29 -0
  95. package/dist/screens/plugins.d.ts +29 -0
  96. package/dist/screens/plugins.js +196 -0
  97. package/dist/screens/settings.d.ts +42 -0
  98. package/dist/screens/start.d.ts +43 -0
  99. package/dist/screens/start.js +25 -17
  100. package/dist/screens/updatePrompt.d.ts +41 -0
  101. package/dist/selection.d.ts +75 -0
  102. package/dist/state.d.ts +26 -0
  103. package/dist/terminal/buffer.d.ts +47 -0
  104. package/dist/terminal/input.d.ts +70 -0
  105. package/dist/terminal/renderer.d.ts +43 -0
  106. package/dist/terminal/restore.d.ts +41 -0
  107. package/dist/terminal/screen.d.ts +57 -0
  108. package/dist/tray.d.ts +14 -0
  109. package/dist/ui.d.ts +123 -0
  110. package/dist/update.d.ts +71 -0
  111. package/dist/updateWatch.d.ts +43 -0
  112. package/dist/web/approvalQueue.d.ts +8 -0
  113. package/dist/web/approvalQueue.js +59 -0
  114. package/dist/web/auth.d.ts +14 -0
  115. package/dist/web/clientState.d.ts +16 -0
  116. package/dist/web/folderPicker.d.ts +34 -0
  117. package/dist/web/folderPicker.js +544 -0
  118. package/dist/web/hub.d.ts +213 -0
  119. package/dist/web/hub.js +14 -2
  120. package/dist/web/localApi.d.ts +11 -0
  121. package/dist/web/localApi.js +260 -0
  122. package/dist/web/plugins.d.ts +8 -0
  123. package/dist/web/plugins.js +64 -0
  124. package/dist/web/server.d.ts +81 -0
  125. package/dist/web/server.js +127 -19
  126. package/dist/web/service.d.ts +7 -0
  127. package/dist/web/service.js +1 -0
  128. package/dist/web/session.d.ts +176 -0
  129. package/dist/web/session.js +153 -19
  130. package/dist/web/settings.d.ts +65 -0
  131. package/dist/web/terminalBridge.d.ts +15 -0
  132. package/dist/web/terminalBridge.js +8 -2
  133. package/dist/width.d.ts +6 -0
  134. package/docs/plugins/README.md +126 -0
  135. package/docs/plugins/agent-sdk.md +92 -0
  136. package/docs/plugins/architecture.md +64 -0
  137. package/docs/plugins/compatibility.md +34 -0
  138. package/docs/plugins/development.md +55 -0
  139. package/docs/plugins/manifest.md +59 -0
  140. package/docs/plugins/manifest.schema.json +286 -0
  141. package/docs/plugins/security.md +53 -0
  142. package/docs/plugins/skills-prompts.md +69 -0
  143. package/docs/plugins/ui-sdk.md +109 -0
  144. package/examples/local-api.py +105 -0
  145. package/examples/plugins/cards/card.html +6 -0
  146. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  147. package/examples/plugins/composer/editor.html +10 -0
  148. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  149. package/examples/plugins/function-tools/agent.js +32 -0
  150. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  151. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  152. package/examples/plugins/menus-tabs/panel.html +6 -0
  153. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  154. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  155. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  156. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  157. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  158. package/package.json +66 -42
  159. package/web/LOCAL-API.md +65 -0
  160. package/web/README.md +27 -6
  161. package/web/app.js +201 -32
  162. package/web/i18n.js +15 -0
  163. package/web/index.html +3 -1
  164. package/web/plugins.css +10 -0
  165. package/web/plugins.js +230 -0
  166. package/web/style.css +11 -2
@@ -1,4 +1,6 @@
1
+ import { runPluginTurn, pluginCommand, pluginApprovalContext } from "../plugins/session.js";
1
2
  import path from "node:path";
3
+ import { ApprovalQueue } from "./approvalQueue.js";
2
4
  /**
3
5
  * Headless conversation runner for the web UI.
4
6
  *
@@ -12,6 +14,8 @@ import path from "node:path";
12
14
  * (snapshot, stream, pause, resume, message) work unchanged.
13
15
  */
14
16
  import { randomUUID } from "node:crypto";
17
+ import { FastAgentEngine } from "../fast/engine.js";
18
+ import { FAST_MODE, assertFastAvailable, fastSettings } from "../fast/types.js";
15
19
  import { findModel, contextCompressionThreshold, modelContextWindow, sessionDir, intensityForModel, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
16
20
  import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
17
21
  import { TaskRuntime, TaskControlError, currentTask, linkSignals, taskLimits, taskBudgetScope, resumeTaskSnapshot } from "../llm/runtime.js";
@@ -34,7 +38,6 @@ const IDLE_DISPOSE_MS = 30 * 60 * 1000;
34
38
  /** Placeholder shown while a tool is still running. */
35
39
  const TOOL_PENDING = "…";
36
40
  /** A sub-agent gets the same tools minus delegation: one level, never a tree. */
37
- const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
38
41
  /** Marker for the model-only summary, separate from the original transcript. */
39
42
  const SUMMARY_PREFIX = "Context was compressed. Summary of the earlier part:";
40
43
  /**
@@ -50,6 +53,8 @@ export function newSessionId(now = new Date()) {
50
53
  * cannot litter the history with empty transcripts.
51
54
  */
52
55
  export function createWebSession(cwd, selection, approvalMode, mode = "standard", proPreset, agentMode = "standard") {
56
+ if (agentMode === FAST_MODE)
57
+ assertFastAvailable(loadConfig(mode));
53
58
  const now = new Date();
54
59
  const source = {
55
60
  id: newSessionId(now),
@@ -170,13 +175,19 @@ export class WebSession {
170
175
  lastTask;
171
176
  taskOutcome = "completed";
172
177
  turnToken = 0;
178
+ /** Set by a resume click: the next turn clears its tool calls without asking. */
179
+ autoApproveNextTurn = false;
180
+ autoApproveToken = null;
173
181
  agentMode = "standard";
174
182
  activeAgentMode = "standard";
183
+ fastState;
184
+ queuedAgentMode = null;
175
185
  turnAbort = null;
176
186
  lastPromptTokens = 0;
177
187
  disposed = false;
178
188
  idleTimer = null;
179
189
  /** Non-null while a tool call waits for the browser to answer. */
190
+ approvalQueue = new ApprovalQueue();
180
191
  pendingApproval = null;
181
192
  /** Reviewer count while an automated review runs, null otherwise. */
182
193
  reviewing = null;
@@ -197,6 +208,11 @@ export class WebSession {
197
208
  this.context = normalizeContext(source.context, this.messages.filter((m) => m.role !== "system").length);
198
209
  this.plan = resumePlan(source.plan ?? null);
199
210
  this.lastTask = resumeTaskSnapshot(source.task_run);
211
+ this.fastState = source.fast_run ? structuredClone(source.fast_run) : undefined;
212
+ if (this.fastState && !["completed", "failed", "cancelled"].includes(this.fastState.phase)) {
213
+ this.fastState.phase = "failed";
214
+ this.fastState.error = "Process restarted; artifacts preserved. Review partial state before resuming; tools are never replayed automatically.";
215
+ }
200
216
  this.tokens = {
201
217
  prompt: source.tokens?.prompt ?? 0,
202
218
  completion: source.tokens?.completion ?? 0,
@@ -216,6 +232,7 @@ export class WebSession {
216
232
  if (this.disposed)
217
233
  return;
218
234
  this.disposed = true;
235
+ this.approvalQueue.cancelWaiting();
219
236
  this.proAbort?.abort();
220
237
  this.turnAbort?.abort();
221
238
  this.turnToken++;
@@ -283,6 +300,7 @@ export class WebSession {
283
300
  approvalMode: this.approvalMode,
284
301
  agentMode: this.agentMode,
285
302
  activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
303
+ ...(this.fastState ? { fast: this.fastState } : {}),
286
304
  task: this.taskRuntime?.snapshot() ?? this.lastTask,
287
305
  busy: this.busy,
288
306
  paused: this.paused,
@@ -295,7 +313,7 @@ export class WebSession {
295
313
  tokens: { prompt: this.tokens.prompt, completion: this.tokens.completion },
296
314
  subagent: this.subagent,
297
315
  approval: this.pendingApproval
298
- ? liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.dir)
316
+ ? { ...liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.dir), id: this.pendingApproval.id }
299
317
  : null,
300
318
  reviewing: this.reviewing,
301
319
  };
@@ -331,6 +349,8 @@ export class WebSession {
331
349
  livePause() {
332
350
  if (this.paused || (!this.busy && !this.queued))
333
351
  return;
352
+ if (this.fastState && this.activeAgentMode === FAST_MODE && this.busy)
353
+ this.fastState.phase = "cancelling";
334
354
  this.paused = true;
335
355
  this.proAbort?.abort();
336
356
  this.turnAbort?.abort();
@@ -358,14 +378,27 @@ export class WebSession {
358
378
  return;
359
379
  this.paused = false;
360
380
  this.publishLive();
361
- this.maybeFlushQueue();
381
+ if (this.busy)
382
+ return;
383
+ // Continue answers the approval the pause threw away: the turn that follows
384
+ // runs its tool calls through the gate instead of asking the human again.
385
+ const restart = !this.queued && lastRequestText(this.messages) !== "";
386
+ if (!this.queued && !restart)
387
+ return;
388
+ this.autoApproveNextTurn = true;
389
+ // A queued message starts the next turn on its own; otherwise the paused turn
390
+ // is restarted from the transcript the pause kept, partial answer included.
391
+ if (this.queued)
392
+ this.maybeFlushQueue();
393
+ else
394
+ this.startTurn();
362
395
  }
363
396
  liveSend(text) {
364
397
  this.send(text);
365
398
  }
366
- liveApprove(approved) {
399
+ liveApprove(approved, approvalId) {
367
400
  const pending = this.pendingApproval;
368
- if (!pending)
401
+ if (!pending || (approvalId !== undefined && pending.id !== approvalId))
369
402
  return false;
370
403
  this.pendingApproval = null;
371
404
  pending.resolve(approved);
@@ -375,6 +408,14 @@ export class WebSession {
375
408
  liveSetAgentMode(mode) {
376
409
  if (this.mode === "pro" || this.disposed)
377
410
  return { ok: false, error: "this session cannot change agent mode" };
411
+ if (mode === FAST_MODE) {
412
+ try {
413
+ assertFastAvailable(loadConfig(this.mode));
414
+ }
415
+ catch (error) {
416
+ return { ok: false, error: describeError(error) };
417
+ }
418
+ }
378
419
  this.agentMode = mode;
379
420
  this.save();
380
421
  this.publishLive();
@@ -410,6 +451,8 @@ export class WebSession {
410
451
  this.turnToken++;
411
452
  this.taskRuntime = null;
412
453
  this.lastTask = undefined;
454
+ this.fastState = undefined;
455
+ this.queuedAgentMode = null;
413
456
  if (this.pendingApproval) {
414
457
  const pending = this.pendingApproval;
415
458
  this.pendingApproval = null;
@@ -443,6 +486,7 @@ export class WebSession {
443
486
  this.save();
444
487
  if (this.busy || this.paused) {
445
488
  this.queued = trimmed;
489
+ this.queuedAgentMode = this.agentMode;
446
490
  this.publishLive();
447
491
  return;
448
492
  }
@@ -472,10 +516,13 @@ export class WebSession {
472
516
  const turnMessages = this.messages.slice();
473
517
  const approvalContext = captureApprovalContext(turnMessages);
474
518
  this.busy = true;
475
- this.activeAgentMode = this.agentMode;
519
+ this.activeAgentMode = this.queuedAgentMode ?? this.agentMode;
520
+ this.queuedAgentMode = null;
476
521
  this.turnAbort?.abort();
477
522
  this.turnAbort = new AbortController();
478
523
  const token = ++this.turnToken;
524
+ this.autoApproveToken = this.autoApproveNextTurn ? token : null;
525
+ this.autoApproveNextTurn = false;
479
526
  this.streamingContent = "";
480
527
  this.publishLive();
481
528
  void (async () => {
@@ -492,6 +539,7 @@ export class WebSession {
492
539
  */
493
540
  async runTurn(token, turnMessages, approvalContext) {
494
541
  const runtime = new TaskRuntime({
542
+ executionMode: this.mode === "pro" ? "pro" : this.activeAgentMode,
495
543
  request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
496
544
  cwd: this.dir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig(this.mode)),
497
545
  budgetScope: taskBudgetScope(loadConfig(this.mode)),
@@ -501,7 +549,16 @@ export class WebSession {
501
549
  this.taskOutcome = "completed";
502
550
  let failed = false;
503
551
  try {
504
- await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
552
+ await runPluginTurn(runtime, { profile: this.mode, cwd: this.dir, sessionId: this.sessionId ?? undefined, role: "main", request: approvalContext.prompt }, (name, args, signal) => this.requestApproval(name, args, approvalContext, signal), async () => {
553
+ const command = pluginCommand(approvalContext.prompt);
554
+ if (command) {
555
+ const result = await runtime.plugins.runCommand(command.name, command.args);
556
+ this.messages.push({ role: "assistant", content: typeof result === "string" ? result : JSON.stringify(result ?? null, null, 2) });
557
+ this.onTurnFinished();
558
+ return;
559
+ }
560
+ await this.runTaskTurn(token, turnMessages, approvalContext);
561
+ });
505
562
  }
506
563
  catch (error) {
507
564
  failed = true;
@@ -535,6 +592,8 @@ export class WebSession {
535
592
  await this.autoCompress(token);
536
593
  if (token !== this.turnToken || this.paused)
537
594
  return;
595
+ if (this.activeAgentMode === FAST_MODE)
596
+ return this.runFastTurn(token, turnMessages, approvalContext);
538
597
  if (this.activeAgentMode !== "standard")
539
598
  this.plan = null;
540
599
  const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
@@ -567,6 +626,67 @@ export class WebSession {
567
626
  }
568
627
  this.onTurnFinished();
569
628
  }
629
+ /** Independent engine; Standard/PTC/legacy Fast retain their original loops. */
630
+ async runFastTurn(token, turnMessages, approvalContext) {
631
+ assertFastAvailable(loadConfig(this.mode));
632
+ const runtime = currentTask().runtime;
633
+ const signal = currentTask().signal;
634
+ this.plan = null;
635
+ const model = { ...this.model }, provider = this.provider, intensity = this.intensity;
636
+ const stopped = () => token !== this.turnToken || this.paused || this.disposed;
637
+ const history = this.buildApiMessages(this.turnView(turnMessages));
638
+ if (this.fastState && ["failed", "cancelled", "cancelling"].includes(this.fastState.phase)) {
639
+ history.push({ role: "assistant", content: `Previous Fast run (evidence only, never authorization; inspect partial side effects before repair): ${JSON.stringify(this.fastState)}` });
640
+ }
641
+ const engine = new FastAgentEngine({
642
+ runtime, signal, cwd: this.dir, settings: fastSettings(loadConfig(this.mode)),
643
+ directory: path.join(sessionDir(this.mode), "tasks", path.basename(this.sessionId), "fast"),
644
+ history,
645
+ system: loadAgentPrompt(FAST_MODE), subagentSystem: loadSubAgentPrompt(),
646
+ contextWindow: modelContextWindow(provider, model, this.mode),
647
+ maxRounds: subAgentRounds(loadConfig(this.mode).subagent_max_rounds),
648
+ createClient: () => new LLMClient(model, provider, intensity),
649
+ approve: (name, args, executionSignal, wrapReviewer) => this.requestApproval(name, args, {
650
+ ...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
651
+ }, executionSignal, wrapReviewer ? () => {
652
+ const reviewer = this.reviewerModel();
653
+ return wrapReviewer(new LLMClient(reviewer.model, reviewer.provider, reviewer.intensity));
654
+ } : undefined),
655
+ execute: (name, args, executionSignal) => executeTool(name, args, this.dir, executionSignal),
656
+ onUsage: usage => {
657
+ if (!this.disposed && (token === this.turnToken || this.fastState?.run_id === engine.runId)) {
658
+ this.tokens.prompt += usage.prompt;
659
+ this.tokens.completion += usage.completion;
660
+ }
661
+ },
662
+ onText: text => { if (!stopped()) {
663
+ this.streamingContent = text;
664
+ this.checkpoint.schedule();
665
+ this.publishLive();
666
+ } },
667
+ onSnapshot: snapshot => {
668
+ if (!this.disposed && (!stopped() || (this.fastState?.run_id === snapshot.run_id && this.fastState.phase === "cancelling"))) {
669
+ this.fastState = snapshot;
670
+ this.checkpoint.schedule();
671
+ this.publishLive();
672
+ }
673
+ },
674
+ onTool: (name, args, result) => {
675
+ if (!stopped()) {
676
+ this.messages.push({ role: "tool", name, arguments: args, result });
677
+ this.save();
678
+ this.publishLive();
679
+ }
680
+ },
681
+ });
682
+ const result = await engine.run();
683
+ if (stopped())
684
+ return;
685
+ this.fastState = result.snapshot;
686
+ this.taskOutcome = result.status;
687
+ this.messages.push({ role: "assistant", content: result.report });
688
+ this.onTurnFinished();
689
+ }
570
690
  /** Freeze one resolved team per task, plan first, then give the whole brief to main. */
571
691
  async runProTurn(token, turnMessages, approvalContext) {
572
692
  const taskSignal = currentTask()?.signal;
@@ -724,6 +844,7 @@ export class WebSession {
724
844
  try {
725
845
  const client = new LLMClient(this.model, this.provider, this.intensity);
726
846
  const answer = await client.complete(loadPlannerPrompt(), buildPlannerInput({ request, cwd: this.dir, transcript: planTranscript(this.messages) }), {
847
+ ...(currentTask()?.runtime.diagnostics ? { diagnostic: { role: "planning" } } : {}),
727
848
  onRetry: (notice) => {
728
849
  this.retrying = notice;
729
850
  this.publishLive();
@@ -991,6 +1112,7 @@ export class WebSession {
991
1112
  this.retrying = null;
992
1113
  this.busy = false;
993
1114
  this.paused = false;
1115
+ this.autoApproveToken = null;
994
1116
  this.streamingContent = null;
995
1117
  this.save();
996
1118
  this.publishLive();
@@ -1001,11 +1123,15 @@ export class WebSession {
1001
1123
  }
1002
1124
  // ---- approval gate ---------------------------------------------------
1003
1125
  /** Classify a tool call and, when it is risky, wait for a decision. */
1004
- async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
1126
+ async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal, createReviewerClient) {
1127
+ context = pluginApprovalContext(context);
1005
1128
  const linked = linkSignals(signal, currentTask()?.signal);
1006
1129
  signal = linked.signal;
1007
1130
  try {
1008
1131
  currentTask()?.runtime.assertTool(tool, args);
1132
+ const plugins = currentTask()?.runtime.plugins;
1133
+ if (plugins)
1134
+ await plugins.guardTool(tool, args);
1009
1135
  }
1010
1136
  catch (error) {
1011
1137
  linked.dispose();
@@ -1019,21 +1145,29 @@ export class WebSession {
1019
1145
  const approvalToken = this.turnToken;
1020
1146
  const team = this.proTeam;
1021
1147
  const gate = new ApprovalGate({
1022
- askHuman: () => new Promise((resolve) => {
1023
- if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true) {
1024
- resolve(false);
1025
- return;
1026
- }
1027
- this.pendingApproval = { tool, args, risk, resolve };
1028
- this.publishLive();
1029
- }),
1148
+ askHuman: () => {
1149
+ const requestedMode = this.approvalMode;
1150
+ return this.approvalQueue.run(() => new Promise((resolve) => {
1151
+ if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true) {
1152
+ resolve(false);
1153
+ return;
1154
+ }
1155
+ if (requestedMode !== this.approvalMode) {
1156
+ resolve(null);
1157
+ return;
1158
+ }
1159
+ this.pendingApproval = { id: randomUUID(), tool, args, risk, resolve };
1160
+ this.publishLive();
1161
+ }), signal);
1162
+ },
1030
1163
  reviewer: () => this.reviewerModel(),
1164
+ ...(createReviewerClient ? { createReviewerClient } : {}),
1031
1165
  ...(this.mode === "pro" && team ? {
1032
1166
  createReviewerClient: () => team.client("confirm_model"),
1033
1167
  reviewerSystemPrompt: () => loadProPrompt("confirm_model"),
1034
1168
  } : {}),
1035
1169
  cancelled: () => approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true,
1036
- autoApprovalEnabled: () => this.approvalMode === "auto",
1170
+ autoApprovalEnabled: () => this.approvalMode === "auto" || approvalToken === this.autoApproveToken,
1037
1171
  extremeAutoApprovalEnabled: () => loadConfig(this.mode).auto_approve_extreme_risk === true,
1038
1172
  onReview: (info) => {
1039
1173
  if (approvalToken !== this.turnToken || this.paused || this.disposed || signal?.aborted === true)
@@ -1209,7 +1343,7 @@ export class WebSession {
1209
1343
  createClient: () => new LLMClient(this.model, this.provider, this.intensity),
1210
1344
  system: loadSubAgentPrompt(),
1211
1345
  task: buildSubAgentTask({ task, context, cwd: this.dir, rounds: maxRounds }),
1212
- tools: SUB_AGENT_TOOLS,
1346
+ tools: getSubAgentToolDefinitions(),
1213
1347
  maxRounds,
1214
1348
  approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
1215
1349
  execute: (name, callArgs) => name === DELEGATE_TOOL
@@ -1347,7 +1481,7 @@ export class WebSession {
1347
1481
  if (chat.length === 0)
1348
1482
  return true;
1349
1483
  try {
1350
- this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan, this.mode, this.proPreset ?? undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
1484
+ this.sessionId = saveSession(chat, this.model.name, this.provider, this.intensity, this.dir, this.sessionId, this.tokens, this.context, this.approvalMode, this.plan, this.mode, this.proPreset ?? undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask, this.fastState);
1351
1485
  this.saveError = null;
1352
1486
  return true;
1353
1487
  }
@@ -0,0 +1,65 @@
1
+ import { type Mode } from "../config.js";
2
+ /**
3
+ * Public settings use a whitelist: API keys and cached credentials stay on disk.
4
+ *
5
+ * `mode` selects the workspace. The Pro workspace has its own defaults,
6
+ * language, plan and reviewer settings; the login password and the approval
7
+ * policy are layered in from the shared config by loadConfig(), so the two
8
+ * pages cannot drift apart on those.
9
+ */
10
+ export declare function getWebSettings(mode?: Mode): {
11
+ providers: {
12
+ name: string;
13
+ models: {
14
+ name: string;
15
+ configured: boolean;
16
+ context_window: number;
17
+ intensities: string[];
18
+ }[];
19
+ }[];
20
+ intensities: string[];
21
+ languages: {
22
+ value: "en" | "zh";
23
+ label: string;
24
+ }[];
25
+ selection: {
26
+ provider: string;
27
+ model: string;
28
+ intensity: string;
29
+ };
30
+ settings: {
31
+ default_intensity: string;
32
+ model_context_window: number;
33
+ approval_model: string;
34
+ auto_approve_extreme_risk: boolean;
35
+ sound_alerts: boolean;
36
+ language: "en" | "zh";
37
+ plan_first: boolean;
38
+ approval_mode: import("../config.js").ApprovalMode;
39
+ web_console: boolean;
40
+ web_lan: boolean;
41
+ background: boolean;
42
+ };
43
+ pro?: {
44
+ presets: {
45
+ name: string;
46
+ chinese_name: string;
47
+ "model-setting": {
48
+ type: "confirm_model" | "main_model" | "thinking_model" | "front_output_model" | "backend_output_model" | "genimage_model" | "plan_model" | "modeling_model";
49
+ provider: string;
50
+ model: string | null;
51
+ intensity: string;
52
+ context_window: number;
53
+ }[];
54
+ }[];
55
+ preset: string;
56
+ max_recursion_depth: number;
57
+ } | undefined;
58
+ update: {
59
+ current_version: string;
60
+ latest_version: string;
61
+ has_update: boolean;
62
+ };
63
+ };
64
+ /** Validate the complete patch before writing anything, preserving unrelated settings. */
65
+ export declare function updateWebSettings(value: unknown, mode?: Mode): void;
@@ -0,0 +1,15 @@
1
+ import type { TerminalBridgeInfo } from "../background.js";
2
+ /** A private loopback bridge exposes only the terminal's live hub. */
3
+ export declare function startTerminalBridge(): Promise<TerminalBridgeInfo & {
4
+ close(): void;
5
+ }>;
6
+ /** Mirror each attached terminal's live sessions into the Web service. */
7
+ export declare class TerminalBridges {
8
+ private entries;
9
+ private timer;
10
+ constructor();
11
+ add(owner: number, bridge: TerminalBridgeInfo): void;
12
+ remove(owner: number): void;
13
+ close(): void;
14
+ private poll;
15
+ }
@@ -66,10 +66,16 @@ export async function startTerminalBridge() {
66
66
  else if (action === "message") {
67
67
  if (typeof body.text !== "string" || !body.text.trim())
68
68
  throw new Error("empty message");
69
- hub.send(key, body.text);
69
+ if (body.agent_mode !== undefined && !isAgentMode(body.agent_mode))
70
+ throw new Error("invalid agent mode");
71
+ const sent = hub.sendConfigured(key, body.text, body.agent_mode);
72
+ if (!sent.ok) {
73
+ sendJson(res, 409, { error: sent.error });
74
+ return;
75
+ }
70
76
  }
71
77
  else if (action === "approve") {
72
- if (!hub.approve(key, body.approved === true)) {
78
+ if (!hub.approve(key, body.approved === true, typeof body.approval_id === "string" ? body.approval_id : undefined)) {
73
79
  sendJson(res, 409, { error: "no approval is pending" });
74
80
  return;
75
81
  }
@@ -0,0 +1,6 @@
1
+ export declare function wcwidth(cp: number): number;
2
+ export declare function displayWidth(text: string): number;
3
+ export declare function truncateByWidth(text: string, width: number): string;
4
+ export declare function ellipsize(text: string, width: number): string;
5
+ /** Trim the beginning of `text`, keeping the end (useful for long paths). */
6
+ export declare function ellipsizeStart(text: string, width: number): string;
@@ -0,0 +1,126 @@
1
+ # Neutrino 插件指南(API v1)
2
+
3
+ 插件可以同时提供 Agent 工具、用户命令、菜单、选项卡、输入框、消息与工具卡片、主题、skill 和提示词。安装插件不需要修改 Neutrino 源码。Standard 和 Pro 共用安装包,分别保存启用状态、配置和组件选择。
4
+
5
+ ## 快速开始
6
+
7
+ 从仓库根目录运行:
8
+
9
+ ```sh
10
+ npm run build
11
+ node dist/bin.js plugin install ./examples/plugins/function-tools
12
+ node dist/bin.js plugin enable community.text-tools
13
+ node dist/bin.js plugin doctor
14
+ ```
15
+
16
+ 在终端或浏览器的对话中输入以下命令,可直接得到结果,不请求模型:
17
+
18
+ ```text
19
+ /plugin community.text-tools/count {"text":"hello plugin"}
20
+ ```
21
+
22
+ 正常对话中,模型也可以调用此插件的文本统计和文件统计工具。工具使用 `p_<插件 ID 的散列>_<工具名>` 作为模型协议名;描述中保留完整的 `插件ID/工具名`。
23
+
24
+ 安装主题:
25
+
26
+ ```sh
27
+ neutrino plugin install ./examples/plugins/theme
28
+ neutrino plugin enable community.indigo-theme
29
+ ```
30
+
31
+ 安装自定义输入框后,在管理界面的 **组件提供者 → chat.composer** 中选择它。命令行也支持选择:
32
+
33
+ ```sh
34
+ neutrino plugin install ./examples/plugins/composer
35
+ neutrino plugin enable community.writing-composer
36
+ neutrino plugin provider chat.composer community.writing-composer/editor
37
+ ```
38
+
39
+ 恢复默认输入框:
40
+
41
+ ```sh
42
+ neutrino plugin provider chat.composer default
43
+ ```
44
+
45
+ ## 管理界面
46
+
47
+ 浏览器侧边栏的 **插件 / Plugins** 按钮打开管理界面。可安装本机目录或 `.neutrino-plugin` 包、启用/停用插件、编辑由 Schema 生成的配置表单、更新、回滚、卸载,以及选择输入框、消息、计划、工具卡片和人格提示词提供者。
48
+
49
+ “当前模式”修改当前 Standard/Pro 的默认配置;“当前项目”将启用状态和配置保存到该项目的覆盖层。安装、更新、回滚、卸载作用于共享包。组件提供者选择按 Standard/Pro 保存。
50
+
51
+ 终端起始页有 **Plugins** 入口,在其他页面按 `Ctrl+X` 也可打开。终端插件页支持:
52
+
53
+ | 按键 | 操作 |
54
+ | --- | --- |
55
+ | ↑ / ↓、Enter | 选择插件、查看详情 |
56
+ | E | 启用 / 停用 |
57
+ | I | 安装本机目录或插件包 |
58
+ | C | 编辑完整配置 JSON |
59
+ | U / R / D | 更新 / 回滚 / 卸载 |
60
+ | V | 选择该插件提供的替换组件 |
61
+ | M | 查看扩展页面的文本内容 |
62
+ | Tab | 切换 Standard / Pro |
63
+ | Esc | 返回 |
64
+
65
+ 启用原生插件时,终端要求按 Y 明确信任该版本。Web 使用独立的信任复选框。
66
+
67
+ ## 命令行
68
+
69
+ ```text
70
+ neutrino plugin help
71
+ neutrino plugin list [--profile standard|pro] [--workspace /absolute/project]
72
+ neutrino plugin inspect <id>
73
+ neutrino plugin create <new-directory>
74
+ neutrino plugin install <directory-or-bundle>
75
+ neutrino plugin enable <id> [--trust-native]
76
+ neutrino plugin disable <id>
77
+ neutrino plugin config <id> '<完整配置 JSON>'
78
+ neutrino plugin update <id>
79
+ neutrino plugin rollback <id>
80
+ neutrino plugin remove <id>
81
+ neutrino plugin provider <slot> <plugin-id/contribution-id|default>
82
+ neutrino plugin pack <directory> <output.neutrino-plugin>
83
+ neutrino plugin doctor
84
+ ```
85
+
86
+ 配置命令提交完整插件配置,未提交的字段回到 Schema 默认值。参数包含空格或 JSON 时,请使用所在 shell 的引号规则。更新从上次安装的本机来源重新读取,原文件夹更新或替换原插件包后执行即可。
87
+
88
+ ## 示例插件
89
+
90
+ | 目录 | ID | 验证的扩展方式 |
91
+ | --- | --- | --- |
92
+ | `examples/plugins/function-tools` | `community.text-tools` | 受限脚本工具、命令、只读文件访问、事件与存储 |
93
+ | `examples/plugins/menus-tabs` | `community.workbench` | 菜单、工作区选项卡、侧栏和终端回退 |
94
+ | `examples/plugins/composer` | `community.writing-composer` | 输入框替换、草稿版本检查、发送、工具栏 |
95
+ | `examples/plugins/cards` | `community.inspector-cards` | 工具、计划和消息渲染器 |
96
+ | `examples/plugins/theme` | `community.indigo-theme` | 无脚本 Web / TUI 主题 |
97
+ | `examples/plugins/skills-prompts` | `community.review-skills` | 按需 skill、规则片段和可选人格 |
98
+
99
+ 插件默认全部停用。卸载不删除旧会话,也不自动重放过去的工具调用。旧包文件会保留,以供运行中的回合继续使用;用户数据保存在独立的 `data` 目录。
100
+
101
+ ## 故障恢复
102
+
103
+ ```sh
104
+ neutrino plugin disable <id>
105
+ neutrino plugin doctor
106
+ neutrino --safe-mode --no-web
107
+ ```
108
+
109
+ 安全模式跳过所有外部插件。`--no-web` 可确保不连接到之前已启动、未处于安全模式的后台控制台。如需浏览器安全模式,先停止旧后台服务,再运行 `neutrino --safe-mode web`。
110
+
111
+ 运行中的回合固定包版本和配置,更新通常从下一回合生效。增加权限或更新原生代码时,插件会自动停用,需重新启用。Web 外观与组件配置会自动刷新。插件脚本激活失败时,该插件的注册项撤销,其他插件和内置功能继续运行;脚本执行失败会记录为该次调用的错误。
112
+
113
+ ## 文档目录与支持范围
114
+
115
+ - [架构、作用域与生命周期](architecture.md)
116
+ - [清单与配置格式](manifest.md),[编辑器 JSON Schema](manifest.schema.json)
117
+ - [Agent SDK 与事件契约](agent-sdk.md)
118
+ - [Web / TUI 界面 SDK](ui-sdk.md)
119
+ - [Skill 与提示词](skills-prompts.md)
120
+ - [权限、受限执行与原生信任](security.md)
121
+ - [开发、打包、测试与发布](development.md)
122
+ - [兼容性与迁移](compatibility.md)
123
+
124
+ API v1 支持本机目录和可校验的离线插件包。插件市场、直接拉取 npm/Git 依赖、模型供应商注册、任意替换 Agent 主循环,以及 DSH 插件二进制兼容不属于这一版的公开 API。可通过工具、命令、钩子、提示词和界面接口组合工作流。Web 可用 HTML/CSS/JavaScript 完整替换相应视图;TUI 提供菜单、文本页面、主题和文本渲染契约,浏览器组件可声明仅在 Web 生效。
125
+
126
+ 架构参考:[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md)、[可逆注册与 Context](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md)。Neutrino 插件使用独立清单和 SDK。