@visual-z/xeno 0.4.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 +10 -0
- package/README.md +35 -8
- package/docs/app-server.md +3 -1
- package/docs/providers.md +4 -0
- package/package.json +1 -1
- package/src/apps/preferences.ts +20 -0
- package/src/apps/shell.ts +34 -30
- package/src/apps/startup-input.ts +7 -2
- package/src/apps/startup.ts +30 -10
- package/src/apps/ui/app.ts +53 -69
- package/src/core/completion-review.ts +13 -4
- package/src/core/model-preferences.ts +28 -0
- package/src/core/permissions.ts +13 -7
- package/src/core/settings.ts +1 -1
- package/src/runtime/catalog.ts +8 -1
- package/src/runtime/oauth.ts +1 -1
- 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/control-panel.ts +7 -5
- package/src/tui/ui/control-sidebar.ts +49 -0
- package/src/tui/ui/screen.ts +107 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
3
13
|
## 0.4.0 — 2026-10-09
|
|
4
14
|
|
|
5
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.
|
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
|
|
@@ -107,7 +107,7 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
|
|
|
107
107
|
[--tools names] [--exclude-tools names] [--no-tools]
|
|
108
108
|
[--tool-profile coding|readonly|research|data|full]
|
|
109
109
|
[--acceptance-file <contract.json>]
|
|
110
|
-
[--allow-tools names] [--approve|--no-approve]
|
|
110
|
+
[--allow-tools names] [--yolo|-y] [--approve|--no-approve]
|
|
111
111
|
[--theme <name|path>] [--use-theme <name>] [--no-themes]
|
|
112
112
|
[--tui-mode regular|fullscreen]
|
|
113
113
|
[--skill <path>] [--no-skills|-ns]
|
|
@@ -265,9 +265,10 @@ mode `0600`.
|
|
|
265
265
|
## Isolation
|
|
266
266
|
|
|
267
267
|
This is not a sandbox. Tools run with the permissions of the user who
|
|
268
|
-
started xeno. `read` / `write` / `edit` stay inside the workspace root
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
272
|
boundary**.
|
|
272
273
|
|
|
273
274
|
For untrusted repositories, generated code you do not intend to watch, or
|
|
@@ -279,17 +280,43 @@ Project trust (`/trust`, `--approve`) only decides whether to load
|
|
|
279
280
|
It does not change what tools can do. `AGENTS.md` is loaded whether the
|
|
280
281
|
project is trusted or not.
|
|
281
282
|
|
|
282
|
-
New sessions default to the
|
|
283
|
-
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`,
|
|
284
285
|
`research` (with branch/resolve), `data` (with query) and `full` choose other
|
|
285
286
|
builtin defaults. Extensions remain available; explicit `--tools` overrides
|
|
286
|
-
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.
|
|
287
298
|
|
|
288
299
|
`--tools` / `--exclude-tools` / `--no-tools` remove tools from the schema
|
|
289
300
|
the model sees. `--allow-tools exec_command` lets commands run without asking, which
|
|
290
301
|
is what a CI job that has no tty has to pass explicitly. Without a tty,
|
|
291
302
|
an `ask` is a deny that names that flag.
|
|
292
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
|
+
|
|
293
320
|
## Workspace data
|
|
294
321
|
|
|
295
322
|
Opening a session does not snapshot the working directory. File tools save the
|
package/docs/app-server.md
CHANGED
|
@@ -70,7 +70,9 @@ bun run agent --control-socket /tmp/xeno-control-private/control.sock
|
|
|
70
70
|
|
|
71
71
|
## TUI 与终端状态
|
|
72
72
|
|
|
73
|
-
常驻 Goal 区展示原始目标、验收状态、执行状态和观察/重试数。`/control`
|
|
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 方法。
|
|
74
76
|
|
|
75
77
|
无验收合同保持 `unverified`;模型结束 `model_end` 不等于目标 `verified`。Progress 的观察、产物、验收、新诊断和原样重试均带日志来源;数量不是完成百分比。Context Lens 复用实际 dispatch 的投影,只过滤检查视图,不修改 frozen prefix、handoff 或消息。
|
|
76
78
|
|
package/docs/providers.md
CHANGED
|
@@ -15,6 +15,10 @@ xeno 内置 Pi 0.99.1 和 OpenCode 所用 Models.dev 的固定源并集,另加
|
|
|
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
|
+
|
|
18
22
|
## Grok 官方账户
|
|
19
23
|
|
|
20
24
|
输入 `/provider grok`,xeno 打开官方确认网页并显示 user code;在浏览器批准后,当前会话直接取得凭据。Esc 取消会中止轮询,取消后返回的 token 不会写入。过期时自动刷新,刷新响应未轮换 refresh token 时保留原值;失败会明确要求重新登录。
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** User intent owns personal defaults; CLI overrides and restored/extension state only own a session. */
|
|
2
|
+
import { mergeSettings, type Settings } from "../core/settings.ts";
|
|
3
|
+
import { writeUserSettings } from "../store/resources.ts";
|
|
4
|
+
import { describeError } from "./render.ts";
|
|
5
|
+
import type { App } from "./startup.ts";
|
|
6
|
+
|
|
7
|
+
export type PreferenceOrigin = "user" | "session";
|
|
8
|
+
|
|
9
|
+
export async function commitUserSettings(app: App, patch: Settings): Promise<boolean> {
|
|
10
|
+
const ui = app.ui;
|
|
11
|
+
if (!ui) return false;
|
|
12
|
+
ui.settings = mergeSettings({ builtin: ui.settings, user: patch });
|
|
13
|
+
try { await writeUserSettings(patch); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
app.io.say(app.io.palette.yellow(` The change is active in this session, but could not save settings for the next startup: ${describeError(error)}`));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
ui.userOverrides = mergeSettings({ builtin: ui.userOverrides, user: patch });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
package/src/apps/shell.ts
CHANGED
|
@@ -58,14 +58,15 @@ import { registerSessionRoot } from "../store/session-browser.ts";
|
|
|
58
58
|
import { createSession, findSessionByPrefix, listSessions, truncationNote, sessionsDir } from "../store/sessions.ts";
|
|
59
59
|
import { exportSessionArchive, importSessionArchive } from "../store/session-archive.ts";
|
|
60
60
|
import { resolveTuiMode } from "../core/tui-mode.ts";
|
|
61
|
-
import {
|
|
61
|
+
import { modelPreferencePatch, thinkingPreferencePatch } from "../core/model-preferences.ts";
|
|
62
62
|
import { flattenTree, treeFromPoints, LEFT_BRANCH_MARK, EMPTY_TREE_MESSAGE, type TreeNode, type NodeParent, type TreePoint } from "../core/session-tree.ts";
|
|
63
63
|
import { cycle } from "../core/cycle.ts";
|
|
64
64
|
import { THINKING_LEVELS, asThinkingLevel as asEffort, supportedThinkingLevels, clampThinkingLevel } from "../core/thinking.ts";
|
|
65
65
|
import { modelKey } from "../core/session-position.ts";
|
|
66
66
|
import { defaultKeymap, listBindings } from "../core/keymap.ts";
|
|
67
|
-
import { settingPatch
|
|
68
|
-
import {
|
|
67
|
+
import { settingPatch } from "../core/settings.ts";
|
|
68
|
+
import { resolveTheme } from "../store/resources.ts";
|
|
69
|
+
import { commitUserSettings, type PreferenceOrigin } from "./preferences.ts";
|
|
69
70
|
import { colorEnabled } from "./render.ts";
|
|
70
71
|
import { createStyle, IDENTITY } from "../tui/style.ts";
|
|
71
72
|
import { lastAssistantText, toHtml } from "../core/export.ts";
|
|
@@ -144,9 +145,9 @@ async function bindExtensions(app: App, tui?: Ui): Promise<void> {
|
|
|
144
145
|
},
|
|
145
146
|
setModel: async model => {
|
|
146
147
|
if (typeof model !== "object" || !model || !("provider" in model) || !("id" in model)) return false;
|
|
147
|
-
await switchByPattern(app, String(model.provider) + "/" + String(model.id)); return app.providerConfig.spec.id === model.id && app.providerConfig.spec.provider === model.provider;
|
|
148
|
+
await switchByPattern(app, String(model.provider) + "/" + String(model.id), "session"); return app.providerConfig.spec.id === model.id && app.providerConfig.spec.provider === model.provider;
|
|
148
149
|
},
|
|
149
|
-
setThinking: async level => { await applyThinkingCommand(app, level, tui ?? null); },
|
|
150
|
+
setThinking: async level => { await applyThinkingCommand(app, level, tui ?? null, "session"); },
|
|
150
151
|
exec: (command, args, options) => app.session.runExtensionCommand(command, args, options),
|
|
151
152
|
});
|
|
152
153
|
if (!initializedExtensions.has(registry)) {
|
|
@@ -822,7 +823,8 @@ async function commitSetting(app: App, field: string, value: string, tui: Ui | n
|
|
|
822
823
|
}
|
|
823
824
|
if (key === "thinking") {
|
|
824
825
|
const effort = asEffort(value);
|
|
825
|
-
if (
|
|
826
|
+
if (effort) await applyThinking(app, effort, tui);
|
|
827
|
+
return;
|
|
826
828
|
}
|
|
827
829
|
const parsed = settingPatch(key === "status" ? "statusBar" : field, value);
|
|
828
830
|
if (parsed.diagnostics.length) { app.io.say(C.yellow(parsed.diagnostics.join("\n"))); return; }
|
|
@@ -833,12 +835,12 @@ async function commitSetting(app: App, field: string, value: string, tui: Ui | n
|
|
|
833
835
|
app.ui.theme = theme;
|
|
834
836
|
app.ui.style = colorEnabled() ? createStyle(theme.colors) : IDENTITY;
|
|
835
837
|
}
|
|
836
|
-
await
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
838
|
+
const saved = await commitUserSettings(app, parsed.settings);
|
|
839
|
+
if (saved && key === "enableskillcommands") { await reloadAppResources(app); await tui?.refreshResources(); }
|
|
840
|
+
if (saved) {
|
|
841
|
+
if (tui) tui.toast(`${field} · ${value}`, "success");
|
|
842
|
+
else app.io.say(C.dim(` ${field} ${value}`));
|
|
843
|
+
}
|
|
842
844
|
tui?.applyConfiguration();
|
|
843
845
|
if (key === "theme") tui?.followTheme();
|
|
844
846
|
tui?.redraw();
|
|
@@ -856,7 +858,7 @@ function effortSupported(spec: ModelSpec, level: Effort): boolean {
|
|
|
856
858
|
* must not cut: a cut drops the prefix cache, and thinking blocks stay
|
|
857
859
|
* valid because the model did not change.
|
|
858
860
|
*/
|
|
859
|
-
async function applyThinking(app: App, level: Effort, tui: Ui | null): Promise<boolean> {
|
|
861
|
+
async function applyThinking(app: App, level: Effort, tui: Ui | null, origin: PreferenceOrigin = "user"): Promise<boolean> {
|
|
860
862
|
const C = app.io.palette;
|
|
861
863
|
if (!effortSupported(app.providerConfig.spec, level)) {
|
|
862
864
|
app.io.say(
|
|
@@ -874,19 +876,20 @@ async function applyThinking(app: App, level: Effort, tui: Ui | null): Promise<b
|
|
|
874
876
|
if (app.ui !== undefined) {
|
|
875
877
|
app.ui.settings = { ...app.ui.settings, thinking: level };
|
|
876
878
|
}
|
|
879
|
+
if (origin === "user") await commitUserSettings(app, thinkingPreferencePatch(app.providerConfig.spec, level));
|
|
877
880
|
app.io.say(C.dim(` thinking ${level}`));
|
|
878
881
|
tui?.redraw();
|
|
879
882
|
return true;
|
|
880
883
|
}
|
|
881
884
|
|
|
882
|
-
async function applyThinkingCommand(app: App, level: string | undefined, tui: Ui | null): Promise<Outcome> {
|
|
885
|
+
async function applyThinkingCommand(app: App, level: string | undefined, tui: Ui | null, origin: PreferenceOrigin = "user"): Promise<Outcome> {
|
|
883
886
|
if (level !== undefined) {
|
|
884
887
|
const effort = asEffort(level);
|
|
885
888
|
if (effort === undefined) {
|
|
886
889
|
app.io.say(app.io.palette.yellow(" thinking must be " + THINKING_LEVELS.join("|")));
|
|
887
890
|
return "continue";
|
|
888
891
|
}
|
|
889
|
-
await applyThinking(app, effort, tui);
|
|
892
|
+
await applyThinking(app, effort, tui, origin);
|
|
890
893
|
return "continue";
|
|
891
894
|
}
|
|
892
895
|
if (tui) {
|
|
@@ -1486,7 +1489,7 @@ async function adoptCredential(app: App, ready: readonly ModelSpec[]): Promise<v
|
|
|
1486
1489
|
const got = app.credentialOf(spec);
|
|
1487
1490
|
if (!got.ok) return;
|
|
1488
1491
|
if (app.credential.ok || app.session.state.handoff !== null || app.session.state.events.some((e) => e.t === "assistant")) {
|
|
1489
|
-
await switchToModel(app, spec);
|
|
1492
|
+
await switchToModel(app, spec, undefined, "session");
|
|
1490
1493
|
return;
|
|
1491
1494
|
}
|
|
1492
1495
|
const nextConfig: ProviderConfig = {
|
|
@@ -1511,7 +1514,7 @@ async function adoptCredential(app: App, ready: readonly ModelSpec[]): Promise<v
|
|
|
1511
1514
|
app.io.say(C.green(` now ${spec.name}`) + C.dim(` ${spec.id}`));
|
|
1512
1515
|
}
|
|
1513
1516
|
|
|
1514
|
-
async function switchByPattern(app: App, pattern: string): Promise<void> {
|
|
1517
|
+
async function switchByPattern(app: App, pattern: string, origin: PreferenceOrigin = "user"): Promise<void> {
|
|
1515
1518
|
const C = app.io.palette;
|
|
1516
1519
|
const found = scopedModels([pattern], app.catalog, true), selected = found.entries[0];
|
|
1517
1520
|
if (!selected) {
|
|
@@ -1519,16 +1522,11 @@ async function switchByPattern(app: App, pattern: string): Promise<void> {
|
|
|
1519
1522
|
app.io.say(C.red(` ${diagnostic.ok ? found.diagnostics.join("; ") : diagnostic.reason}`));
|
|
1520
1523
|
return;
|
|
1521
1524
|
}
|
|
1522
|
-
await switchToModel(app, selected.model, selected.thinking);
|
|
1525
|
+
await switchToModel(app, selected.model, selected.thinking, origin);
|
|
1523
1526
|
}
|
|
1524
1527
|
|
|
1525
|
-
export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort): Promise<void> {
|
|
1528
|
+
export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort, origin: PreferenceOrigin = "user"): Promise<void> {
|
|
1526
1529
|
const C = app.io.palette;
|
|
1527
|
-
if (spec.id === app.providerConfig.spec.id && spec.provider === app.providerConfig.spec.provider) {
|
|
1528
|
-
if (thinking) await applyThinking(app, clampThinkingLevel(spec, thinking), null);
|
|
1529
|
-
app.io.say(C.dim(` already on ${spec.id}`));
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
1530
|
const got = app.credentialOf(spec);
|
|
1533
1531
|
if (!got.ok) {
|
|
1534
1532
|
app.io.say(C.red(`\n ${got.reason}\n`));
|
|
@@ -1537,6 +1535,12 @@ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort
|
|
|
1537
1535
|
}
|
|
1538
1536
|
return;
|
|
1539
1537
|
}
|
|
1538
|
+
if (spec.id === app.providerConfig.spec.id && spec.provider === app.providerConfig.spec.provider) {
|
|
1539
|
+
if (thinking) await applyThinking(app, clampThinkingLevel(spec, thinking), null, "session");
|
|
1540
|
+
if (origin === "user") await rememberModelSelection(app, spec, thinking);
|
|
1541
|
+
app.io.say(C.dim(` already on ${spec.id}`));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1540
1544
|
const nextConfig: ProviderConfig = {
|
|
1541
1545
|
spec,
|
|
1542
1546
|
maxTokens: outputTokenBudget(spec),
|
|
@@ -1554,15 +1558,15 @@ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort
|
|
|
1554
1558
|
app.providerConfig = nextConfig;
|
|
1555
1559
|
app.credential = got;
|
|
1556
1560
|
app.io.say(C.green(` now ${spec.name}`) + C.dim(` ${spec.id}`));
|
|
1557
|
-
await
|
|
1561
|
+
if (origin === "user") await rememberModelSelection(app, spec, thinking);
|
|
1558
1562
|
}
|
|
1559
1563
|
|
|
1560
|
-
/**
|
|
1561
|
-
async function
|
|
1564
|
+
/** N.8 must accept the checkpoint before a user choice can own the next startup. */
|
|
1565
|
+
async function rememberModelSelection(app: App, spec: ModelSpec, explicitThinking: Effort | undefined): Promise<void> {
|
|
1562
1566
|
if (!app.ui) return;
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1567
|
+
const effort = app.providerConfig.effort;
|
|
1568
|
+
const patch = modelPreferencePatch(spec, app.ui.settings.recentModels ?? [], effort);
|
|
1569
|
+
await commitUserSettings(app, explicitThinking === undefined ? patch : { ...patch, ...thinkingPreferencePatch(spec, effort) });
|
|
1566
1570
|
}
|
|
1567
1571
|
|
|
1568
1572
|
function sessionDirOf(app: App): string | undefined {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
type StartupStdin = Pick<NodeJS.ReadStream, "isTTY" | "isRaw" | "setRawMode" | "setEncoding" | "on" | "off" | "resume" | "pause" | "unshift">;
|
|
8
8
|
export interface StartupInput {
|
|
9
9
|
phase(name: string): void;
|
|
10
|
-
/** Pause
|
|
10
|
+
/** Pause and replay held bytes while keeping raw mode for the next input owner. */
|
|
11
11
|
handoff(): void;
|
|
12
12
|
/** Final cleanup discards unsubmitted input instead of leaking it to the shell. */
|
|
13
13
|
restore(): void;
|
|
@@ -45,9 +45,14 @@ export function createStartupInput(opts: {
|
|
|
45
45
|
stdin.off("data", receive);
|
|
46
46
|
signals.off("SIGINT", interrupt); signals.off("SIGTERM", terminate); signals.off("SIGHUP", hangup);
|
|
47
47
|
pasting = false; partial = "";
|
|
48
|
+
};
|
|
49
|
+
const restore = () => {
|
|
50
|
+
if (closed) return;
|
|
51
|
+
closed = true; release(); pending = [];
|
|
52
|
+
// D20: ownership transfer must not expose a cooked-mode gap that turns CR into LF.
|
|
53
|
+
// Final cleanup also restores termios when the next renderer never mounted.
|
|
48
54
|
stdin.setRawMode(wasRaw);
|
|
49
55
|
};
|
|
50
|
-
const restore = () => { closed = true; release(); pending = []; };
|
|
51
56
|
const cancel = (code: number) => {
|
|
52
57
|
if (!active) return;
|
|
53
58
|
restore();
|
package/src/apps/startup.ts
CHANGED
|
@@ -177,6 +177,8 @@ export interface Args {
|
|
|
177
177
|
/** `--no-builtin-tools`. False when the flag was not passed. */
|
|
178
178
|
noBuiltinTools: boolean;
|
|
179
179
|
allowTools: string[];
|
|
180
|
+
/** `--yolo` / `-y`: skip tool approval for this process only. */
|
|
181
|
+
yolo: boolean;
|
|
180
182
|
/** `--approve` true, `--no-approve` false. Omitted when neither. */
|
|
181
183
|
approve?: boolean;
|
|
182
184
|
configCommand?: boolean;
|
|
@@ -271,6 +273,7 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
271
273
|
"--no-tools", "-nt",
|
|
272
274
|
"--no-builtin-tools",
|
|
273
275
|
"--allow-tools",
|
|
276
|
+
"--yolo", "-y",
|
|
274
277
|
"--approve", "-a",
|
|
275
278
|
"--no-approve", "-na",
|
|
276
279
|
"--theme",
|
|
@@ -343,7 +346,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
343
346
|
model: "", provider: "", apiKey: "", listModels: false, modelsCommand: false, modelsRefresh: false, fresh: false,
|
|
344
347
|
noContextFiles: false, appendSystemPrompt: [],
|
|
345
348
|
systemPrompt: null,
|
|
346
|
-
excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [],
|
|
349
|
+
excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [], yolo: false,
|
|
347
350
|
print: false,
|
|
348
351
|
theme: "",
|
|
349
352
|
thinking: "",
|
|
@@ -428,6 +431,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
428
431
|
else if (a === "--no-tools" || a === "-nt") args.noTools = true;
|
|
429
432
|
else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
|
|
430
433
|
else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
|
|
434
|
+
else if (a === "--yolo" || a === "-y") args.yolo = true;
|
|
431
435
|
else if (a === "--approve" || a === "-a") args.approve = true;
|
|
432
436
|
else if (a === "--no-approve" || a === "-na") args.approve = false;
|
|
433
437
|
else if (a === "--theme") {
|
|
@@ -853,10 +857,14 @@ export function toolSelectionOf(args: Args) {
|
|
|
853
857
|
* the provider bills, and an extra field there would look like a tool
|
|
854
858
|
* argument.
|
|
855
859
|
*/
|
|
856
|
-
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
|
|
860
|
+
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
|
|
857
861
|
const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
|
|
858
|
-
|
|
859
|
-
|
|
862
|
+
// O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
|
|
863
|
+
// Explicit selection still wins; extension loading and current exclusions remain live.
|
|
864
|
+
const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
|
|
865
|
+
const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
|
|
866
|
+
(retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
|
|
867
|
+
const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) });
|
|
860
868
|
const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
|
|
861
869
|
.map(({ name, description, input_schema }) => ({ name, description, input_schema }));
|
|
862
870
|
const deferredNames = new Set(deferred.map(tool => tool.name));
|
|
@@ -909,7 +917,7 @@ export function createSessionFor(opts: {
|
|
|
909
917
|
...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
|
|
910
918
|
allow: names,
|
|
911
919
|
deferredTools: deferred,
|
|
912
|
-
rules: rulesWithAllowTools(args.allowTools),
|
|
920
|
+
rules: rulesWithAllowTools(args.allowTools, { yolo: args.yolo }),
|
|
913
921
|
sessionRules,
|
|
914
922
|
askSlot,
|
|
915
923
|
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
@@ -1097,10 +1105,19 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1097
1105
|
opts.startupPhase?.("Opening session history");
|
|
1098
1106
|
await session.start();
|
|
1099
1107
|
resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1108
|
+
const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
|
|
1109
|
+
priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
|
|
1110
|
+
const refreshResources = resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
|
|
1111
|
+
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
|
|
1112
|
+
const explicitTools = opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
|
|
1113
|
+
opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
|
|
1114
|
+
if (refreshResources || (priorCursor !== null && explicitTools)) {
|
|
1115
|
+
await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas });
|
|
1103
1116
|
}
|
|
1117
|
+
// Admission follows this app's selected declaration; current process approval rules stay intact.
|
|
1118
|
+
// Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
|
|
1119
|
+
session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
|
|
1120
|
+
session.config.tools.deferredTools = restoredTools.deferred;
|
|
1104
1121
|
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1105
1122
|
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1106
1123
|
{ sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
@@ -1455,7 +1472,7 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1455
1472
|
" [--acceptance-file contract.json]\n" +
|
|
1456
1473
|
" [--control-socket /private/directory/control.sock]\n" +
|
|
1457
1474
|
" [--no-builtin-tools]\n" +
|
|
1458
|
-
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
1475
|
+
" [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
|
|
1459
1476
|
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
1460
1477
|
" [--skill <path>] [--no-skills]\n" +
|
|
1461
1478
|
" [--prompt-template <path>] [--no-prompt-templates]\n" +
|
|
@@ -1481,7 +1498,10 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1481
1498
|
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1482
1499
|
" New sessions use coding (10 tools); full exposes default built-ins.\n" +
|
|
1483
1500
|
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1484
|
-
"
|
|
1501
|
+
" Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
|
|
1502
|
+
" --allow-tools exec_command skips asks for that tool for this process.\n" +
|
|
1503
|
+
" --yolo (-y) skips all tool approvals for this process, including resumed sessions.\n" +
|
|
1504
|
+
" Tool exclusions and path limits still apply; YOLO does not change project trust.\n" +
|
|
1485
1505
|
" --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
|
|
1486
1506
|
" --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
|
|
1487
1507
|
" --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +
|