@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
@@ -0,0 +1,65 @@
1
+ # Local programmatic API / 本机程序 API
2
+
3
+ Neutrino 的后端运行时,会同时启动独立的本机 HTTP API。它只监听 IPv4 `127.0.0.1`;即使 Web 控制台开启局域网访问,本 API 也不会随之对局域网开放。默认端口为 `8766`,可在启动前设置 `NEUTRINO_API_PORT`(`0` 表示由操作系统选择空闲端口)。Web 端口设置为 `0` 时,未指定 API 端口也会自动选择空闲端口。端口被占用时后端启动失败并报告错误。API 随后端关闭而关闭。需要不启动 Web 界面时,可单独运行 `neutrino api`(可选 `--port 0` 自动选择端口),只启动本机 API;退出该进程后 API 关闭。`--no-web` 本身不会自动启动这个独立进程。
4
+
5
+ 后端启动后在 `~/.neutrino/api.json` 写入连接信息;设置了 `NEUTRINO_HOME` 时使用 `$NEUTRINO_HOME/api.json`。文件内容为 `{ "pid": 123, "url": "http://127.0.0.1:8766/api/v1", "token": "..." }`,其中端口以文件中的实际地址为准。正常关闭时文件会删除;异常退出可能留下旧文件,连接失败时应检查进程和重新读取文件。多个使用同一数据目录的独立服务会共用发现文件,最后启动的服务会更新它。不要把 token 分享到日志、URL 或网页。Unix 上文件以 `0600` 创建;Windows 的访问控制取决于用户目录的 ACL。
6
+
7
+ 所有请求都需要 `Authorization: Bearer <token>`;Host 必须是发现文件中的 `127.0.0.1:<port>`,带 `Origin` 的请求会被拒绝。使用编程语言的 HTTP 客户端访问;这个接口不开放浏览器跨域调用。接口中的路径均相对于 `api.json` 的 `url`。Pro 工作区在路径开头添加 `/pro`,例如 `/pro/conversations`、`/pro/live/:key/events`。普通和 Pro 的会话、历史互相隔离。
8
+
9
+ ## 开始对话
10
+
11
+ 1. `GET /models` 查看服务商、模型和支持的思考强度;`GET /capabilities` 查看模式。Pro 使用 `GET /pro/models` 查看团队预设。
12
+ 2. `POST /conversations` 创建对话:
13
+
14
+ ```json
15
+ {
16
+ "cwd": "/absolute/path/to/project",
17
+ "selection": { "provider": "Provider", "model": "Model", "intensity": "High" },
18
+ "agent_mode": "standard",
19
+ "approval_mode": "request"
20
+ }
21
+ ```
22
+
23
+ `cwd` 必须是存在的目录。`selection` 指已配置模型;可选 `agent_mode` 为 `standard`、`ptc`、`fast` 或 `fast_dag`,后者还受现有 Fast 可用性配置约束。审批模式为 `request` 或 `auto`。不传的字段沿用当前工作区默认值。返回 `{ok, key, cwd, snapshot}`;`key` 是当前进程中的活动会话标识,重启后应通过历史记录恢复。
24
+
25
+ Pro 创建使用 `POST /pro/conversations` 和 `{ "cwd": "...", "pro_preset": "medium", "approval_mode": "request" }`。Pro 按团队预设选择各角色的模型与强度,不能通过普通 `agent_mode` 指定其他执行模式。
26
+ 3. 建议先连接 `GET /live/:key/events`,再 `POST /live/:key/message`,请求体 `{ "text": "请检查这个项目" }`。运行中的消息按现有 Web 行为排队;请求也可带 `agent_mode` 指定下一回合的模式。
27
+ 4. 流中出现 `snapshot.approval` 或 `approval.requested` 时,使用 `POST /live/:key/approvals/:approval_id` 答复 `{ "approved": true }` 或 `{ "approved": false }`。必须传 JSON 布尔值。请求已取消、已回答或 ID 已过期时返回 `409`;不能用旧 ID 回答新请求。多个审批会依次呈现。`auto` 模式仍遵循现有的自动审查和极高风险规则。
28
+
29
+ `examples/local-api.py` 是标准库示例:
30
+
31
+ ```sh
32
+ python3 examples/local-api.py --cwd /path/to/project \
33
+ --provider Provider --model Model --intensity High --mode standard \
34
+ '请检查这个项目'
35
+ ```
36
+
37
+ 示例订阅输出与状态并在终端询问审批;按 Ctrl+C 可退出客户端。退出或断线不会自动停止服务端任务、批准或拒绝待处理审批;可重新连接并在快照中找到待处理请求,或调用暂停接口。长任务客户端应实现重连,保存事件 ID,并同时展示完整快照中的消息;示例只演示基础连接。
38
+
39
+ ## 流式协议
40
+
41
+ `GET /live/:key/events` 返回 SSE,含 `snapshot`、`approval.requested`、`approval.resolved` 和 `gone` 事件,另有注释心跳。`snapshot` 与 Web 使用同一会话快照:
42
+
43
+ - `messages` 和 `streamingContent` 提供已记录消息、工具调用与当前流式文本;文本为当前累计值,不是增量。
44
+ - `busy`、`paused`、`queued`、`compressing`、`retrying`、`saveError`、`tokens`、`task`、`plan`、`subagent`、`fast`、`pro`、`reviewing` 和 `approval` 等字段描述运行状态。字段可为空或按模式缺省。应结合这些字段和最终消息判断结果,`busy: false` 本身不代表任务成功。
45
+ - `approval` 包含 ID、工具、完整参数、工作目录、风险等级、摘要和原因;`reviewing` 是自动审查进度,不表示正在等待人工审批。`approval.resolved` 表示该请求不再等待,并不单独声明批准还是拒绝。
46
+
47
+ 快照更新采用现有 Web 的合并机制,适合实时显示当前状态,不保证逐 token 或每一次短暂中间状态都单独送达。订阅期间产生的事件带 `id`;重新连接时可用 `Last-Event-ID` 请求重放。服务端在首次订阅后为每个会话保留最近 256 个事件,且尽量限制到约 4 MiB,未连接的会话在 10 分钟后可被清理;单个大快照可能超过这个大小。重放历史不可用时发送 `reset`,随后发送当前完整 `snapshot`。每次连接都会提供当前快照;首次订阅之前的短暂事件和服务重启前的事件无法重放,历史消息请通过会话历史读取。服务支持最多 64 个同时被监视的会话,资源不足时返回 `503`。
48
+
49
+ ## 其他接口
50
+
51
+ 这些接口复用 Web 的业务处理,参数、响应及冲突行为与 [Web API](README.md#api) 一致;普通工作区在以下路径前加 `/api/v1`,Pro 在 `/api/v1` 后再加 `/pro`:
52
+
53
+ | 功能 | 路径 |
54
+ | --- | --- |
55
+ | 会话列表和状态 | `GET /overview`、`GET /sessions`、`GET /live/:key` |
56
+ | 活动会话控制 | `POST /live/:key/pause`、`resume`、`clear`、`close` |
57
+ | 会话配置 | `POST /live/:key/model`、`agent-mode`、`approval-mode`;Pro 使用 `pro-preset` |
58
+ | 历史记录 | `GET /session/:id`、`POST /session/:id/continue`、`POST /session/:id/delete` |
59
+ | 工作目录 | `GET/POST /workspaces`、`GET /fs?path=...`、`POST /fs/mkdir`、`POST /fs/pick` |
60
+ | 其他 Agent 会话 | `GET /agents/:agent/sessions?cwd=...`、`GET /agents/:agent/sessions/:id`、`POST /agents/:agent/sessions/:id/migrate` |
61
+ | 插件 | `GET /plugins`、`GET /plugins/catalog`、`GET /plugins/asset`、`POST /plugins`;参见[插件文档](../docs/plugins/README.md) |
62
+ | 更新状态与操作 | `GET /update/events`、`POST /update/check`、`POST /update` |
63
+ | 侧边栏已读状态 | `GET/POST /seen` |
64
+
65
+ `GET /health` 为健康检查。`GET /live/:key/stream` 仍提供 Web 原有快照流,推荐新客户端使用 `/events`。Web 自身的 `/api/...`、登录方式和局域网行为保持不变。本 API 不提供全局设置、登录、后台服务管理接口;对话级配置和插件操作可用。终端拥有的会话不能通过 `close` 关闭,与 Web 相同。
package/web/README.md CHANGED
@@ -11,10 +11,11 @@ A small, dependency-free single page app that is served by the Neutrino CLI at
11
11
  (the turn being answered is open, and a pause is not a fold);
12
12
  - read replies as rendered markdown: headings, nested/task lists, tables, block
13
13
  quotes, code fences, emphasis, safe links and images; raw HTML is escaped;
14
- - start a new conversation in a working directory you choose, including a folder
15
- created from the built-in folder picker;
14
+ - start a new conversation in a working directory you choose, in the operating
15
+ system's folder window (or the in-page picker, which can also create a folder);
16
16
  - switch the workspace between every known folder and **All directories**;
17
17
  - switch the provider, model and intensity for the next turn;
18
+ - select Standard, PTC, the original Fast mode, the new **Fast Mode** (`fast_dag`), or Pro from the composer menu; the choice is saved per conversation and frozen when a message is submitted. See [Fast Mode](../docs/fast-mode.md) for configuration and measured limitations;
18
19
  - edit every terminal setting — defaults, interface language, plan-first turns,
19
20
  default approval mode, context compression, reviewer, Web password, and LAN
20
21
  access;
@@ -260,10 +261,23 @@ working directory plus provider, model, thinking level and approval mode. The
260
261
  conversation is browser-owned from that moment: it appears in the sidebar under
261
262
  its folder and is written to its own session file once the first message exists.
262
263
 
263
- **Browse…** opens the server-side folder picker. It lists folders only, offers
264
+ **Browse…** opens the operating system's own folder window — the Explorer
265
+ dialog on Windows, `choose folder` on macOS, zenity or kdialog on Linux —
266
+ drawn by the console process, so the folder is picked the way it is picked in
267
+ any other application. A browser can only ever see the *contents* of a folder
268
+ it was allowed to read, never the absolute path, which is why the window comes
269
+ from the server and not from the page.
270
+
271
+ The window is raised above the browser and flashed when it opens: it belongs to
272
+ a process that has no window of its own, so Windows would otherwise leave it
273
+ behind the page, with nothing in the taskbar to find it by.
274
+
275
+ The in-page folder picker stays as the fallback: it lists folders only, offers
264
276
  the existing drive roots (on Windows) or `/`, moves up through the directory
265
- tree, and can create a folder in place. **Use this folder** fills the working
266
- directory of the new conversation.
277
+ tree, and can create a folder in place. It opens when no system window can be
278
+ drawn — a phone or another device on the LAN, a headless server, a desktop
279
+ without zenity or kdialog — and **Use this folder** fills the working directory
280
+ in that case too.
267
281
 
268
282
  **Working directories** (the folder button under New conversation) shows every
269
283
  folder the console has opened or that already holds conversations. Choosing one
@@ -392,6 +406,8 @@ to know whether a conversation is driven by the terminal or by the server.
392
406
 
393
407
  ## API
394
408
 
409
+ For other local programs, use the independently authenticated, loopback-only [programmatic API](LOCAL-API.md).
410
+
395
411
  | Method | Path | Purpose |
396
412
  | --- | --- | --- |
397
413
  | GET | `/api/overview` | Sessions grouped by cwd + live sessions |
@@ -399,7 +415,8 @@ to know whether a conversation is driven by the terminal or by the server.
399
415
  | GET | `/api/workspaces` | Recent working directories, launch cwd and home |
400
416
  | POST | `/api/workspaces` | Validate and remember `{ "path": "…" }` |
401
417
  | GET | `/api/fs?path=…` | Folder listing for the picker (folders only, roots, parent) |
402
- | POST | `/api/fs/mkdir` | Create `{ "path": "…" }` and return the new path |
418
+ | POST | `/api/fs/mkdir` | Create `{ "path": "…" }` and return the new path |
419
+ | POST | `/api/fs/pick` | Open the operating system's folder window on this machine and return `{ "path": … }` — `null` when it was closed; loopback clients only |
403
420
  | POST | `/api/conversations` | Start `{ cwd, selection?, approval_mode? }`, returns `{ key, snapshot }` |
404
421
  | GET | `/api/settings` | Model catalog (each model lists its supported intensities), languages, public settings, the live bind address (`web_host`, `web_urls`) and the background state (`background`, `background_running`, `background_url`, one-time `background_prompt`); no credentials |
405
422
  | POST | `/api/settings` | Save validated `{ selection?, settings? }`, including `language`, `plan_first`, `approval_mode`, `web_lan`, `background` and `web_password` |
@@ -466,3 +483,7 @@ programmatically (409 when nothing is pending).
466
483
 
467
484
  `POST /api/live/:key/approval-mode` with `{"mode":"request"|"auto"}` changes
468
485
  the conversation mode and returns its current snapshot (400 for invalid modes).
486
+
487
+ ## Plugins / 插件
488
+
489
+ Open **Plugins** in the sidebar to install local plugins, enable them per profile or project, edit configuration, and select replacement composers and cards. Views use sandboxed iframes and versioned draft APIs. See [插件文档](../docs/plugins/README.md).
package/web/app.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createPluginUI } from "./plugins.js";
2
+ let pluginUI = null;
1
3
  import { createSettingsUI } from "./settings.js";
