@visual-z/xeno 0.3.0 → 0.5.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/CHANGELOG.md +19 -0
- package/README.md +55 -16
- package/docs/app-server.md +86 -0
- package/docs/install.md +1 -1
- package/docs/providers.md +20 -6
- package/package.json +3 -2
- package/src/apps/app-server.ts +28 -0
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +10 -6
- package/src/apps/completions.ts +15 -13
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/preferences.ts +20 -0
- package/src/apps/provider-connect.ts +4 -4
- package/src/apps/shell.ts +73 -50
- package/src/apps/startup-input.ts +7 -2
- package/src/apps/startup.ts +52 -19
- package/src/apps/ui/app.ts +210 -56
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/surface.ts +2 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +38 -29
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/event-validation.ts +1 -1
- package/src/core/events.ts +2 -0
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/model-preferences.ts +28 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/permissions.ts +13 -7
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +25 -1
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/settings.ts +8 -1
- package/src/core/tool-input.ts +6 -0
- package/src/runtime/auth.ts +10 -3
- package/src/runtime/catalog.ts +21 -4
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/harness-eval.ts +3 -1
- package/src/runtime/loop.ts +72 -19
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval.ts +6 -3
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +2 -2
- package/src/runtime/provider-presets.ts +1 -1
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tools.ts +11 -9
- package/src/store/resources.ts +13 -22
- package/src/store/user-settings.ts +30 -0
- package/src/tui/ui/composer.ts +3 -2
- package/src/tui/ui/control-panel.ts +25 -0
- package/src/tui/ui/control-sidebar.ts +49 -0
- package/src/tui/ui/mascot.ts +1 -1
- package/src/tui/ui/screen.ts +107 -11
- package/src/tui/ui/status.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# xeno changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
- Place the Control Panel in a persistent right sidebar, with a drawer on narrow terminals. Preserve the composer, drafts, attachments, scrolling and input focus.
|
|
6
|
+
- Save confirmed model and thinking choices for later launches, and restore the exact available model from recent sessions.
|
|
7
|
+
- Add explicit `--yolo` / `-y` runs across interactive, print and control sessions. The flag applies only to the current process; tool selection, file checks and cancellation still apply.
|
|
8
|
+
- Request the exact required verification command after shell-only changes instead of ending without the declared acceptance receipt.
|
|
9
|
+
- Preserve saved tool selections when reopening sessions, including extension-enabled sessions, while respecting explicit CLI filters and current approval rules.
|
|
10
|
+
- Keep raw terminal input continuous during startup handoff so Enter executes early commands correctly; restore the original terminal mode even if the renderer fails to mount.
|
|
11
|
+
- Prefer automatic Session version checks after reading files, while retaining explicit version receipts and conflict rejection. Keep the ten-tool coding default after separate live trials did not support reducing it.
|
|
12
|
+
|
|
13
|
+
## 0.4.0 — 2026-10-09
|
|
14
|
+
|
|
15
|
+
- Add the goal, evidence progress and Context Lens workbench through `/control`. Keep execution outcomes separate from verified acceptance and explain the sources retained in the actual model context.
|
|
16
|
+
- Share one live Session between the TUI and the experimental `xeno-control/0.1` JSONL app server or private Unix socket. Expose initialization, thread/turn/item identity, snapshots, steering, cancellation and approval; include the interface guide in the package.
|
|
17
|
+
- Report terminal state through OSC 7501 with capability detection, explicit opt-outs, private program identity and generic messages that exclude prompts and model output.
|
|
18
|
+
- Preserve cancellation when a provider returns late. Deliver TUI steering and follow-ups into remotely started turns at their original boundaries, and return undelivered drafts after interruption.
|
|
19
|
+
- Carry original request identity, command and artifact evidence, acceptance gaps and diagnostic progress across context cuts without requiring extra model bookkeeping.
|
|
20
|
+
- Honor selected provider arguments, add official Grok OAuth, and preserve raw tool-call identity, response validation and measured usage/cache accounting across native provider workflows.
|
|
21
|
+
|
|
3
22
|
## 0.3.0 — 2026-10-07
|
|
4
23
|
|
|
5
24
|
- Replace the vendored Pi terminal UI with OpenTUI: a growing composer, command palette, model and login pickers, responsive status surfaces, terminal colours and bounded message rendering.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ is ever summarized by a model, and nothing is ever rewritten in place.
|
|
|
17
17
|
messages[] frozen system prefix + handoff + this node's events
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Version **0.
|
|
20
|
+
Version **0.5.0**. The on-disk session format will still change; `0.x` is that
|
|
21
21
|
statement. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
|
|
22
22
|
|
|
23
23
|
## Install
|
|
@@ -47,6 +47,7 @@ Validation commands and current evidence are recorded in
|
|
|
47
47
|
## Harness capabilities
|
|
48
48
|
|
|
49
49
|
- One live execution per session, with cancellation, draining, and explicit completion/stop/failure outcomes.
|
|
50
|
+
- Evidence-backed goals, progress and Context Lens in `/control`, shared with the experimental JSONL app server and optional local control socket. See [`docs/app-server.md`](docs/app-server.md) for identity, events, approval, recovery and OSC 7501 status.
|
|
50
51
|
- Stable work IDs with explicit unblock, reopen, and cancel operations. Verified completion still requires fresh executed evidence.
|
|
51
52
|
- Shared schema validation and structured tool feedback. Reads return file versions; edits support an expected version and return a diff.
|
|
52
53
|
- Writing branches start from the parent's working copy, including uncommitted files, in an isolated Git worktree. Adopt continues that checkout.
|
|
@@ -106,7 +107,7 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
|
|
|
106
107
|
[--tools names] [--exclude-tools names] [--no-tools]
|
|
107
108
|
[--tool-profile coding|readonly|research|data|full]
|
|
108
109
|
[--acceptance-file <contract.json>]
|
|
109
|
-
[--allow-tools names] [--approve|--no-approve]
|
|
110
|
+
[--allow-tools names] [--yolo|-y] [--approve|--no-approve]
|
|
110
111
|
[--theme <name|path>] [--use-theme <name>] [--no-themes]
|
|
111
112
|
[--tui-mode regular|fullscreen]
|
|
112
113
|
[--skill <path>] [--no-skills|-ns]
|
|
@@ -116,6 +117,11 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
|
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
- No prompt, a tty: interactive session.
|
|
120
|
+
- `/state` shows the current request's execution outcome, command receipts,
|
|
121
|
+
verification state and diagnostic progress across segment cuts and restarts.
|
|
122
|
+
Short tasks need no `note` calls to associate evidence. Without a declared
|
|
123
|
+
acceptance contract, successful commands remain evidence rather than verified
|
|
124
|
+
task completion. Diagnostic novelty does not change the stall policy.
|
|
119
125
|
- `/acceptance` shows declared command checks and human criteria. `/accept ID`
|
|
120
126
|
records your acceptance of a human criterion; subsequent edits invalidate it.
|
|
121
127
|
Use `--acceptance-file` or a trusted project's `.xeno/acceptance.json` with
|
|
@@ -167,8 +173,8 @@ Slash commands that exist in the session (from `/help`):
|
|
|
167
173
|
```
|
|
168
174
|
/model [id] search models, or switch (cuts a node)
|
|
169
175
|
/models refresh refresh ~/.xeno/models-cache.json; preserve models.json
|
|
170
|
-
/
|
|
171
|
-
/
|
|
176
|
+
/provider [provider] connect or manage an account, API key, gateway or local server
|
|
177
|
+
/provider claude-pro|codex|github-copilot|grok
|
|
172
178
|
subscription login (first-party client credentials)
|
|
173
179
|
/logout [provider] remove a stored credential
|
|
174
180
|
/scoped-models enable, disable and order models for cycling
|
|
@@ -239,13 +245,19 @@ credentials, native cloud protocols, and explicit coverage limits.
|
|
|
239
245
|
## Credentials
|
|
240
246
|
|
|
241
247
|
Order: `--api-key`, `~/.xeno/auth.json`, `models.json` provider/model credentials,
|
|
242
|
-
then the provider's environment variables.
|
|
243
|
-
|
|
248
|
+
then the provider's environment variables. `/login` and `/connect` remain hidden
|
|
249
|
+
aliases of `/provider`. Enter confirms the selected command argument; Tab inserts it.
|
|
250
|
+
A failed explicit source does not
|
|
251
|
+
fall through to another account. `/provider codex` uses the `openai-codex` protocol;
|
|
244
252
|
ordinary OpenAI API models use `openai`.
|
|
245
253
|
|
|
246
|
-
|
|
254
|
+
`/provider grok` uses official device authorization and refreshes the account token.
|
|
255
|
+
Its model is `xai-grok/grok-build` on the official CLI inference proxy. xAI API keys
|
|
256
|
+
remain on `xai`; credentials and rates are not shared between these accounts.
|
|
257
|
+
|
|
258
|
+
Interactive sessions can start with no key and wait for `/provider`. `-p` and
|
|
247
259
|
non-tty runs exit 1 and name `--api-key` or the variable; they do not mention
|
|
248
|
-
`/
|
|
260
|
+
`/provider`.
|
|
249
261
|
|
|
250
262
|
Keys in `auth.json` may be a literal, `!cmd`, or `$VAR`. The file is written
|
|
251
263
|
mode `0600`.
|
|
@@ -253,9 +265,10 @@ mode `0600`.
|
|
|
253
265
|
## Isolation
|
|
254
266
|
|
|
255
267
|
This is not a sandbox. Tools run with the permissions of the user who
|
|
256
|
-
started xeno. `read` / `write` / `edit` stay inside the workspace root
|
|
257
|
-
|
|
258
|
-
|
|
268
|
+
started xeno. `read` / `write` / `edit` stay inside the workspace root.
|
|
269
|
+
Shell entry points (`exec_command`, `environment`, `bash`, `target`) ask
|
|
270
|
+
before they run so you can see the command. That ask is the last look at
|
|
271
|
+
what is about to happen, **not a security
|
|
259
272
|
boundary**.
|
|
260
273
|
|
|
261
274
|
For untrusted repositories, generated code you do not intend to watch, or
|
|
@@ -267,17 +280,43 @@ Project trust (`/trust`, `--approve`) only decides whether to load
|
|
|
267
280
|
It does not change what tools can do. `AGENTS.md` is loaded whether the
|
|
268
281
|
project is trusted or not.
|
|
269
282
|
|
|
270
|
-
New sessions default to the
|
|
271
|
-
write, edit, exec_command, write_stdin, history and note. `readonly`,
|
|
283
|
+
New sessions default to the ten-tool `coding` profile: read, grep, find,
|
|
284
|
+
write, edit, environment, exec_command, write_stdin, history and note. `readonly`,
|
|
272
285
|
`research` (with branch/resolve), `data` (with query) and `full` choose other
|
|
273
286
|
builtin defaults. Extensions remain available; explicit `--tools` overrides
|
|
274
|
-
the profile and exclusions apply last.
|
|
287
|
+
the profile and exclusions apply last. Default session resume keeps the frozen
|
|
288
|
+
tool selection; explicit tool flags apply at a resource checkpoint. `/reload`
|
|
289
|
+
reselects tools using the current settings.
|
|
290
|
+
Tool selection does not bypass permission checks; restoring a tool does not
|
|
291
|
+
restore approvals from the previous process.
|
|
292
|
+
|
|
293
|
+
After reading an existing file, normally omit `read_handle` and `expected_sha256`
|
|
294
|
+
when writing or editing it: Session checks the latest observed file version.
|
|
295
|
+
Pass an exact receipt only to select a specific observation; a stale version
|
|
296
|
+
still requires a fresh read. Without any prior observation, omission provides
|
|
297
|
+
no stale-version protection.
|
|
275
298
|
|
|
276
299
|
`--tools` / `--exclude-tools` / `--no-tools` remove tools from the schema
|
|
277
300
|
the model sees. `--allow-tools exec_command` lets commands run without asking, which
|
|
278
301
|
is what a CI job that has no tty has to pass explicitly. Without a tty,
|
|
279
302
|
an `ask` is a deny that names that flag.
|
|
280
303
|
|
|
304
|
+
`--yolo` (or `-y`) allows every available tool without an approval prompt for
|
|
305
|
+
the current process. It works in interactive, print, and app-server modes;
|
|
306
|
+
resumed sessions use the current invocation's choice. Tool exclusions,
|
|
307
|
+
read-only branch capabilities, input checks and file path limits still apply.
|
|
308
|
+
YOLO does not enable project trust or get saved in settings or session history.
|
|
309
|
+
Without the flag, the next launch uses the default approval rules again.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
bun run agent --yolo # interactive session from this checkout
|
|
313
|
+
bun run agent --yolo -c # continue with approvals skipped
|
|
314
|
+
bun run agent --yolo -p "run tests" # explicit approval-free automation
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The installed CLI accepts `xeno --yolo` in version 0.5.0 and later. To
|
|
318
|
+
allow only host commands, use `--allow-tools exec_command,environment`.
|
|
319
|
+
|
|
281
320
|
## Workspace data
|
|
282
321
|
|
|
283
322
|
Opening a session does not snapshot the working directory. File tools save the
|
|
@@ -399,8 +438,8 @@ Shipped, not planned:
|
|
|
399
438
|
`-e` tries a package for one invocation. Manifests, per-kind filters and trust
|
|
400
439
|
feed the same resource inventory. Normal startup stays offline.
|
|
401
440
|
See [package management](docs/packages.md).
|
|
402
|
-
- `/
|
|
403
|
-
|
|
441
|
+
- `/provider` for API keys and for Claude Pro/Max, Codex, GitHub Copilot,
|
|
442
|
+
and official Grok account flows. See Known gaps.
|
|
404
443
|
|
|
405
444
|
Images can be attached with `@path`, a dropped image path, or Ctrl+V (files,
|
|
406
445
|
image, then text). Pasted and dropped images become `[Image #N]` tags at the
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 会话控制接口与 TUI 工作台
|
|
2
|
+
|
|
3
|
+
状态:实验性 `xeno-control/0.1`。TUI 和适配器共用一个 `SessionControl`、一个实际 Session 和同一份 append-only 日志。协议借鉴 Codex App Server 的生命周期,**不声明与 Codex v2 wire format 兼容**。没有新增模型调用、第二个执行进程或独立目标数据库。
|
|
4
|
+
|
|
5
|
+
## 从源码使用
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run agent app-server schema
|
|
9
|
+
bun run agent app-server --model openai-codex/gpt-6-luna
|
|
10
|
+
bun run agent --control-socket /tmp/xeno-control-private/control.sock
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
第一条只打印版本、请求参数 schema、通知清单及限制,不加载模型凭证。第二条用 stdin/stdout 双向 JSONL;诊断在 stderr。启动会话仍采用原有的凭证、工具权限、`--session` / `--continue` 和 `--acceptance-file` 参数。提示从 `turn/start` 提交,不能作为位置参数。结构化模式不构造 TUI,也不发送 OSC。
|
|
14
|
+
|
|
15
|
+
第三条正常启动 TUI,Unix socket 连接到它当前的同一个会话。父目录必须由当前用户拥有且权限为 0700;不存在时自动创建,socket 为 0600。已有路径拒绝覆盖。这里没有 TCP、Web 面板或远程认证服务。每个适配器都是具有当前用户本地控制能力的客户端,可启动工具和答复审批。
|
|
16
|
+
|
|
17
|
+
## 初始化与恢复
|
|
18
|
+
|
|
19
|
+
每行一个 JSON 对象;省略 `jsonrpc`。请求必须有字符串或安全整数 `id`,响应返回相同 `id` 及 `result` 或 `error`。只有 `initialized` 是客户端无 id 通知。所有其他方法必须等初始化完成。
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"my-adapter","version":"0.1"}}}
|
|
23
|
+
{"method":"initialized"}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
读取 initialize 的 `result.threadId` 和 `result.streamId` 后,提交:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"id":2,"method":"thread/subscribe","params":{"threadId":"<returned-threadId>"}}
|
|
30
|
+
{"id":3,"method":"turn/start","params":{"threadId":"<returned-threadId>","input":"Inspect the failing test and report the evidence."}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
通知可以早于对应请求响应,客户端应按 request id 关联响应。`thread/subscribe` 先订阅,再返回快照;应先缓存这期间的通知,拿到快照后只应用同一 streamId 且 sequence 大于快照 sequence 的通知。sequence 是控制器生命周期内递增的通知序号,包含瞬时输出,不能作为持久日志地址。
|
|
34
|
+
|
|
35
|
+
- `threadId` 是规范化绝对 session 目录路径的 SHA-256。同路径冷恢复保持身份;移动或复制目录会改变身份,不是可携带 UUID。
|
|
36
|
+
- `streamId` 每个活控制器生成一次。重启会变化;`turnId` 等于这一轮原始 user event ID;steering 引用这个请求,不另开目标。
|
|
37
|
+
- `item/completed` 在持久 append 成功后发送 `{node,id,type}`。完整事件通过 `item/read` 读取,唯一地址是 node + event id。
|
|
38
|
+
- 断线、序号跳跃或 streamId 改变:重新初始化、订阅,消费返回快照,再分页读 `item/list` / `item/read`。可给订阅传 `after:{streamId,sequence}`;游标与当前快照不同就返回 `resyncRequired:true`。
|
|
39
|
+
- 不保存、重放瞬时文本 delta,不重放命令。控制请求不具备幂等重试保证;丢失 `turn/start` 响应时先查看当前 turn 和日志,不能盲目重发。
|
|
40
|
+
- TUI 切换 Session 后,旧 threadId 拒绝继续控制,适配器应建立新连接重新初始化。当前协议没有多会话列表、创建或切换方法。
|
|
41
|
+
|
|
42
|
+
## 方法与事件
|
|
43
|
+
|
|
44
|
+
`app-server schema` 输出的 `methods` 是实际入参验证器共用的 schema;目前不是所有响应类型的完整 JSON Schema/生成式 SDK。
|
|
45
|
+
|
|
46
|
+
| 方法 | 除 threadId 外的参数 | 结果 / 作用 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `thread/read` | 无 | 当前目标、执行、进展、上下文和待审批快照 |
|
|
49
|
+
| `thread/subscribe` | 可选 after | 订阅通知并返回快照及 resyncRequired |
|
|
50
|
+
| `turn/start` | input | 开始唯一执行,返回 turnId;忙时拒绝 |
|
|
51
|
+
| `turn/steer` | turnId, input | 排队,在原有模型/工具边界消费;最多 32 项 |
|
|
52
|
+
| `turn/interrupt` | turnId | 取消并清理当前执行;完成由 turn/completed 确认 |
|
|
53
|
+
| `approval/respond` | turnId, approvalId, choice | once / command / tool / deny;过期身份拒绝 |
|
|
54
|
+
| `acceptance/accept` | requestId, criterionId | 只在 idle 显式批准本请求已声明的人验条件 |
|
|
55
|
+
| `context/read` | focus,可选 offset / limit | messages / omitted / history 来源分页,不改模型输入 |
|
|
56
|
+
| `item/list` | 可选 after:{node,event} / limit | 当前活动路径的日志身份分页 |
|
|
57
|
+
| `item/read` | node, event | 当前活动路径上的完整日志事件 |
|
|
58
|
+
|
|
59
|
+
分页 limit 为 1–100,默认 50。切换路径后旧游标可能失效,应重新获取快照。完整单条事件超过帧上限会报错;此版本不提供大事件分块下载。
|
|
60
|
+
|
|
61
|
+
通知包括 `turn/started`、`turn/event`(原有 LoopEvent 的瞬时进展)、`item/completed`、`turn/completed`、`turn/error`、`turn/steeringQueued`、`approval/request`、`approval/resolved`、`thread/statusChanged`。每条带 threadId / streamId / sequence,活动 turn 存在时带 turnId。`turn/completed.steeringUndelivered` 明确没有消费的排队输入;客户端不能将“已入队”显示为“已执行”。provider 抛错时仍从持久日志读取 outcome,错误通知不透出原始 provider 报文。
|
|
62
|
+
|
|
63
|
+
日志是事实来源。观察者得到独立副本,修改或抛错不能改变执行。刷新 idle 快照沿原有验收观察边界重新检查工作区,可能追加失效证据;这不提交提示或调用模型。活动目标和快照游标采用同一个已发布事件边界;切段期间无法一致读取时返回刷新错误。
|
|
64
|
+
|
|
65
|
+
## 审批、断连与限制
|
|
66
|
+
|
|
67
|
+
审批是同一个权限请求;TUI 或适配器答复后另一端的待审批状态同时结束。取消清理审批承诺,不允许晚到 provider 响应覆盖取消状态或继续执行工具。stdio 是独立会话的主控制连接:EOF/关闭会拒绝审批并取消执行。Unix socket 客户端断开只撤掉该观察者,TUI 继续拥有当前审批与执行。
|
|
68
|
+
|
|
69
|
+
输入单帧上限 1 MiB,每连接最多排队 32 请求,输出缓冲上限 2 MiB,socket 最多 32 客户端;这些是传输容量假设,不是模型策略阈值。超限断开或返回有界错误。错误码:`-32600` envelope、`-32601` 未知方法、`-32602` 参数、`-32700` JSON、`-32000` 生命周期/执行冲突、`-32001` 传输容量、`-32002` 响应过大。快照本身过大时使用分页 inspector;超大目标详情需要后续协议扩展。
|
|
70
|
+
|
|
71
|
+
## TUI 与终端状态
|
|
72
|
+
|
|
73
|
+
常驻 Goal 区展示原始目标、验收状态、执行状态和观察/重试数。`/control` 在右侧栏打开同一会话的面板,保留底部输入框和草稿;窄窗口改为消息区内的右侧抽屉,尺寸不足时提示放大。F6 切换面板与编辑器焦点,Esc 回到编辑器并保留面板,Q 关闭并恢复状态侧栏。面板有焦点时,Tab 切 Goal / Progress / Context Lens,左右键切上下文来源/省略事件/handoff 恢复引用,上下键或 PageUp/PageDown 翻阅,R 刷新,I 取消,A 选择当前声明的人验条件;其它文字输入会回到原编辑器。长目标和证据换行显示,缩放保留页签、滚动和草稿。
|
|
74
|
+
|
|
75
|
+
远程开始的普通 turn 会更新当前 TUI,并可在面板取消。TUI 输入的 steering 在同一 turn 的原有边界消费,followup 等原 turn 释放后执行;远程取消也把未发送内容归还编辑器。既有隔离 skill / shell 保留各自执行入口,当前协议不新增远程 skill/shell 方法。
|
|
76
|
+
|
|
77
|
+
无验收合同保持 `unverified`;模型结束 `model_end` 不等于目标 `verified`。Progress 的观察、产物、验收、新诊断和原样重试均带日志来源;数量不是完成百分比。Context Lens 复用实际 dispatch 的投影,只过滤检查视图,不修改 frozen prefix、handoff 或消息。
|
|
78
|
+
|
|
79
|
+
OSC 7501 遵循 [Program Status Protocol](https://www.superlogical.com/rex/docs/build/program-status) 的状态、身份及 UTF-8 base64 消息语法:
|
|
80
|
+
|
|
81
|
+
- `XENO_PROGRAM_STATUS=auto`(默认):TTY 探测成功后才发送状态;未回复时只有一次探测,不发送报告。
|
|
82
|
+
- `XENO_PROGRAM_STATUS=1`:显式开启;`0`:关闭。管道始终关闭;auto 在 tmux/screen 内关闭,可用 1 显式覆盖。
|
|
83
|
+
- running → working,审批/认证/提问 → blocked + kind,取消 → idle,失败 → error。结束使用 done,同时固定消息明确是否通过目标合同;待人验保持 blocked。
|
|
84
|
+
- 不发送用户提示、模型输出、凭证或假百分比。每进程只操作自己的 `xeno-<pid>` 记录;退出清理瞬时记录,保留规范要求可留存的 done/error。
|
|
85
|
+
|
|
86
|
+
自动验证包含真实 TUI、真实 Unix socket、CLI JSONL 与 PTY 探测;不等同于作者在所有终端上的视觉验收。配对实验及真实历史语料统计见[研究报告](research/session-control-evaluation-2026-10-08.md)。
|
package/docs/install.md
CHANGED
|
@@ -78,7 +78,7 @@ xeno says when a newer version is published. It never installs one: the
|
|
|
78
78
|
notice names the command, and you run it.
|
|
79
79
|
|
|
80
80
|
```
|
|
81
|
-
xeno 0.
|
|
81
|
+
xeno 0.4.0 is available (you have 0.3.0)
|
|
82
82
|
bun add -g @visual-z/xeno@latest
|
|
83
83
|
```
|
|
84
84
|
|
package/docs/providers.md
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
# 供应商、模型目录与自定义配置
|
|
2
2
|
|
|
3
|
-
xeno 内置 Pi 0.99.1 和 OpenCode 所用 Models.dev
|
|
3
|
+
xeno 内置 Pi 0.99.1 和 OpenCode 所用 Models.dev 的固定源并集,另加 Grok 官方账户代理入口:227 个供应商、7,502 个模型条目。同一个模型在不同供应商下分别计数。目录保留能力与已知价格;未提供的价格显示未知,不用零补全。
|
|
4
4
|
|
|
5
5
|
来源固定为 Pi `1b347794e2a630e4359f2584f4eea388145d0ddf`、OpenCode `2fa3363c924c5c3e367b84a87ae478296a0ed59b`,Models.dev JSON SHA256 `7345de7358c471e78ac756aed556cd804677cd0d8748b71edc3931225440158f`。SDK 固定为 `@earendil-works/pi-ai@0.99.1`;引入它是为了复用维护中的原生协议和签名处理,避免在 xeno 重写十套 SDK。SDK 仅在 runtime;原有 TUI vendor 的版本与升级方式独立。
|
|
6
6
|
|
|
7
|
-
## 最快的接入:`/
|
|
7
|
+
## 最快的接入:`/provider`
|
|
8
8
|
|
|
9
|
-
在交互界面输入 `/
|
|
9
|
+
在交互界面输入 `/provider`(`/provider <provider>` 直达;`/login`、`/connect` 是兼容别名)。一个列表里包含全部接入方式。Enter 执行选中的命令或参数,Tab 只插入补全;文件引用只插入,不提交提示:
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- **账户**:Claude Pro/Max、ChatGPT Plus/Pro(Codex)、GitHub Copilot、Grok 官方账户,走浏览器或设备码登录。
|
|
12
12
|
- **API key**:选择目录中的供应商(Anthropic、OpenAI、Gemini、DeepSeek、OpenRouter、Kimi、智谱、硅基流动、通义、豆包等,可搜索),粘贴 key(掩码显示;`$MY_KEY` 读取环境变量)。
|
|
13
13
|
- **+ Custom endpoint…**:OpenAI Chat Completions / OpenAI Responses / Anthropic Messages / Gemini 兼容地址,或 Ollama、LM Studio。填 base URL(粘贴完整的 `…/chat/completions`、`…/v1/messages` 会自动截断)、ID 和 key 后,xeno 用这个 key 向端点列出模型(OpenAI `GET /models`;Anthropic `GET /v1/models`;Gemini `GET /v1beta/models`),勾选要启用的模型后保存。
|
|
14
14
|
- **⚙ Manage connected…**:重新列出并选择模型、更换 key 或断开。
|
|
15
15
|
|
|
16
16
|
`/model` 只显示已接入且凭据可用的模型,最近使用的排在最前。保存位置与手写配置相同:key 在 `~/.xeno/auth.json`(0600),端点和启用的模型在 `~/.xeno/models.json` 的 `providers.<id>`。
|
|
17
17
|
|
|
18
|
+
确认模型或 thinking 后会自动保存到 `~/.xeno/settings.json`,下次新会话沿用,不需要额外按 Ctrl+S。模型菜单、`/model <provider/model>`、命令面板与模型循环共用这一行为;thinking 菜单、`/thinking <level>` 与快捷键也会保存。保存失败会提示本次已生效但下次启动不能保证沿用,不会切到另一个供应商。
|
|
19
|
+
|
|
20
|
+
显式启动参数只覆盖本次运行;`--continue` / `--resume` 沿用会话日志里的模型与 thinking,恢复和扩展程序化切换不会改写个人默认。未保存默认模型的旧配置先尝试可认证的最近选择,再按目录兜底;明确的配置层级顺序保持不变。
|
|
21
|
+
|
|
22
|
+
## Grok 官方账户
|
|
23
|
+
|
|
24
|
+
输入 `/provider grok`,xeno 打开官方确认网页并显示 user code;在浏览器批准后,当前会话直接取得凭据。Esc 取消会中止轮询,取消后返回的 token 不会写入。过期时自动刷新,刷新响应未轮换 refresh token 时保留原值;失败会明确要求重新登录。
|
|
25
|
+
|
|
26
|
+
账户存为 `auth.json` 的 `xai-grok`(0600),模型身份为 `xai-grok/grok-build`,请求走 `https://cli-chat-proxy.grok.com/v1`。`xai` 仍表示 `XAI_API_KEY` 与 `https://api.x.ai/v1`;两个身份不共享凭据或计费账户。也可显式提供 `GROK_ACCESS_TOKEN`,但环境 token 没有 refresh token,不能自动刷新。账户代理地址不能覆盖到其它服务。
|
|
27
|
+
|
|
28
|
+
采用官方 RFC 8628 设备授权(`https://auth.x.ai/oauth2/device/code` 与 `/oauth2/token`);只申请推理及刷新所需 scopes,`referrer` 与 User-Agent 标识 xeno。协议所需 `X-XAI-Token-Auth` 与模型路由头遵循官方代理合同。依据:[Grok Build 企业认证](https://docs.x.ai/build/enterprise)、[官方授权源码](https://github.com/xai-org/grok-build/tree/main/crates/codegen/xai-grok-login/src)、[官方代理调用示例](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-shell/README.md)。
|
|
29
|
+
|
|
30
|
+
`grok-build` 是官方代理别名,不能把 API 模型的价格、上下文长度或可选 effort 套给它。当前保守暴露文本/工具调用,价格未知、effort 选择关闭;实际账户权限由官方服务决定。设备授权启动已实测返回 200,授权后的推理/刷新有受控 HTTP 验证;没有替用户完成真实账户批准或调用付费模型。
|
|
31
|
+
|
|
18
32
|
## 目录和选择
|
|
19
33
|
|
|
20
34
|
```sh
|
|
@@ -125,7 +139,7 @@ xeno models --refresh
|
|
|
125
139
|
| `anthropic-messages` | Anthropic、支持 Messages 的网关;API key 或已有 Claude OAuth |
|
|
126
140
|
| `openai-completions` | Chat Completions 兼容端点 |
|
|
127
141
|
| `openai-responses` | OpenAI Responses;每次发完整投影,`store: false` |
|
|
128
|
-
| `openai-codex-responses` | ChatGPT 订阅端点;`/
|
|
142
|
+
| `openai-codex-responses` | ChatGPT 订阅端点;`/provider codex` 或 `OPENAI_CODEX_ACCESS_TOKEN` |
|
|
129
143
|
| `azure-openai-responses` | Azure key + resource/base URL;支持 `AZURE_RESOURCE_NAME` / `AZURE_API_KEY` 别名 |
|
|
130
144
|
| `google-generative-ai` | Gemini 原生 API,Google/Gemini API key |
|
|
131
145
|
| `google-vertex` | Vertex API key,或 `auth: "ambient"` + ADC;项目和位置必需 |
|
|
@@ -135,7 +149,7 @@ xeno models --refresh
|
|
|
135
149
|
|
|
136
150
|
Vertex 可配置 scoped `env`:`GOOGLE_APPLICATION_CREDENTIALS`、`GOOGLE_CLOUD_PROJECT`、`GOOGLE_CLOUD_LOCATION`;也接受 OpenCode 的 `GOOGLE_VERTEX_PROJECT` / `GOOGLE_VERTEX_LOCATION`。Bedrock 使用 `AWS_PROFILE`、`AWS_REGION` 等 SDK 环境设置;resource/project/location 不会被当成 API key。需要端点模板的服务必须提供对应变量,例如 Cloudflare 的 account/gateway。
|
|
137
151
|
|
|
138
|
-
旧 `auth.json` 中存于 `openai` 的 Codex OAuth 可由 `openai-codex` 兼容读取;显式原生条目优先,普通 OpenAI API key 不共享到订阅协议。新的 `/
|
|
152
|
+
旧 `auth.json` 中存于 `openai` 的 Codex OAuth 可由 `openai-codex` 兼容读取;显式原生条目优先,普通 OpenAI API key 不共享到订阅协议。新的 `/provider codex` 写入 `openai-codex`。其他 Pi 专属订阅登录流程不冒充已接入;可按协议配置 key/token。
|
|
139
153
|
|
|
140
154
|
## 可配置参数与边界
|
|
141
155
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visual-z/xeno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Segmented append-only agent log with pure context and handoff projections",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"README.md",
|
|
13
13
|
"CHANGELOG.md",
|
|
14
14
|
"docs/install.md",
|
|
15
|
-
"docs/providers.md"
|
|
15
|
+
"docs/providers.md",
|
|
16
|
+
"docs/app-server.md"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
|
18
19
|
"bun": ">=1.3.0"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
|
|
2
|
+
import { sessionControl } from "../runtime/session-control.ts";
|
|
3
|
+
import { ControlJsonl } from "./control-transport.ts";
|
|
4
|
+
import { interactiveHooks } from "./shell.ts";
|
|
5
|
+
import type { App } from "./startup.ts";
|
|
6
|
+
|
|
7
|
+
/** Owns stdin/stdout only. Startup diagnostics use stderr; terminal UI is never constructed. */
|
|
8
|
+
export async function runAppServer(app: App): Promise<void> {
|
|
9
|
+
const control = sessionControl(app.session);
|
|
10
|
+
await interactiveHooks.bindExtensions(app);
|
|
11
|
+
app.askSlot.current = request => control.ask(request);
|
|
12
|
+
const stop = () => { process.stdin.destroy(); if (control.activeTurnId) control.interrupt(control.activeTurnId); };
|
|
13
|
+
const peer = new ControlJsonl(() => control, text => {
|
|
14
|
+
if (process.stdout.writableLength + Buffer.byteLength(text) > CONTROL_SCHEMA.limits.outputBytes) stop();
|
|
15
|
+
else process.stdout.write(text);
|
|
16
|
+
}, stop, true);
|
|
17
|
+
const signal = () => { process.exitCode = 130; stop(); };
|
|
18
|
+
process.once("SIGINT", signal); process.once("SIGTERM", signal); process.stdout.on("error", stop);
|
|
19
|
+
try {
|
|
20
|
+
for await (const chunk of process.stdin) peer.feed(typeof chunk === "string" ? Buffer.from(chunk) : chunk as Uint8Array);
|
|
21
|
+
await peer.drained();
|
|
22
|
+
} finally {
|
|
23
|
+
peer.close(); stop();
|
|
24
|
+
await control.whenIdle().catch(() => {});
|
|
25
|
+
await app.session.dispose(); await app.extensions?.dispose(); delete app.askSlot.current;
|
|
26
|
+
process.off("SIGINT", signal); process.off("SIGTERM", signal); process.stdout.off("error", stop);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/apps/cli.ts
CHANGED
|
@@ -19,8 +19,16 @@ import { createStartupInput } from "./startup-input.ts";
|
|
|
19
19
|
import { runPrompt, runRepl } from "./shell.ts";
|
|
20
20
|
import { runPackageCommand } from "../runtime/package-manager.ts";
|
|
21
21
|
import { exportHtmlFile } from "./export-cli.ts";
|
|
22
|
+
import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
|
|
23
|
+
import { sessionControl } from "../runtime/session-control.ts";
|
|
24
|
+
import { listenControl } from "./control-transport.ts";
|
|
25
|
+
import { runAppServer } from "./app-server.ts";
|
|
22
26
|
|
|
23
|
-
const
|
|
27
|
+
const serverMode = process.argv[2] === "app-server";
|
|
28
|
+
if (serverMode && process.argv[3] === "schema") { console.log(JSON.stringify(CONTROL_SCHEMA, null, 2)); process.exit(0); }
|
|
29
|
+
const args = parseArgs(process.argv.slice(serverMode ? 3 : 2), { deferExtensions: true });
|
|
30
|
+
if (serverMode) { args.print = true; args.mode = "json"; if (args.prompt) throw Error("app-server receives prompts through turn/start"); }
|
|
31
|
+
let closeControl: (() => Promise<void>) | undefined;
|
|
24
32
|
const input = createStartupInput({ interactive: isInteractiveSession(args) && !args.packageCommand && !args.version && !args.exportFile && !args.listModels && !args.modelsCommand });
|
|
25
33
|
process.once("exit", input.restore);
|
|
26
34
|
try {
|
|
@@ -43,22 +51,29 @@ try {
|
|
|
43
51
|
finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
44
52
|
} else {
|
|
45
53
|
const app = await startup(args, input);
|
|
46
|
-
if (args.
|
|
47
|
-
|
|
48
|
-
args.prompt = mergePrintPrompt(args.prompt,
|
|
49
|
-
await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
|
|
50
|
-
}
|
|
51
|
-
if (args.prompt) {
|
|
54
|
+
if (args.controlSocket) closeControl = await listenControl(args.controlSocket, () => sessionControl(app.session));
|
|
55
|
+
if (serverMode) {
|
|
52
56
|
input.restore();
|
|
53
|
-
await
|
|
57
|
+
await runAppServer(app);
|
|
54
58
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
if (args.print) {
|
|
60
|
+
// With a prompt already given, stdin is optional context: wait briefly for it.
|
|
61
|
+
args.prompt = mergePrintPrompt(args.prompt,
|
|
62
|
+
await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
|
|
63
|
+
}
|
|
64
|
+
if (args.prompt) {
|
|
65
|
+
input.restore();
|
|
66
|
+
await runPrompt(app, args.prompt);
|
|
67
|
+
} else {
|
|
68
|
+
input.phase("Loading conversation");
|
|
69
|
+
await runRepl(app, input.handoff);
|
|
70
|
+
}
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
} catch (error) {
|
|
60
74
|
console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;
|
|
61
75
|
} finally {
|
|
76
|
+
await closeControl?.();
|
|
62
77
|
process.off("exit", input.restore);
|
|
63
78
|
input.restore();
|
|
64
79
|
}
|
package/src/apps/commands.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type Command =
|
|
|
21
21
|
| { kind: "usage" }
|
|
22
22
|
| { kind: "state" }
|
|
23
23
|
| { kind: "context" }
|
|
24
|
+
| { kind: "control" }
|
|
24
25
|
| { kind: "acceptance" }
|
|
25
26
|
| { kind: "tasks" }
|
|
26
27
|
| { kind: "branches"; action?: "wait" | "cancel" | "status"; node?: number }
|
|
@@ -60,7 +61,7 @@ export const COMMANDS = [
|
|
|
60
61
|
{ name: "scoped-models", description: "Enable and order models for cycling", busy: "idle" },
|
|
61
62
|
{ name: "thinking", description: "Set reasoning effort", argumentHint: "[level]", busy: "idle" },
|
|
62
63
|
{ name: "settings", description: "Configure appearance, input and model behavior", argumentHint: "[key value]", busy: "idle" },
|
|
63
|
-
{ name: "
|
|
64
|
+
{ name: "provider", description: "Connect and manage providers: account, API key, gateway or local server", argumentHint: "[provider]", busy: "idle" },
|
|
64
65
|
{ name: "logout", description: "Remove a stored credential", argumentHint: "[provider]", busy: "idle" },
|
|
65
66
|
{ name: "new", description: "Start a new session", busy: "idle" },
|
|
66
67
|
{ name: "resume", description: "Search saved sessions", argumentHint: "[id]", busy: "idle" },
|
|
@@ -73,6 +74,7 @@ export const COMMANDS = [
|
|
|
73
74
|
{ name: "usage", description: "Show token usage and costs", busy: "run" },
|
|
74
75
|
{ name: "state", description: "Show log and handoff state", busy: "run" },
|
|
75
76
|
{ name: "context", description: "Explain context sources, omissions and token estimates", busy: "run" },
|
|
77
|
+
{ name: "control", description: "Inspect the goal, evidence and context; control the active turn", busy: "run" },
|
|
76
78
|
{ name: "acceptance", description: "Show declared criteria and current evidence", busy: "idle" },
|
|
77
79
|
{ name: "tasks", description: "Show work dependencies, readiness and ownership", busy: "run" },
|
|
78
80
|
{ name: "branches", description: "Inspect or join owned background work", argumentHint: "[wait|cancel|status node]", busy: "idle" },
|
|
@@ -84,7 +86,7 @@ export const COMMANDS = [
|
|
|
84
86
|
{ name: "changelog", description: "Show local release notes", busy: "run" },
|
|
85
87
|
{ name: "copy", description: "Copy the last assistant reply", busy: "run" },
|
|
86
88
|
{ name: "attachments", description: "Remove images from the current draft", busy: "run" },
|
|
87
|
-
{ name: "models", description: "Refresh the model catalog", argumentHint: "refresh", busy: "idle" },
|
|
89
|
+
{ name: "models", description: "Refresh the model catalog", argumentHint: "<refresh>", busy: "idle" },
|
|
88
90
|
{ name: "trust", description: "Set workspace configuration trust", argumentHint: "[here|parent|never]", busy: "idle" },
|
|
89
91
|
{ name: "reload", description: "Reload settings, skills, prompts, themes and models", busy: "idle" },
|
|
90
92
|
{ name: "config", description: "Configure resource sources and enablement", busy: "idle" },
|
|
@@ -94,13 +96,14 @@ export const COMMANDS = [
|
|
|
94
96
|
{ name: "exit", description: "Exit xeno", busy: "run" },
|
|
95
97
|
] as const;
|
|
96
98
|
|
|
97
|
-
export const BUILTIN_COMMAND_NAMES = COMMANDS.map((command) => command.name);
|
|
98
|
-
|
|
99
99
|
/**
|
|
100
100
|
* Hidden spellings of a listed command. Not in COMMANDS, so the `/` menu,
|
|
101
101
|
* the startup hint and completion show one entry per job.
|
|
102
102
|
*/
|
|
103
|
-
const ALIASES: Readonly<Record<string, string>> = { connect: "
|
|
103
|
+
const ALIASES: Readonly<Record<string, string>> = { login: "provider", connect: "provider" };
|
|
104
|
+
|
|
105
|
+
// Hidden spellings still own their names, so resources cannot promise an unreachable route.
|
|
106
|
+
export const BUILTIN_COMMAND_NAMES: readonly string[] = [...COMMANDS.map(command => command.name), ...Object.keys(ALIASES)];
|
|
104
107
|
|
|
105
108
|
export function commandDefinition(input: string): (typeof COMMANDS)[number] | undefined {
|
|
106
109
|
const typed = input.trim().split(/\s/, 1)[0]?.slice(1).toLowerCase() ?? "";
|
|
@@ -162,6 +165,7 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
|
|
|
162
165
|
const [head, ...rest] = text.split(/\s+/);
|
|
163
166
|
const typed = (head ?? "").toLowerCase();
|
|
164
167
|
const token = `/${ALIASES[typed.slice(1)] ?? typed.slice(1)}`;
|
|
168
|
+
if (token === "/control") return { kind: "control" };
|
|
165
169
|
const arg = rest.join(" ");
|
|
166
170
|
|
|
167
171
|
if (!commandDefinition(text)) return resolveRegistered(token, arg, text, rest.length, catalog);
|
|
@@ -191,7 +195,7 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
|
|
|
191
195
|
case "/accept": return { kind: "accept", criterion: arg };
|
|
192
196
|
case "/model":
|
|
193
197
|
return arg.length > 0 ? { kind: "model", pattern: arg } : { kind: "model" };
|
|
194
|
-
case "/
|
|
198
|
+
case "/provider":
|
|
195
199
|
return rest[0] === undefined ? { kind: "login" } : { kind: "login", provider: rest[0] };
|
|
196
200
|
case "/logout":
|
|
197
201
|
return rest[0] === undefined ? { kind: "logout" } : { kind: "logout", provider: rest[0] };
|
package/src/apps/completions.ts
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* `@` inside a word (an e-mail) or after a backslash is not a reference.
|
|
8
8
|
*/
|
|
9
9
|
import type { App } from "./startup.ts";
|
|
10
|
-
import { SUBSCRIPTION_OPTIONS } from "../runtime/oauth.ts";
|
|
10
|
+
import { SUBSCRIPTION_OPTIONS, subscriptionById } from "../runtime/oauth.ts";
|
|
11
11
|
import { supportedThinkingLevels } from "../core/thinking.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { interactiveSettingKeys } from "../core/settings.ts";
|
|
13
13
|
import { fuzzyFilter } from "../tui/fuzzy.ts";
|
|
14
|
-
import { COMMANDS } from "./commands.ts";
|
|
14
|
+
import { COMMANDS, commandDefinition } from "./commands.ts";
|
|
15
|
+
import { commandCompletionRank } from "../core/completion-action.ts";
|
|
15
16
|
import type { Completion } from "../tui/ui/composer.ts";
|
|
16
17
|
import type { ListItem } from "../tui/ui/list-model.ts";
|
|
17
18
|
|
|
@@ -33,17 +34,18 @@ export function completionAt(text: string, cursor: number, sources: CompletionSo
|
|
|
33
34
|
if (space < 0) {
|
|
34
35
|
const query = line.slice(1).toLowerCase();
|
|
35
36
|
const items = sources.commands
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
37
|
+
.map(command => ({ command, rank: commandCompletionRank(command.name, query) }))
|
|
38
|
+
.filter(item => Number.isFinite(item.rank)).sort((a, b) => a.rank - b.rank)
|
|
39
|
+
.map(({ command }) => ({ value: `/${command.name} `, label: `/${command.name}`, description: command.description }));
|
|
40
|
+
return items.length ? { kind: "command", start: 0, end: cursor, items: items.slice(0, MAX_ITEMS) } : undefined;
|
|
39
41
|
}
|
|
40
|
-
const command = line.slice(1, space);
|
|
42
|
+
const command = line.slice(1, space).toLowerCase();
|
|
41
43
|
const arg = line.slice(space + 1);
|
|
42
44
|
if (arg.includes(" ")) return undefined;
|
|
43
45
|
const values = sources.argumentValues(command);
|
|
44
46
|
if (!values.length) return undefined;
|
|
45
47
|
const items = fuzzyFilter(values, arg).slice(0, MAX_ITEMS).map(value => ({ value, label: value }));
|
|
46
|
-
return items.length ? { start: space + 1, end: cursor, items } : undefined;
|
|
48
|
+
return items.length ? { kind: "argument", start: space + 1, end: cursor, items } : undefined;
|
|
47
49
|
}
|
|
48
50
|
const at = line.lastIndexOf("@");
|
|
49
51
|
if (at < 0) return undefined;
|
|
@@ -54,23 +56,23 @@ export function completionAt(text: string, cursor: number, sources: CompletionSo
|
|
|
54
56
|
const bare = query.replace(/^"/, "");
|
|
55
57
|
const items: ListItem[] = fuzzyFilter(sources.paths, bare).slice(0, MAX_ITEMS)
|
|
56
58
|
.map(path => ({ value: `@${/\s/.test(path) ? JSON.stringify(path) : path} `, label: path }));
|
|
57
|
-
return items.length ? { start: lineStart + at, end: cursor, items } : undefined;
|
|
59
|
+
return items.length ? { kind: "path", start: lineStart + at, end: cursor, items } : undefined;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export function completionSources(app: App, paths: readonly string[]): CompletionSources {
|
|
61
|
-
const providers = [...new Set([...app.catalog.map(m => m.provider), ...SUBSCRIPTION_OPTIONS.map(p => p.id)])];
|
|
63
|
+
const providers = [...new Set([...app.catalog.map(m => m.provider).filter(provider => !subscriptionById(provider)), ...SUBSCRIPTION_OPTIONS.map(p => p.id)])];
|
|
62
64
|
const values: Record<string, () => string[]> = {
|
|
63
65
|
// Model identities belong to the searchable picker. The generic completer
|
|
64
66
|
// still handles arbitrary argument sources; this app supplies only one
|
|
65
67
|
// model selection surface, rather than a second list with different keys.
|
|
66
68
|
thinking: () => [...supportedThinkingLevels(app.providerConfig.spec)],
|
|
67
|
-
|
|
68
|
-
settings:
|
|
69
|
+
provider: () => providers, logout: () => Object.keys(app.auth).filter(provider => app.auth[provider] !== undefined),
|
|
70
|
+
settings: interactiveSettingKeys,
|
|
69
71
|
models: () => ["refresh"], trust: () => ["once", "here", "parent", "never"],
|
|
70
72
|
};
|
|
71
73
|
return {
|
|
72
74
|
commands: [...COMMANDS.filter(command => command.name !== "exit"), ...commandEntries(app)],
|
|
73
|
-
argumentValues: command => values[command]?.() ?? [],
|
|
75
|
+
argumentValues: command => values[commandDefinition(`/${command}`)?.name ?? command.toLowerCase()]?.() ?? [],
|
|
74
76
|
paths,
|
|
75
77
|
};
|
|
76
78
|
}
|