@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.
- package/README.md +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +19 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +77 -4
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +4 -2
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +139 -21
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +153 -19
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +27 -6
- package/web/app.js +201 -32
- package/web/i18n.js +15 -0
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +11 -2
package/web/LOCAL-API.md
ADDED
|
@@ -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,
|
|
15
|
-
|
|
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
|
|
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.
|
|
266
|
-
|
|
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
|
|
1633
|
-
|
|
1634
|
-
el.send.classList.toggle("is-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
el.send.setAttribute("
|
|
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
|
-
|
|
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 =
|
|
1927
|
+
button.textContent = tr("Pause");
|
|
1903
1928
|
button.addEventListener("click", () => {
|
|
1904
|
-
postAction(snapshot.key,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3387
|
-
|
|
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
|
-
|
|
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">
|
package/web/plugins.css
ADDED
|
@@ -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}}
|