2
4
  import { createProUI } from "./pro.js";
3
5
  import { renderMarkdown as renderMarkdownHtml } from "./markdown.js";
@@ -596,9 +598,9 @@ function storedWorkspace() {
596
598
  return { key: pathKey(path), path };
597
599
  }
598
600
 
599
- const AGENT_MODE_LABELS = { standard: "Standard mode", ptc: "PTC mode", fast: "Fast mode", pro: "Professional mode" };
601
+ const AGENT_MODE_LABELS = { standard: "Standard mode", ptc: "PTC mode", fast: "Fast mode", fast_dag: "Fast Mode", pro: "Professional mode" };
600
602
  let newAgentMode = readStorage("neutrino.agent-mode");
601
- if (!["standard", "ptc", "fast"].includes(newAgentMode)) newAgentMode = "standard";
603
+ if (!["standard", "ptc", "fast", "fast_dag"].includes(newAgentMode)) newAgentMode = "standard";
602
604
  let agentModePending = Promise.resolve();
603
605
  let agentModeChanging = false;
604
606
  let workspaceNavigation = false;
@@ -625,7 +627,8 @@ function renderAgentMode() {
625
627
  el.modePicker.classList.toggle("is-pending", Boolean(agentModeChanging || nextTurn));
626
628
  for (const button of el.modePicker.querySelectorAll?.("[data-agent-mode]") || []) {
627
629
  button.setAttribute("aria-checked", String(button.dataset.agentMode === mode));
628
- button.disabled = agentModeChanging || workspaceNavigation;
630
+ button.disabled = agentModeChanging || workspaceNavigation || (button.dataset.agentMode === "fast_dag" && state.overview?.fast_mode_enabled === false);
631
+ if (button.dataset.agentMode === "fast_dag") button.title = state.overview?.fast_mode_enabled === false ? tr("Fast Mode is disabled; select another mode.") : "";
629
632
  }
630
633
  if (el.modeFeedback) {
631
634
  el.modeFeedback.textContent = agentModeError || (agentModeChanging ? tr("Changing mode…") : nextTurn ? tr("Applies to the next turn") : "");
@@ -727,7 +730,7 @@ function initializeAgentMode() {
727
730
  if (event.key === "Escape") { el.modePicker.open = false; el.modePicker.querySelector("summary")?.focus(); event.stopPropagation(); }
728
731
  if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
729
732
  event.preventDefault(); el.modePicker.open = true;
730
- const buttons = [...el.modePicker.querySelectorAll("[data-agent-mode]")];
733
+ const buttons = [...el.modePicker.querySelectorAll("[data-agent-mode]")].filter(button => !button.disabled);
731
734
  let index = buttons.indexOf(document.activeElement);
732
735
  index = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
733
736
  buttons[index]?.focus();
@@ -983,6 +986,7 @@ async function loadOverview(options = {}) {
983
986
  const overview = await response.json();
984
987
  if (gateShown) return;
985
988
  state.overview = overview;
989
+ if (el.modePicker) renderAgentMode();
986
990
  setStatus(state.selection?.kind === "live" ? "live" : "online", state.selection?.kind === "live" ? "live" : "online");
987
991
  renderWorkspaceChrome();
988
992
  renderSidebar();
@@ -1628,14 +1632,32 @@ function canPauseFromComposer() {
1628
1632
  && snapshot.busy && !snapshot.paused && !el.input.value.trim());
1629
1633
  }
1630
1634
 
1635
+ // A paused turn resumes from the composer button, exactly like an empty draft
1636
+ // pauses a running one: the draft wins, so typing always means "send".
1637
+ function canResumeFromComposer() {
1638
+ const selection = state.selection;
1639
+ const snapshot = state.snapshot;
1640
+ return Boolean(selection?.kind === "live" && snapshot?.key === selection.key
1641
+ && snapshot.paused && !el.input.value.trim());
1642
+ }
1643
+
1644
+ function composerAction() {
1645
+ if (canPauseFromComposer()) return "pause";
1646
+ if (canResumeFromComposer()) return "resume";
1647
+ return "send";
1648
+ }
1649
+
1631
1650
  function renderComposerAction() {
1632
- const pause = canPauseFromComposer();
1633
- const label = pause ? "Pause" : "Send message";
1634
- el.send.classList.toggle("is-pause", pause);
1635
- el.send.setAttribute("data-i18n-title", label);
1636
- el.send.setAttribute("data-i18n-aria-label", label);
1637
- el.send.title = tr(label);
1638
- el.send.setAttribute("aria-label", tr(label));
1651
+ const action = composerAction();
1652
+ el.send.classList.toggle("is-pause", action === "pause");
1653
+ el.send.classList.toggle("is-resume", action === "resume");
1654
+ const [source, text] = action === "pause" ? ["Pause", tr("Pause")]
1655
+ : action === "resume" ? ["Resume", tr("Resume")]
1656
+ : ["Send message", tr("Send message")];
1657
+ el.send.setAttribute("data-i18n-title", source);
1658
+ el.send.setAttribute("data-i18n-aria-label", source);
1659
+ el.send.title = text;
1660
+ el.send.setAttribute("aria-label", text);
1639
1661
  }
1640
1662
 
1641
1663
  function hideComposer() {
@@ -1885,7 +1907,7 @@ function renderControls(snapshot) {
1885
1907
  ? reviewStatus(reviewing)
1886
1908
  : subagent
1887
1909
  ? tr("sub-agent: {label}", { label: subagent })
1888
- : tr("working")
1910
+ : snapshot.fast && snapshot.activeAgentMode === "fast_dag" ? fastPhaseLabel(snapshot.fast.phase) : tr("working")
1889
1911
  : tr("idle");
1890
1912
  el.controls.appendChild(badge);
1891
1913
 
@@ -1896,12 +1918,15 @@ function renderControls(snapshot) {
1896
1918
  el.controls.appendChild(warning);
1897
1919
  }
1898
1920
 
1899
- if (snapshot.busy || snapshot.paused) {
1921
+ // Resuming belongs to the composer button (see renderComposerAction), which
1922
+ // offers "Continue" on an empty draft, so the status bar never shows a second
1923
+ // way in. Pausing stays here, for a turn that is actually running.
1924
+ if (snapshot.busy && !snapshot.paused) {
1900
1925
  const button = document.createElement("button");
1901
1926
  button.className = "control";
1902
- button.textContent = snapshot.paused ? tr("Resume") : tr("Pause");
1927
+ button.textContent = tr("Pause");
1903
1928
  button.addEventListener("click", () => {
1904
- postAction(snapshot.key, snapshot.paused ? "resume" : "pause");
1929
+ postAction(snapshot.key, "pause");
1905
1930
  });
1906
1931
  el.controls.appendChild(button);
1907
1932
  }
@@ -2028,6 +2053,7 @@ function renderMessageList(messages, streaming, options) {
2028
2053
  function renderMessage(message, index) {
2029
2054
  const article = document.createElement("article");
2030
2055
  article.className = `message ${message.role}`;
2056
+ article.dataset.pluginMessage = String(index);
2031
2057
 
2032
2058
  if (message.role === "tool") {
2033
2059
  const details = document.createElement("details");
@@ -2178,52 +2204,69 @@ function scrollToBottom() {
2178
2204
 
2179
2205
  async function postAction(key, action, body) {
2180
2206
  try {
2181
- await apiFetch(`/api/live/${encodeURIComponent(key)}/${action}`, {
2207
+ const response = await apiFetch(`/api/live/${encodeURIComponent(key)}/${action}`, {
2182
2208
  method: "POST",
2183
2209
  headers: body ? { "Content-Type": "application/json" } : undefined,
2184
2210
  body: body ? JSON.stringify(body) : undefined,
2185
2211
  });
2212
+ if (!response.ok) { const data = await response.json(); throw new Error(data.error || `HTTP ${response.status}`); }
2213
+ return true;
2186
2214
  } catch (error) {
2215
+ el.sub.textContent = error.message;
2187
2216
  console.error(error);
2217
+ return false;
2188
2218
  }
2189
2219
  }
2190
2220
 
2191
2221
  function activateComposer() {
2192
- if (canPauseFromComposer()) return postAction(state.selection.key, "pause");
2222
+ const action = composerAction();
2223
+ if (action === "pause" || action === "resume") return postAction(state.selection.key, action);
2193
2224
  return sendMessage();
2194
2225
  }
2195
2226
 
2196
2227
  async function sendMessage() {
2228
+ // Internal plugin requests pin their originating selection and can preserve the user draft.
2229
+ const pluginRequest = arguments[0] ?? null;
2230
+ // The keydown handler passes the text it saw: sendMessage awaits the agent-mode
2231
+ // preference before touching the textarea, so reading it here is too late.
2232
+ const submittedText = arguments[1] ?? null;
2197
2233
  try { await agentModePending; } catch (error) { el.sub.textContent = error.message; return; }
2234
+ if (pluginRequest && state.selection !== pluginRequest.selection) throw new Error("Conversation changed before the plugin message was sent");
2198
2235
  const selection = state.selection;
2199
2236
  if (!selection) return;
2200
- const text = el.input.value;
2237
+ const text = pluginRequest?.text ?? submittedText ?? el.input.value;
2201
2238
  if (!text.trim()) return;
2202
2239
  try {
2203
2240
  if (PRO_MODE) await proUI?.flush();
2204
2241
  else await preferences.flush?.();
2205
2242
  } catch (error) { el.sub.textContent = tr(error.message); return; }
2206
- if (state.selection !== selection) return;
2243
+ if (state.selection !== selection) { if (pluginRequest) throw new Error("Conversation changed before the plugin message was sent"); return; }
2244
+ const messagePayload = { text, ...(PRO_MODE ? {} : { agent_mode: currentAgentMode() }) };
2207
2245
 
2208
2246
  if (selection.kind === "live") {
2209
- el.input.value = "";
2247
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2210
2248
  autosize();
2211
- await postAction(selection.key, "message", { text });
2249
+ // A paused turn only queues the text, so a user send resumes right away: the
2250
+ // message then starts the turn instead of waiting for a separate continue click.
2251
+ const resumes = !pluginRequest && state.snapshot?.key === selection.key && Boolean(state.snapshot.paused);
2252
+ const sent = await postAction(selection.key, "message", messagePayload);
2253
+ if (sent === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2254
+ else if (resumes) await postAction(selection.key, "resume");
2212
2255
  return;
2213
2256
  }
2214
2257
 
2215
2258
  if (selection.kind === "new") {
2216
2259
  // A workspace landing page: typing starts a fresh conversation there, just
2217
2260
  // like typing on the terminal's start screen.
2218
- el.input.value = "";
2261
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2219
2262
  autosize();
2220
2263
  try {
2221
2264
  const created = await createConversation(selection.cwd, preferences.defaultSelection?.());
2222
2265
  await selectLive(created.key);
2223
2266
  setConversationView(true);
2224
- await postAction(created.key, "message", { text });
2267
+ if (await postAction(created.key, "message", messagePayload) === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2225
2268
  } catch (error) {
2226
- el.input.value = text;
2269
+ if (!pluginRequest?.preserveDraft) el.input.value = text;
2227
2270
  autosize();
2228
2271
  el.sub.textContent = tr("Could not start the conversation: {error}", { error: error.message });
2229
2272
  }
@@ -2231,13 +2274,13 @@ async function sendMessage() {
2231
2274
  }
2232
2275
 
2233
2276
  // Reading a saved transcript: resume it in the browser, then deliver the text.
2234
- el.input.value = "";
2277
+ if (!pluginRequest?.preserveDraft) el.input.value = "";
2235
2278
  autosize();
2236
2279
  try {
2237
2280
  const key = await continueSession(selection.id);
2238
- await postAction(key, "message", { text });
2281
+ if (await postAction(key, "message", messagePayload) === false) { if (!pluginRequest?.preserveDraft) el.input.value = text; autosize(); }
2239
2282
  } catch (error) {
2240
- el.input.value = text;
2283
+ if (!pluginRequest?.preserveDraft) el.input.value = text;
2241
2284
  autosize();
2242
2285
  el.sub.textContent = tr("Could not continue this conversation: {error}", { error: error.message });
2243
2286
  }
@@ -2709,6 +2752,46 @@ async function loadFolder(path) {
2709
2752
  }
2710
2753
  }
2711
2754
 
2755
+ /**
2756
+ * Ask the server to open the operating system's folder window.
2757
+ *
2758
+ * The browser's own picker never reveals an absolute path, so a working
2759
+ * directory has to be chosen on the machine that runs the agent. Resolves to the
2760
+ * chosen path, "" when the user closed the window, or null when this browser
2761
+ * cannot use a system window: another device, or no desktop behind the server.
2762
+ */
2763
+ async function pickFolderInSystemWindow(start, feedback) {
2764
+ if (feedback) feedback.textContent = tr("Waiting for the folder window on your desktop…");
2765
+ try {
2766
+ const response = await apiFetch("/api/fs/pick", {
2767
+ method: "POST",
2768
+ headers: { "Content-Type": "application/json" },
2769
+ body: JSON.stringify({ start, title: tr("Select a working directory") }),
2770
+ });
2771
+ const data = await response.json().catch(() => ({}));
2772
+ if (!response.ok) return null;
2773
+ return typeof data.path === "string" ? data.path : "";
2774
+ } catch {
2775
+ // A window that could not open is a reason to fall back, never an error.
2776
+ return null;
2777
+ } finally {
2778
+ if (feedback) feedback.textContent = "";
2779
+ }
2780
+ }
2781
+
2782
+ /**
2783
+ * Choose a working directory: the system folder window first, the in-page
2784
+ * folder browser when no window can open here.
2785
+ */
2786
+ async function chooseFolder({ start = "", onPick = null, feedback = null } = {}) {
2787
+ const picked = await pickFolderInSystemWindow(start, feedback);
2788
+ if (picked === null) {
2789
+ openFolderDialog({ start, onPick });
2790
+ return;
2791
+ }
2792
+ if (picked && typeof onPick === "function") onPick(picked);
2793
+ }
2794
+
2712
2795
  function openFolderDialog({ start = "", onPick = null } = {}) {
2713
2796
  state.folder = {
2714
2797
  path: "",
@@ -2812,12 +2895,28 @@ function planStatusLabel(status) {
2812
2895
  return tr("Pending");
2813
2896
  }
2814
2897
 
2898
+ function fastPhaseLabel(phase) {
2899
+ switch (phase) {
2900
+ case "planning": return tr("Planning tasks");
2901
+ case "executing": return tr("Running tasks concurrently");
2902
+ case "verifying": return tr("Verifying local results");
2903
+ case "integrating": return tr("Integrating results");
2904
+ case "reviewing": return tr("Final global review");
2905
+ case "completed": return tr("Fast Mode completed");
2906
+ case "cancelled": return tr("Fast Mode cancelled");
2907
+ case "cancelling": return tr("Stopping tasks…");
2908
+ default: return tr("Fast Mode failed");
2909
+ }
2910
+ }
2911
+
2815
2912
  function renderPlanCard(source) {
2816
2913
  if (!el.planCard) return;
2817
2914
  const plan = source && source.plan && Array.isArray(source.plan.steps) && source.plan.steps.length ? source.plan : null;
2818
2915
  const tokens = source && source.tokens ? source.tokens : null;
2819
2916
  const hasUsage = tokens && (Number(tokens.prompt) > 0 || Number(tokens.completion) > 0);
2820
- if (!state.planOpen || (!plan && !hasUsage)) {
2917
+ const fast = (source?.activeAgentMode === "fast_dag" || (!source?.activeAgentMode && source?.agent_mode === "fast_dag"))
2918
+ ? source.fast || source.fast_run : null;
2919
+ if (!state.planOpen || (!plan && !hasUsage && !fast)) {
2821
2920
  el.planCard.classList.add("hidden");
2822
2921
  el.planCard.replaceChildren();
2823
2922
  return;
@@ -2842,6 +2941,39 @@ function renderPlanCard(source) {
2842
2941
  head.append(title, close);
2843
2942
  card.appendChild(head);
2844
2943
 
2944
+ if (fast) {
2945
+ const progress = document.createElement("div");
2946
+ progress.className = "fast-progress";
2947
+ progress.setAttribute("role", "status");
2948
+ progress.setAttribute("aria-live", "polite");
2949
+ const phase = document.createElement("strong");
2950
+ phase.textContent = tr("Fast Mode") + " · " + fastPhaseLabel(fast.phase);
2951
+ const tasks = (fast.tasks || []).filter(task => !task.verifies);
2952
+ const count = document.createElement("p");
2953
+ count.textContent = tr("{running} running · {verified}/{total} verified", {
2954
+ running: (fast.tasks || []).filter(task => task.status === "RUNNING" || (task.kind === "verification" && task.status === "VERIFYING")).length,
2955
+ verified: tasks.filter(task => task.status === "COMPLETED").length, total: tasks.length,
2956
+ });
2957
+ progress.append(phase, count);
2958
+ const list = document.createElement("ul");
2959
+ for (const task of tasks) {
2960
+ const row = document.createElement("li");
2961
+ row.textContent = `${task.task_goal} · ${task.status}`;
2962
+ list.appendChild(row);
2963
+ }
2964
+ progress.appendChild(list);
2965
+ if (fast.error) {
2966
+ const error = document.createElement("p");
2967
+ error.className = "error"; error.textContent = fast.error;
2968
+ const fallback = document.createElement("button");
2969
+ fallback.type = "button"; fallback.className = "control";
2970
+ fallback.textContent = tr("Select Standard mode to continue");
2971
+ fallback.addEventListener("click", () => { agentModePending = chooseAgentMode("standard"); void agentModePending.catch(() => {}); });
2972
+ progress.append(error, fallback);
2973
+ }
2974
+ card.appendChild(progress);
2975
+ }
2976
+
2845
2977
  if (plan) {
2846
2978
  if (plan.goal) {
2847
2979
  const goal = document.createElement("div");
@@ -3367,9 +3499,12 @@ el.back.addEventListener("click", () => setConversationView(false));
3367
3499
  el.send.addEventListener("click", activateComposer);
3368
3500
  el.input.addEventListener("input", autosize);
3369
3501
  el.input.addEventListener("keydown", (event) => {
3502
+ // An Enter that picks an IME candidate belongs to the input method, not to us:
3503
+ // preventDefault would cancel the commit and send the uncommitted composition.
3504
+ if (event.isComposing || event.keyCode === 229) return;
3370
3505
  if (event.key === "Enter" && !event.shiftKey) {
3371
3506
  event.preventDefault();
3372
- sendMessage();
3507
+ sendMessage(null, el.input.value);
3373
3508
  }
3374
3509
  });
3375
3510
 
@@ -3383,8 +3518,20 @@ el.workspaceAll.addEventListener("click", () => {
3383
3518
  showHome();
3384
3519
  });
3385
3520
  el.workspaceAdd.addEventListener("click", () => {
3386
- el.workspaceDialog.close();
3387
- openFolderDialog({ start: state.workspace?.path || state.workspaces?.cwd || "", onPick: (path) => openWorkspace(path) });
3521
+ // The window opens on the desktop of the machine running the console, so this
3522
+ // sheet stays open with a line saying where to look, and closes once a folder
3523
+ // has been chosen (or when the in-page browser takes over).
3524
+ const start = state.workspace?.path || state.workspaces?.cwd || "";
3525
+ void pickFolderInSystemWindow(start, el.workspaceFeedback).then((picked) => {
3526
+ if (picked === null) {
3527
+ el.workspaceDialog.close();
3528
+ openFolderDialog({ start, onPick: (path) => openWorkspace(path) });
3529
+ return;
3530
+ }
3531
+ if (!picked) return;
3532
+ el.workspaceDialog.close();
3533
+ openWorkspace(picked);
3534
+ });
3388
3535
  });
3389
3536
  el.agentsOpen.addEventListener("click", () => openAgentsDialog());
3390
3537
  el.agentMigrate.addEventListener("click", () => migrateSelectedAgentSession());
@@ -3406,8 +3553,9 @@ el.newChatForm.addEventListener("submit", (event) => {
3406
3553
  submitNewChat();
3407
3554
  });
3408
3555
  el.newChatBrowse.addEventListener("click", () => {
3409
- openFolderDialog({
3556
+ void chooseFolder({
3410
3557
  start: el.newChatCwd.value.trim() || state.workspace?.path || "",
3558
+ feedback: el.newChatFeedback,
3411
3559
  onPick: (path) => {
3412
3560
  el.newChatCwd.value = path;
3413
3561
  renderNewChatRecents();
@@ -3495,3 +3643,24 @@ loadSeen().then(() => loadOverview()).then(async () => {
3495
3643
  if (state.workspace && !state.selection) renderWorkspaceLanding(state.workspace.path);
3496
3644
  });
3497
3645
  setInterval(() => loadOverview({ silent: true }), REFRESH_MS);
3646
+
3647
+ // The plugin shell consumes the same public conversation projection as the built-in UI.
3648
+ if (typeof createPluginUI === "function") pluginUI = createPluginUI({
3649
+ profile: PRO_MODE ? "pro" : "standard",
3650
+ api: apiFetch,
3651
+ context: () => {
3652
+ const data = state.snapshot ?? state.fileData ?? state.agentData ?? {};
3653
+ const selection = state.selection;
3654
+ return { identity:selection ? `${selection.kind}:${selection.key ?? selection.id ?? selection.cwd ?? ""}` : "",
3655
+ cwd:data.cwd ?? selection?.cwd ?? state.workspace?.path ?? "",messages:data.messages ?? [],
3656
+ plan:data.plan,model:data.model,streamingContent:data.streamingContent,pluginUI:data.task?.pluginUI,pluginPrompt:data.task?.pluginPrompt,pluginErrors:data.task?.pluginErrors };
3657
+ },
3658
+ getDraft: () => el.input.value,
3659
+ setDraft: text => { el.input.value=text; autosize(); },
3660
+ submit: async () => { if (!state.selection) throw new Error("Open a conversation first"); await sendMessage({ selection:state.selection,text:el.input.value }); },
3661
+ command: async (name,args) => {
3662
+ if (!state.selection) throw new Error("Open a conversation first");
3663
+ await sendMessage({ selection:state.selection,text:`/plugin ${name} ${JSON.stringify(args)}`,preserveDraft:true });
3664
+ },
3665
+ notice: message => { el.sub.textContent=message; },
3666
+ });
package/web/i18n.js CHANGED
@@ -83,6 +83,19 @@ const ZH = {
83
83
  "Standard mode": "标准模式",
84
84
  "PTC mode": "PTC 模式",
85
85
  "Fast mode": "快速模式",
86
+ "Fast Mode": "极速模式",
87
+ "Fast Mode is disabled; select another mode.": "极速模式已关闭,请选择其他模式。",
88
+ "Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.": "通过动态任务调度与多任务并发执行,大幅缩短复杂任务的完成时间,同时保持原有推理能力。",
89
+ "Planning tasks": "正在规划任务",
90
+ "Running tasks concurrently": "正在并发执行",
91
+ "Verifying local results": "正在执行局部验证",
92
+ "Integrating results": "正在整合结果",
93
+ "Final global review": "正在进行最终审查",
94
+ "Fast Mode completed": "极速模式已完成",
95
+ "Fast Mode failed": "极速模式执行失败",
96
+ "Fast Mode cancelled": "极速模式已取消",
97
+ "{running} running · {verified}/{total} verified": "{running} 个正在运行 · {verified}/{total} 个已验证",
98
+ "Select Standard mode to continue": "选择标准模式继续",
86
99
  "Professional mode": "专业模式",
87
100
  "Full-featured Agent for every scenario": "功能完整的 Agent ,适用于所有场景",
88
101
  "All standard capabilities, with code-based planning to save tokens": "拥有标准模式所有功能,通过代码实现规划,节省 Token ",
@@ -180,6 +193,8 @@ const ZH = {
180
193
  "Open folder…": "打开文件夹…",
181
194
  "Open working directory": "打开工作目录",
182
195
  "Choose a folder to open as the working directory.": "选择一个文件夹作为工作目录。",
196
+ "Select a working directory": "选择工作目录",
197
+ "Waiting for the folder window on your desktop…": "正在等待桌面上的文件夹窗口…",
183
198
  "This folder no longer exists.": "此文件夹已不存在。",
184
199
  "{n} conversations": "{n} 个对话",
185
200
  "Recent folders": "最近文件夹",
package/web/index.html CHANGED
@@ -25,6 +25,7 @@
25
25
  <title>Neutrino</title>
26
26
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f9fafb' stroke-width='1.7'%3E%3Cellipse cx='12' cy='12' rx='9.4' ry='3.8' transform='rotate(-30 12 12)'/%3E%3Ccircle cx='12' cy='12' r='3.1'/%3E%3C/svg%3E" />
27
27
  <link rel="stylesheet" href="style.css" />
28
+ <link rel="stylesheet" href="plugins.css" />
28
29
  </head>
29
30
  <body>
30
31
  <div id="app">
@@ -135,6 +136,7 @@
135
136
  <button type="button" role="menuitemradio" aria-checked="true" data-agent-mode="standard"><span><strong data-i18n="Standard mode">Standard mode</strong><small data-i18n="Full-featured Agent for every scenario">Full-featured Agent for every scenario</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
136
137
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="ptc"><span><strong data-i18n="PTC mode">PTC mode</strong><small data-i18n="All standard capabilities, with code-based planning to save tokens">All standard capabilities, with code-based planning to save tokens</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
137
138
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="fast"><span><strong data-i18n="Fast mode">Fast mode</strong><small data-i18n="Less thinking and fewer calls for quick answers">Less thinking and fewer calls for quick answers</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
139
+ <button class="fast-mode-card" type="button" role="menuitemradio" aria-checked="false" data-agent-mode="fast_dag"><span><strong data-i18n="Fast Mode">Fast Mode</strong><small data-i18n="Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.">Dynamic task scheduling and concurrent execution shorten complex tasks while preserving reasoning capability.</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
138
140
  <button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="pro"><span><strong data-i18n="Professional mode">Professional mode</strong><small data-i18n="Assign tasks to the best-suited models so multiple models complement each other">Assign tasks to the best-suited models so multiple models complement each other</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
139
141
  </div>
140
142
  <p id="agent-mode-feedback" class="agent-mode-feedback" role="status" aria-live="polite"></p>
@@ -182,7 +184,7 @@
182
184
  </div>
183
185
  </details>
184
186
  <button id="send" class="primary" type="button" title="Send message" aria-label="Send message" data-i18n-title="Send message" data-i18n-aria-label="Send message">
185
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /></svg>
187
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /><path class="resume-icon" d="M8 5.6v12.8L19 12z" fill="currentColor" stroke="none" /></svg>
186
188
  </button>
187
189
  </div>
188
190
  <div class="composer-hint">
@@ -0,0 +1,10 @@
1
+ .plugin-manager-body{display:flex;flex-direction:column;gap:14px}.plugin-dialog{width:min(860px,94vw)}
2
+ .plugin-list{display:grid;gap:12px}.plugin-card{padding:16px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-1)}
3
+ .plugin-card-heading,.plugin-actions,.plugin-install{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.plugin-card-heading{justify-content:space-between}.plugin-card code{font-size:12px;color:var(--text-caption)}
4
+ .plugin-card p{margin:9px 0}.plugin-badge{font-size:11px;color:var(--accent);border:1px solid var(--accent-line);border-radius:20px;padding:3px 8px}.plugin-warning{color:var(--amber);font-size:13px}
5
+ .plugin-config,.plugin-providers{display:grid;gap:10px;margin-top:12px}.plugin-field{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:13px}.plugin-field>span{flex:1}.plugin-field select,.plugin-field input:not([type=checkbox]){max-width:55%;min-width:140px}.plugin-field textarea{max-width:60%;min-height:90px;color:var(--text);background:var(--surface-code);border:1px solid var(--border)}
6
+ .plugin-install input{flex:1}.plugin-details{white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;max-height:280px;overflow:auto}.plugin-card summary{cursor:pointer;font-size:13px;margin-top:12px}
7
+ .plugin-frame{width:100%;display:block;border:0;min-height:40px;background:transparent}.plugin-tabs,.plugin-toolbar,.plugin-header-actions,.plugin-menu{display:flex;flex-wrap:wrap;gap:7px}.plugin-tabs:empty,.plugin-toolbar:empty,.plugin-menu:empty{display:none}.plugin-tabs{padding:8px 22px}.plugin-tabs .control{font-size:12px}.plugin-toolbar{padding:0 0 7px}.plugin-sidebar{padding:8px 16px}.plugin-sidebar:empty{display:none}
8
+ .plugin-panel{margin:8px 22px;padding:12px;background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-md)}.plugin-panel-content{margin-top:10px}.plugin-skill-select{max-width:260px;font-size:12px}.plugin-composer-target{width:100%}
9
+ .composer-card{border-radius:var(--neutrino-composer-radius,var(--radius-xl))}.message{border-radius:var(--neutrino-message-radius,0)}
10
+ @media(max-width:640px){.plugin-field{align-items:flex-start;flex-direction:column}.plugin-field input:not([type=checkbox]),.plugin-field select,.plugin-field textarea{max-width:100%;width:100%}.plugin-panel{margin:6px}.plugin-actions .control{font-size:12px}}