@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,10 @@
1
+ import { terminalTemplate } from "../plugins/terminal.js";
2
+ import { runPluginTurn, pluginCommand, pluginApprovalContext } from "../plugins/session.js";
1
3
  import path from "node:path";
4
+ import { ApprovalQueue } from "../web/approvalQueue.js";
5
+ import { randomUUID } from "node:crypto";
6
+ import { FastAgentEngine } from "../fast/engine.js";
7
+ import { FAST_MODE, assertFastAvailable, fastSettings } from "../fast/types.js";
2
8
  import { Screen } from "../terminal/screen.js";
3
9
  import { C } from "../colors.js";
4
10
  import { findModel, findConfiguredModel, intensityForModel, contextCompressionThreshold, modelContextWindow, sessionDir, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
@@ -21,9 +27,7 @@ import { COMPACT_KEEP, COMPACT_MAX_CHARS_PER_MESSAGE, planCompaction, transcript
21
27
  import { hub, liveApprovalDetails } from "../web/hub.js";
22
28
  import { SessionCheckpoint } from "../persistence.js";
23
29
  import { writeCrashLog } from "../terminal/restore.js";
24
- const TOOLS = getToolDefinitions();
25
30
  /** A sub-agent gets the same tools minus delegation: one level, never a tree. */
26
- const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
27
31
  /**
28
32
  * A finished turn folds its *work* away — the thinking and the tool calls —
29
33
  * while the full user prompt and final reply always stay on screen. Doing
@@ -84,6 +88,7 @@ export class ChatScreen extends Screen {
84
88
  loadedApprovalMode = null;
85
89
  loadedPlan = null;
86
90
  loadedTask;
91
+ loadedFast;
87
92
  loadedAgentMode = null;
88
93
  approvalMode = "request";
89
94
  modelProvider = "DeepSeek";
@@ -117,6 +122,8 @@ export class ChatScreen extends Screen {
117
122
  turnToken = 0;
118
123
  agentMode = "standard";
119
124
  activeAgentMode = "standard";
125
+ fastState;
126
+ queuedAgentMode = null;
120
127
  turnAbort = null;
121
128
  inputValue = "";
122
129
  cursor = 0;
@@ -161,6 +168,7 @@ export class ChatScreen extends Screen {
161
168
  liveDir = process.cwd();
162
169
  // ---- approval gate ---------------------------------------------------
163
170
  /** Non-null while a tool call is waiting for a human decision. */
171
+ approvalQueue = new ApprovalQueue();
164
172
  pendingApproval = null;
165
173
  reviewing = null;
166
174
  /** Decisions taken by the gate, keyed by the tool message (survives
@@ -215,6 +223,11 @@ export class ChatScreen extends Screen {
215
223
  this.finishedTurns.add(start);
216
224
  this.plan = resumePlan(this.loadedPlan);
217
225
  this.lastTask = resumeTaskSnapshot(this.loadedTask);
226
+ this.fastState = this.loadedFast ? structuredClone(this.loadedFast) : undefined;
227
+ if (this.fastState && !["completed", "failed", "cancelled"].includes(this.fastState.phase)) {
228
+ this.fastState.phase = "failed";
229
+ this.fastState.error = "Process restarted; review saved artifacts before resuming. Tools are never replayed automatically.";
230
+ }
218
231
  this.context = normalizeContext(this.loadedContext, this.messages.filter((m) => m.role !== "system").length);
219
232
  this.liveDir = this.loadedCwd ?? process.cwd();
220
233
  this.checkpoint.register();
@@ -228,6 +241,7 @@ export class ChatScreen extends Screen {
228
241
  }
229
242
  }
230
243
  onDeactivate() {
244
+ this.approvalQueue.cancelWaiting();
231
245
  // Never leave the agent awaiting a decision nobody can give.
232
246
  if (this.pendingApproval)
233
247
  this.settleApproval(false);
@@ -274,6 +288,7 @@ export class ChatScreen extends Screen {
274
288
  approvalMode: this.approvalMode,
275
289
  agentMode: this.agentMode,
276
290
  activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
291
+ ...(this.fastState ? { fast: this.fastState } : {}),
277
292
  task: this.taskRuntime?.snapshot() ?? this.lastTask,
278
293
  busy: this.busy,
279
294
  paused: this.paused,
@@ -283,7 +298,7 @@ export class ChatScreen extends Screen {
283
298
  tokens: { prompt: this.tokens.prompt, completion: this.tokens.completion },
284
299
  subagent: this.subagent,
285
300
  approval: this.pendingApproval
286
- ? liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.liveDir)
301
+ ? { ...liveApprovalDetails(this.pendingApproval.tool, this.pendingApproval.args, this.pendingApproval.risk, this.liveDir), id: this.pendingApproval.id }
287
302
  : null,
288
303
  reviewing: this.reviewing,
289
304
  };
@@ -316,6 +331,8 @@ export class ChatScreen extends Screen {
316
331
  livePause() {
317
332
  if (this.paused || (!this.busy && !this.queued))
318
333
  return;
334
+ if (this.fastState && this.activeAgentMode === FAST_MODE && this.busy)
335
+ this.fastState.phase = "cancelling";
319
336
  this.paused = true;
320
337
  this.subagent = null;
321
338
  this.retryNotice = null;
@@ -339,13 +356,21 @@ export class ChatScreen extends Screen {
339
356
  this.togglePause();
340
357
  }
341
358
  /** Answer the pending call from the web UI. */
342
- liveApprove(approved) {
343
- if (!this.pendingApproval)
359
+ liveApprove(approved, approvalId) {
360
+ if (!this.pendingApproval || (approvalId !== undefined && this.pendingApproval.id !== approvalId))
344
361
  return false;
345
362
  this.settleApproval(approved);
346
363
  return true;
347
364
  }
348
365
  liveSetAgentMode(mode) {
366
+ if (mode === FAST_MODE) {
367
+ try {
368
+ assertFastAvailable(loadConfig());
369
+ }
370
+ catch (error) {
371
+ return { ok: false, error: describeError(error) };
372
+ }
373
+ }
349
374
  this.agentMode = mode;
350
375
  this.save();
351
376
  this.publishLive();
@@ -376,6 +401,8 @@ export class ChatScreen extends Screen {
376
401
  this.turnToken++;
377
402
  this.taskRuntime = null;
378
403
  this.lastTask = undefined;
404
+ this.fastState = undefined;
405
+ this.queuedAgentMode = null;
379
406
  if (this.pendingApproval)
380
407
  this.settleApproval(false);
381
408
  this.busy = false;
@@ -738,6 +765,7 @@ export class ChatScreen extends Screen {
738
765
  try {
739
766
  const client = new LLMClient(this.model, this.modelProvider, this.intensity);
740
767
  const answer = await client.complete(loadPlannerPrompt(), buildPlannerInput({ request, cwd: this.liveDir, transcript: planTranscript(this.messages) }), {
768
+ ...(currentTask()?.runtime.diagnostics ? { diagnostic: { role: "planning" } } : {}),
741
769
  onRetry: (notice) => {
742
770
  this.retryNotice =
743
771
  `Planning — network hiccup, retrying in ${formatDelay(notice.delayMs)} ` +
@@ -791,6 +819,7 @@ export class ChatScreen extends Screen {
791
819
  */
792
820
  async runTurn(token, turnMessages, approvalContext) {
793
821
  const runtime = new TaskRuntime({
822
+ executionMode: this.activeAgentMode,
794
823
  request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
795
824
  cwd: this.liveDir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig("standard")),
796
825
  budgetScope: taskBudgetScope(loadConfig("standard")),
@@ -800,7 +829,16 @@ export class ChatScreen extends Screen {
800
829
  this.taskOutcome = "completed";
801
830
  let failed = false;
802
831
  try {
803
- await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
832
+ await runPluginTurn(runtime, { profile: "standard", cwd: this.liveDir, sessionId: this.sessionId ?? undefined, role: "main", request: approvalContext.prompt }, (name, args, signal) => this.requestApproval(name, args, approvalContext, signal), async () => {
833
+ const command = pluginCommand(approvalContext.prompt);
834
+ if (command) {
835
+ const result = await runtime.plugins.runCommand(command.name, command.args);
836
+ this.messages.push({ role: "assistant", content: typeof result === "string" ? result : JSON.stringify(result ?? null, null, 2) });
837
+ this.onTurnFinished();
838
+ return;
839
+ }
840
+ await this.runTaskTurn(token, turnMessages, approvalContext);
841
+ });
804
842
  }
805
843
  catch (error) {
806
844
  failed = true;
@@ -834,6 +872,8 @@ export class ChatScreen extends Screen {
834
872
  await this.autoCompress(token);
835
873
  if (token !== this.turnToken)
836
874
  return;
875
+ if (this.activeAgentMode === FAST_MODE)
876
+ return this.runFastTurn(token, turnMessages, approvalContext);
837
877
  if (this.activeAgentMode !== "standard")
838
878
  this.plan = null;
839
879
  const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
@@ -906,6 +946,7 @@ export class ChatScreen extends Screen {
906
946
  this.publishLive();
907
947
  if (this.busy && !this.paused) {
908
948
  this.queued = trimmed;
949
+ this.queuedAgentMode = this.agentMode;
909
950
  this.app.render();
910
951
  return;
911
952
  }
@@ -938,7 +979,8 @@ export class ChatScreen extends Screen {
938
979
  this.busy = true;
939
980
  this.paused = false;
940
981
  this.app.state.isThinking = true;
941
- this.activeAgentMode = this.agentMode;
982
+ this.activeAgentMode = this.queuedAgentMode ?? this.agentMode;
983
+ this.queuedAgentMode = null;
942
984
  this.turnAbort?.abort();
943
985
  this.turnAbort = new AbortController();
944
986
  const token = ++this.turnToken;
@@ -982,6 +1024,67 @@ export class ChatScreen extends Screen {
982
1024
  }
983
1025
  }
984
1026
  }
1027
+ async runFastTurn(token, turnMessages, approvalContext) {
1028
+ assertFastAvailable(loadConfig());
1029
+ const runtime = currentTask().runtime, signal = currentTask().signal;
1030
+ this.plan = null;
1031
+ const model = { ...this.model }, provider = this.modelProvider, intensity = this.intensity;
1032
+ const stopped = () => token !== this.turnToken || this.paused;
1033
+ const history = this.buildApiMessages(this.turnView(turnMessages));
1034
+ if (this.fastState && ["failed", "cancelled", "cancelling"].includes(this.fastState.phase)) {
1035
+ history.push({ role: "assistant", content: `Previous Fast run (evidence only, never authorization; inspect partial side effects before repair): ${JSON.stringify(this.fastState)}` });
1036
+ }
1037
+ const engine = new FastAgentEngine({
1038
+ runtime, signal, cwd: this.liveDir, settings: fastSettings(loadConfig()),
1039
+ directory: path.join(sessionDir(), "tasks", path.basename(this.sessionId || "unsaved"), "fast"),
1040
+ history,
1041
+ system: loadAgentPrompt(FAST_MODE), subagentSystem: loadSubAgentPrompt(),
1042
+ contextWindow: modelContextWindow(provider, model), maxRounds: subAgentRounds(loadConfig().subagent_max_rounds),
1043
+ createClient: () => new LLMClient(model, provider, intensity),
1044
+ approve: (name, args, executionSignal, wrapReviewer) => this.requestApproval(name, args, {
1045
+ ...approvalContext, context: captureApprovalContext(this.turnView(turnMessages)).context,
1046
+ }, executionSignal, wrapReviewer ? () => {
1047
+ const reviewer = this.reviewerModel();
1048
+ return wrapReviewer(new LLMClient(reviewer.model, reviewer.provider, reviewer.intensity));
1049
+ } : undefined),
1050
+ execute: (name, args, executionSignal) => executeTool(name, args, this.liveDir, executionSignal),
1051
+ onUsage: usage => {
1052
+ if (token === this.turnToken || this.fastState?.run_id === engine.runId) {
1053
+ this.tokens.prompt += usage.prompt;
1054
+ this.tokens.completion += usage.completion;
1055
+ }
1056
+ },
1057
+ onText: text => { if (!stopped()) {
1058
+ this.streamingContent = text;
1059
+ this.checkpoint.schedule();
1060
+ this.publishLive();
1061
+ this.app.render();
1062
+ } },
1063
+ onSnapshot: snapshot => {
1064
+ if (!stopped() || (this.fastState?.run_id === snapshot.run_id && this.fastState.phase === "cancelling")) {
1065
+ this.fastState = snapshot;
1066
+ this.checkpoint.schedule();
1067
+ this.publishLive();
1068
+ this.app.render();
1069
+ }
1070
+ },
1071
+ onTool: (name, args, result) => {
1072
+ if (!stopped()) {
1073
+ this.messages.push({ role: "tool", name, arguments: args, result });
1074
+ this.save();
1075
+ this.publishLive();
1076
+ }
1077
+ },
1078
+ });
1079
+ const result = await engine.run();
1080
+ if (stopped())
1081
+ return;
1082
+ this.fastState = result.snapshot;
1083
+ this.taskOutcome = result.status;
1084
+ this.messages.push({ role: "assistant", content: result.report });
1085
+ this.streamingContent = null;
1086
+ this.onTurnFinished();
1087
+ }
985
1088
  buildApiMessages(messages = this.messages, directive = "") {
986
1089
  // A step directive extends the system prompt rather than adding a user
987
1090
  // message: the plan is an instruction, not something the user just said.
@@ -1010,7 +1113,7 @@ export class ChatScreen extends Screen {
1010
1113
  */
1011
1114
  async agentLoop(apiMessages, token, approvalContext) {
1012
1115
  const tools = this.activeAgentMode === "ptc"
1013
- ? [...TOOLS, (await import("../llm/codemode.js")).getCodeToolDefinition()] : TOOLS;
1116
+ ? [...getToolDefinitions(), (await import("../llm/codemode.js")).getCodeToolDefinition()] : getToolDefinitions();
1014
1117
  let emptyReplies = 0;
1015
1118
  try {
1016
1119
  while (true) {
@@ -1235,11 +1338,15 @@ export class ChatScreen extends Screen {
1235
1338
  }
1236
1339
  // ---- approval gate ---------------------------------------------------
1237
1340
  /** Classify a tool call and, when it is risky, run it past the gate. */
1238
- async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
1341
+ async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal, createReviewerClient) {
1342
+ context = pluginApprovalContext(context);
1239
1343
  const linked = linkSignals(signal, currentTask()?.signal);
1240
1344
  signal = linked.signal;
1241
1345
  try {
1242
1346
  currentTask()?.runtime.assertTool(tool, args);
1347
+ const plugins = currentTask()?.runtime.plugins;
1348
+ if (plugins)
1349
+ await plugins.guardTool(tool, args);
1243
1350
  }
1244
1351
  catch (error) {
1245
1352
  linked.dispose();
@@ -1254,8 +1361,9 @@ export class ChatScreen extends Screen {
1254
1361
  const cancelled = () => approvalToken !== this.turnToken || this.paused || signal?.aborted === true;
1255
1362
  const gate = new ApprovalGate({
1256
1363
  cancelled,
1257
- askHuman: () => cancelled() ? Promise.resolve(false) : this.askHuman(tool, args, risk),
1364
+ askHuman: () => this.askHuman(tool, args, risk, cancelled, signal),
1258
1365
  reviewer: () => this.reviewerModel(),
1366
+ ...(createReviewerClient ? { createReviewerClient } : {}),
1259
1367
  autoApprovalEnabled: () => this.approvalMode === "auto",
1260
1368
  extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
1261
1369
  // Reviews happen before any card is shown, so without this the app looks
@@ -1293,9 +1401,18 @@ export class ChatScreen extends Screen {
1293
1401
  }
1294
1402
  }
1295
1403
  /** Park the turn until the user answers; the header turns orange meanwhile. */
1296
- askHuman(tool, args, risk) {
1297
- return new Promise((resolve) => {
1298
- this.pendingApproval = { tool, args, risk, resolve };
1404
+ askHuman(tool, args, risk, cancelled, signal) {
1405
+ const requestedMode = this.approvalMode;
1406
+ return this.approvalQueue.run(() => new Promise((resolve) => {
1407
+ if (cancelled()) {
1408
+ resolve(false);
1409
+ return;
1410
+ }
1411
+ if (requestedMode !== this.approvalMode) {
1412
+ resolve(null);
1413
+ return;
1414
+ }
1415
+ this.pendingApproval = { id: randomUUID(), tool, args, risk, resolve };
1299
1416
  this.app.state.approval = { tool, level: risk.level };
1300
1417
  // The card floats over the same corner the approval card needs on a short
1301
1418
  // terminal, and a decision the user cannot see is not a decision. The plan
@@ -1304,7 +1421,7 @@ export class ChatScreen extends Screen {
1304
1421
  this.app.bell?.("approval");
1305
1422
  this.app.render();
1306
1423
  this.publishLive();
1307
- });
1424
+ }), signal);
1308
1425
  }
1309
1426
  /** Answer the pending request (keyboard, mouse, or the web UI). */
1310
1427
  settleApproval(approved) {
@@ -1498,7 +1615,7 @@ export class ChatScreen extends Screen {
1498
1615
  createClient: () => new LLMClient(this.model, this.modelProvider, this.intensity),
1499
1616
  system: loadSubAgentPrompt(),
1500
1617
  task: buildSubAgentTask({ task, context, cwd: this.liveDir, rounds: maxRounds }),
1501
- tools: SUB_AGENT_TOOLS,
1618
+ tools: getSubAgentToolDefinitions(),
1502
1619
  maxRounds,
1503
1620
  approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
1504
1621
  execute: (name, callArgs) => name === DELEGATE_TOOL
@@ -1791,7 +1908,7 @@ export class ChatScreen extends Screen {
1791
1908
  if (chatMessages.length === 0)
1792
1909
  return true;
1793
1910
  try {
1794
- this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan, "standard", undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
1911
+ this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan, "standard", undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask, this.fastState);
1795
1912
  this.saveError = null;
1796
1913
  return true;
1797
1914
  }
@@ -1850,7 +1967,7 @@ export class ChatScreen extends Screen {
1850
1967
  }
1851
1968
  else if (m.role === "user") {
1852
1969
  add({ spans: [{ text: "You:", style: { fg: C.accent, bold: true } }] });
1853
- pushMarkdown(sanitizeText(m.content), { fg: C.user });
1970
+ pushMarkdown(sanitizeText(terminalTemplate("chat.message.renderer", { content: m.content, role: "user" }) ?? m.content), { fg: C.user });
1854
1971
  add({ spans: [] });
1855
1972
  // The fold control follows the complete prompt, which stays visible
1856
1973
  // even when the turn or the earlier conversation is collapsed.
@@ -1864,7 +1981,7 @@ export class ChatScreen extends Screen {
1864
1981
  }
1865
1982
  else if (m.role === "assistant") {
1866
1983
  add({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
1867
- pushMarkdown(sanitizeText(m.content), { fg: C.assistant });
1984
+ pushMarkdown(sanitizeText(terminalTemplate("chat.message.renderer", { content: m.content, role: "assistant" }) ?? m.content), { fg: C.assistant });
1868
1985
  add({ spans: [] });
1869
1986
  }
1870
1987
  else if (m.role === "summary") {
@@ -1902,7 +2019,7 @@ export class ChatScreen extends Screen {
1902
2019
  }
1903
2020
  // Command output routinely carries ANSI colours and tabs; clean it once
1904
2021
  // so the collapsed line count and the expanded body agree.
1905
- const body = sanitizeText(m.result || "");
2022
+ const body = sanitizeText(terminalTemplate("chat.card.tool", { name: m.name, result: m.result, role: "tool", arguments: JSON.stringify(m.arguments) }) ?? m.result);
1906
2023
  if (this.expandedTools.has(mi)) {
1907
2024
  pushText(body || "(no output)", { fg: C.dim }, mi);
1908
2025
  add({ spans: [{ text: " ▾ click to collapse", style: { fg: C.toolHeader } }] }, mi);
@@ -2024,7 +2141,7 @@ export class ChatScreen extends Screen {
2024
2141
  active: true,
2025
2142
  dimmed: this.busy,
2026
2143
  cursorOn: this.app.state.cursorOn,
2027
- placeholder: "Type a message...",
2144
+ placeholder: sanitizeText(terminalTemplate("chat.composer", { text: this.inputValue }) ?? "Type a message...", { singleLine: true }),
2028
2145
  });
2029
2146
  const hintY = h - 1;
2030
2147
  const hints = "Enter send · Ctrl+P plan · Ctrl+C pause · ESC ESC pause · Ctrl+A approval · Ctrl+B menu · Ctrl+L clear · drag to copy";
@@ -2086,7 +2203,8 @@ export class ChatScreen extends Screen {
2086
2203
  { kind: "line", text: `Total: ${total}` },
2087
2204
  ];
2088
2205
  }
2089
- const rows = planCardRows(plan, { width, maxRows: Math.max(1, maxRows - 1) });
2206
+ const custom = terminalTemplate("chat.card.plan", { plan: JSON.stringify(plan), role: "main" });
2207
+ const rows = custom ? wrapText(sanitizeText(custom), width).slice(0, Math.max(1, maxRows - 1)).map(text => ({ kind: "line", text })) : planCardRows(plan, { width, maxRows: Math.max(1, maxRows - 1) });
2090
2208
  rows.push({ kind: "line", text: `Tokens ${this.tokens.prompt} → ${this.tokens.completion}` });
2091
2209
  return rows;
2092
2210
  }
@@ -0,0 +1,23 @@
1
+ import type { Buffer } from "../terminal/buffer.js";
2
+ import { Screen } from "../terminal/screen.js";
3
+ import type { Key, MouseEvent } from "../terminal/input.js";
4
+ export declare class HistoryScreen extends Screen {
5
+ private sessions;
6
+ private selectedIdx;
7
+ private scrollTop;
8
+ private lastHoverX;
9
+ private lastHoverY;
10
+ private rows;
11
+ private itemBounds;
12
+ private cwd;
13
+ onActivate(): void;
14
+ private move;
15
+ onKey(key: Key): void;
16
+ onMouse(ev: MouseEvent): void;
17
+ /** Map a terminal row to the history item index (0 = "Open other Agent's history"). */
18
+ private itemIndexAtRow;
19
+ private loadSelected;
20
+ private deleteSelected;
21
+ private buildRows;
22
+ render(buf: Buffer): void;
23
+ }
@@ -0,0 +1,29 @@
1
+ import type { Buffer } from "../terminal/buffer.js";
2
+ import { Screen } from "../terminal/screen.js";
3
+ import type { Key, MouseEvent } from "../terminal/input.js";
4
+ export declare class ModelSelectScreen extends Screen {
5
+ private level;
6
+ private providers;
7
+ private modelMap;
8
+ private selectedProviderIdx;
9
+ private selectedModelIdx;
10
+ private selectedIntensityIdx;
11
+ private scrollBtn;
12
+ private lastHoverX;
13
+ private lastHoverY;
14
+ onActivate(): void;
15
+ private currentList;
16
+ /** Only the intensity names this exact model accepts. */
17
+ private currentIntensities;
18
+ private currentIdx;
19
+ private setIdx;
20
+ private move;
21
+ private confirm;
22
+ private goBack;
23
+ /** Map a terminal row to the list index rendered there, if any. */
24
+ private indexAtRow;
25
+ onMouse(ev: MouseEvent): void;
26
+ onKey(key: Key): void;
27
+ render(buf: Buffer): void;
28
+ private currentModelName;
29
+ }
@@ -0,0 +1,29 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import type { Buffer } from "../terminal/buffer.js";
3
+ import type { Key, MouseEvent } from "../terminal/input.js";
4
+ import type { OwnedUI } from "../plugins/host.js";
5
+ export declare class PluginViewScreen extends Screen {
6
+ private item;
7
+ private offset;
8
+ constructor(item: OwnedUI);
9
+ onKey(key: Key): void;
10
+ onMouse(ev: MouseEvent): void;
11
+ render(buf: Buffer): void;
12
+ }
13
+ export declare class PluginsScreen extends Screen {
14
+ private profile;
15
+ private plugins;
16
+ private index;
17
+ private detail;
18
+ private editing;
19
+ private draft;
20
+ private message;
21
+ private trustPending;
22
+ onActivate(): void;
23
+ private refresh;
24
+ private action;
25
+ onKey(key: Key): void;
26
+ onPaste(text: string): void;
27
+ onMouse(ev: MouseEvent): void;
28
+ render(buf: Buffer): void;
29
+ }
@@ -0,0 +1,196 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { drawHeader } from "../header.js";
4
+ import { drawHint } from "../ui.js";
5
+ import { configurePlugin, installPlugin, listPlugins, removePlugin, rollbackPlugin, selectProvider } from "../plugins/manager.js";
6
+ import { invalidateTuiPlugins, terminalUI } from "../plugins/terminal.js";
7
+ import { sanitizeText } from "../sanitize.js";
8
+ export class PluginViewScreen extends Screen {
9
+ item;
10
+ offset = 0;
11
+ constructor(item) {
12
+ super();
13
+ this.item = item;
14
+ }
15
+ onKey(key) { if (key.name === "escape" || key.name === "backspace") {
16
+ this.app.popScreen();
17
+ return;
18
+ } if (key.name === "down" || key.name === "pagedown")
19
+ this.offset++; if (key.name === "up" || key.name === "pageup")
20
+ this.offset = Math.max(0, this.offset - 1); this.app.render(); }
21
+ onMouse(ev) { if (ev.type === "click" && ev.button === 2)
22
+ this.app.popScreen(); }
23
+ render(buf) { drawHeader(buf, this.app.state); buf.writeText(2, 3, this.item.title, { fg: C.accent, bold: true }); const text = sanitizeText(this.item.text ?? "This view is available in the browser. / 此视图可在浏览器中打开。"); text.split("\n").slice(this.offset, this.offset + Math.max(1, buf.height - 8)).forEach((line, i) => buf.writeText(2, 5 + i, line.slice(0, Math.max(1, buf.width - 4)), { fg: C.white })); drawHint(buf, buf.height - 2, "Esc back · ↑/↓ scroll"); }
24
+ }
25
+ export class PluginsScreen extends Screen {
26
+ profile = "standard";
27
+ plugins = [];
28
+ index = 0;
29
+ detail = false;
30
+ editing = null;
31
+ draft = "";
32
+ message = "";
33
+ trustPending = false;
34
+ onActivate() { this.refresh(); }
35
+ refresh() { try {
36
+ this.plugins = listPlugins(this.profile).plugins;
37
+ this.index = Math.min(this.index, Math.max(0, this.plugins.length - 1));
38
+ invalidateTuiPlugins();
39
+ }
40
+ catch (e) {
41
+ this.message = e.message;
42
+ } }
43
+ action(run) { try {
44
+ run();
45
+ this.message = "Saved / 已保存";
46
+ this.refresh();
47
+ }
48
+ catch (e) {
49
+ this.message = e.message;
50
+ } this.app.render(); }
51
+ onKey(key) {
52
+ if (this.editing) {
53
+ if (key.name === "escape") {
54
+ this.editing = null;
55
+ this.app.render();
56
+ return;
57
+ }
58
+ if (key.name === "return") {
59
+ const type = this.editing;
60
+ this.editing = null;
61
+ this.action(() => { if (type === "install")
62
+ installPlugin(this.draft);
63
+ else
64
+ configurePlugin(this.plugins[this.index].manifest.id, { config: JSON.parse(this.draft) }, this.profile); });
65
+ return;
66
+ }
67
+ if (key.name === "backspace")
68
+ this.draft = this.draft.slice(0, -1);
69
+ else if (!key.ctrl && !key.meta && key.name.length === 1)
70
+ this.draft += key.name;
71
+ this.app.render();
72
+ return;
73
+ }
74
+ if (key.name === "escape" || key.name === "backspace") {
75
+ if (this.detail) {
76
+ this.detail = false;
77
+ this.trustPending = false;
78
+ this.app.render();
79
+ }
80
+ else
81
+ this.app.popScreen();
82
+ return;
83
+ }
84
+ if (key.name === "tab") {
85
+ this.profile = this.profile === "standard" ? "pro" : "standard";
86
+ this.refresh();
87
+ this.app.render();
88
+ return;
89
+ }
90
+ if (key.name === "up") {
91
+ this.index = Math.max(0, this.index - 1);
92
+ this.app.render();
93
+ return;
94
+ }
95
+ if (key.name === "down") {
96
+ this.index = Math.min(this.plugins.length - 1, this.index + 1);
97
+ this.app.render();
98
+ return;
99
+ }
100
+ if (key.name === "i") {
101
+ this.editing = "install";
102
+ this.draft = "";
103
+ this.app.render();
104
+ return;
105
+ }
106
+ if (key.name === "m") {
107
+ const items = terminalUI().filter(u => ["app.menu", "workspace.tabs", "settings.sections"].includes(u.slot));
108
+ if (items.length)
109
+ this.app.pushScreen(new PluginViewScreen({ ...items[0], text: items.map(i => `${i.title}\n${i.text ?? ""}`).join("\n\n") }));
110
+ return;
111
+ }
112
+ const p = this.plugins[this.index];
113
+ if (!p)
114
+ return;
115
+ if (key.name === "return") {
116
+ this.detail = !this.detail;
117
+ this.app.render();
118
+ return;
119
+ }
120
+ if (key.name === "e") {
121
+ if (!p.enabled && p.manifest.runtime === "native" && p.manifest.entries?.agent) {
122
+ this.trustPending = true;
123
+ this.message = "Native plugin can execute unrestricted local code. Press Y to trust this exact revision, Esc to cancel.";
124
+ this.app.render();
125
+ return;
126
+ }
127
+ this.action(() => configurePlugin(p.manifest.id, { enabled: !p.enabled }, this.profile));
128
+ return;
129
+ }
130
+ if (key.name === "y" && this.trustPending) {
131
+ this.trustPending = false;
132
+ this.action(() => configurePlugin(p.manifest.id, { enabled: true, trustNative: true }, this.profile));
133
+ return;
134
+ }
135
+ if (key.name === "c") {
136
+ this.editing = "config";
137
+ this.draft = JSON.stringify(p.config);
138
+ this.app.render();
139
+ return;
140
+ }
141
+ if (key.name === "u") {
142
+ this.action(() => { installPlugin(p.source); });
143
+ return;
144
+ }
145
+ if (key.name === "r") {
146
+ this.action(() => rollbackPlugin(p.manifest.id));
147
+ return;
148
+ }
149
+ if (key.name === "d") {
150
+ this.action(() => removePlugin(p.manifest.id));
151
+ return;
152
+ }
153
+ if (key.name === "v") {
154
+ const replacements = (p.manifest.contributes?.ui ?? []).filter(u => ["chat.composer", "chat.message.renderer", "chat.card.plan", "chat.card.tool"].includes(u.slot));
155
+ this.action(() => { for (const u of replacements)
156
+ selectProvider(u.slot, `${p.manifest.id}/${u.id}`, this.profile); for (const prompt of p.manifest.contributes?.prompts ?? [])
157
+ if (prompt.replace)
158
+ selectProvider("persona", `${p.manifest.id}/${prompt.id}`, this.profile); });
159
+ }
160
+ }
161
+ onPaste(text) { if (this.editing) {
162
+ this.draft += text;
163
+ this.app.render();
164
+ } }
165
+ onMouse(ev) { if (ev.type === "click" && ev.button === 2)
166
+ this.app.popScreen();
167
+ else if (ev.type === "click" && ev.button === 0 && ev.y >= 5) {
168
+ this.index = Math.min(this.plugins.length - 1, Math.max(0, ev.y - 5));
169
+ this.app.render();
170
+ } }
171
+ render(buf) {
172
+ drawHeader(buf, this.app.state);
173
+ buf.writeText(2, 2, `Plugins / 插件 · ${this.profile}`, { fg: C.accent, bold: true });
174
+ let y = 4;
175
+ if (this.editing) {
176
+ buf.writeText(2, y++, this.editing === "install" ? "Local plugin directory / 本机插件目录" : "Configuration JSON / 配置 JSON", { fg: C.white });
177
+ buf.writeText(2, y++, this.draft.slice(-Math.max(1, buf.width - 5)), { fg: C.accent });
178
+ }
179
+ else if (this.detail && this.plugins[this.index]) {
180
+ const p = this.plugins[this.index];
181
+ const lines = [p.manifest.name, `${p.manifest.id} @ ${p.manifest.version}`, p.manifest.description ?? "", `Runtime: ${p.manifest.runtime ?? "sandbox"}`, `Permissions: ${(p.manifest.permissions ?? []).join(", ")}`, `Source: ${p.source}`, `Integrity: ${p.integrity}`, p.error ?? "", JSON.stringify(p.config)];
182
+ for (const line of lines)
183
+ buf.writeText(2, y++, sanitizeText(line, { singleLine: true }).slice(0, buf.width - 4), { fg: C.white });
184
+ }
185
+ else {
186
+ if (!this.plugins.length)
187
+ buf.writeText(2, 5, "No plugins. Press I to install a local directory. / 按 I 安装本机插件。", { fg: C.dim });
188
+ const start = Math.max(0, this.index - Math.max(1, buf.height - 12) + 1);
189
+ this.plugins.slice(start, start + Math.max(1, buf.height - 12)).forEach((p, i) => buf.writeText(2, 5 + i, `${i + start === this.index ? "›" : " "} ${p.enabled ? "●" : "○"} ${p.manifest.name} ${p.manifest.version}${p.error ? " !" : ""}`.slice(0, buf.width - 4), { fg: i + start === this.index ? C.accent : C.white, bold: i + start === this.index }));
190
+ }
191
+ if (this.message)
192
+ buf.writeText(2, buf.height - 4, sanitizeText(this.message, { singleLine: true }).slice(0, buf.width - 4), { fg: C.amber });
193
+ drawHint(buf, buf.height - 3, "↑/↓ select · Enter details · E enable/disable · I install · C config · U update");
194
+ drawHint(buf, buf.height - 2, "R rollback · D remove · V select providers · M extensions · Tab profile · Esc back");
195
+ }
196
+ }