@armadra/agent 0.5.0 → 0.5.1

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.
@@ -68,12 +68,21 @@ export function createKeyDispatch(deps) {
68
68
  const current = editor.getText();
69
69
  editor.setText(current.trim() === "" ? last : `${last}\n${current}`);
70
70
  };
71
- const cyclePermission = () => {
72
- const session = deps.session();
73
- const next = nextCycleMode(session.state.permissionMode);
74
- session.setPermissionMode(next);
71
+ const applyMode = (mode, skipped) => {
72
+ deps.session().setPermissionMode(mode);
75
73
  status.refresh();
76
- deps.showHint(`权限模式:${permissionModeLabel(next)}`);
74
+ const label = `权限模式:${permissionModeLabel(mode)}`;
75
+ deps.showHint(skipped ? `未进入 Bypass · ${label}` : label);
76
+ };
77
+ const cyclePermission = () => {
78
+ const next = nextCycleMode(deps.session().state.permissionMode);
79
+ // 取消进入 Bypass:跳过它继续循环(回到起点 Manual),而不是停在原模式——否则不进 Bypass 就绕不回去
80
+ const settle = (ok) => ok ? applyMode(next, false) : applyMode(nextCycleMode(next), true);
81
+ const answer = deps.confirmMode?.(next) ?? true;
82
+ if (typeof answer === "boolean")
83
+ settle(answer);
84
+ else
85
+ void answer.then(settle);
77
86
  };
78
87
  return (data) => {
79
88
  if (deps.inactive())
@@ -122,7 +131,9 @@ export function createKeyDispatch(deps) {
122
131
  dequeue();
123
132
  return true;
124
133
  }
125
- if (is("app.permission.cycle")) {
134
+ // 与补全共用的键(缺省 Tab)只在输入为空、补全未打开时切换模式;有输入时照常交给编辑器补全
135
+ if (is("app.permission.cycle") &&
136
+ (!is("tui.editor.complete") || (editor.isEmpty() && !editor.isCompletionOpen))) {
126
137
  cyclePermission();
127
138
  return true;
128
139
  }
@@ -6,6 +6,8 @@
6
6
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
7
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
8
  * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
9
+ * - raw 终端里 `/permission full-auto` 进入 Bypass 前文本确认(`确认进入 Bypass?[y/N]`,本次运行确认过
10
+ * 一次后不再问;启动时已是 Bypass 视为已确认)。
9
11
  * - 斜杠命令走 commands-core(与 B7 同一语义),`pick` 退化为列出候选;`/rewind` 列编号、
10
12
  * `/rewind <n> …` 执行,回到的单行原消息放回编辑行。
11
13
  */
@@ -6,6 +6,8 @@
6
6
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
7
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
8
  * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
9
+ * - raw 终端里 `/permission full-auto` 进入 Bypass 前文本确认(`确认进入 Bypass?[y/N]`,本次运行确认过
10
+ * 一次后不再问;启动时已是 Bypass 视为已确认)。
9
11
  * - 斜杠命令走 commands-core(与 B7 同一语义),`pick` 退化为列出候选;`/rewind` 列编号、
10
12
  * `/rewind <n> …` 执行,回到的单行原消息放回编辑行。
11
13
  */
@@ -13,6 +15,7 @@ import { promptImages, sessionModel } from "../../image-input.js";
13
15
  import { taskRegistryView } from "../../../agent/subagent-registry.js";
14
16
  import { currentSession, switchSession } from "../../../cli/compose-session.js";
15
17
  import { ExitCode } from "../../../cli/exit-codes.js";
18
+ import { BYPASS_LINE_QUESTION, BYPASS_MODE, BYPASS_RISK_LINES, createBypassGate, } from "../../../permissions/bypass.js";
16
19
  import { previewDisplayLines } from "../../../permissions/preview.js";
17
20
  import { AMA_VERSION } from "../../../version.js";
18
21
  import { runSlashCommand } from "../../commands-core.js";
@@ -105,6 +108,12 @@ export async function runLineMode(runtime, context, options = {}) {
105
108
  const model = session.state.model;
106
109
  io.stdout(`ama ${AMA_VERSION} · ${model?.provider}/${model?.id} · /help 查看命令,Ctrl+D 退出\n`);
107
110
  }
111
+ // /permission full-auto:进入 Bypass 前文本确认一次(管道里没有这一步,命令本身就是显式选择)
112
+ commands.confirmPermissionMode = createBypassGate(() => {
113
+ ed.hide();
114
+ io.stdout(`\n${BYPASS_RISK_LINES.map((l) => ` ${l}\n`).join("")}`);
115
+ return ed.ask(BYPASS_LINE_QUESTION, "n").then((answer) => answer === "y");
116
+ }, session.state.permissionMode === BYPASS_MODE);
108
117
  runtime.approvals.setUiBroker({
109
118
  ask: (request, signal) => new Promise((resolve) => {
110
119
  if (signal.aborted)
@@ -43,6 +43,7 @@ function askSelect(r, prompt) {
43
43
  keybindings: r.keys,
44
44
  maxVisible: 10,
45
45
  filterable: prompt.filterable === true,
46
+ ...(prompt.numberKeys === true ? { numberKeys: true } : {}),
46
47
  onSelect: (item) => {
47
48
  finish(item.label);
48
49
  done(item);
@@ -57,7 +58,9 @@ function askSelect(r, prompt) {
57
58
  body.addChild(list);
58
59
  body.addChild(new Text(r.theme.fg("dim", prompt.filterable === true
59
60
  ? "输入过滤 · ↑↓ 选择 · Enter 确认 · Esc 取消"
60
- : "↑↓ 选择 · Enter 确认 · Esc 取消")));
61
+ : prompt.numberKeys === true
62
+ ? `↑↓ 选择 · Enter 确认 · 1-${prompt.items.length} 直接选 · Esc 取消`
63
+ : "↑↓ 选择 · Enter 确认 · Esc 取消")));
61
64
  tui.start();
62
65
  tui.setFocus(list);
63
66
  });
@@ -221,6 +224,7 @@ export function createStartupUi(options = {}) {
221
224
  lines: [cwd, ...shown],
222
225
  items: TRUST_CHOICES.map(({ value, label }) => ({ value, label })),
223
226
  selected: "once",
227
+ numberKeys: true,
224
228
  });
225
229
  const choice = TRUST_CHOICES.find((c) => c.value === picked?.value);
226
230
  return choice?.answer ?? { trusted: false, remember: false };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 进入 Bypass permissions(`full-auto`)前的确认(docs/permissions.md「进入 Bypass」)。
3
+ *
4
+ * - 交互界面里的切换(Tab / Shift+Tab 循环、`/permission` 选择器、`/permission full-auto` 命令)
5
+ * 进入 Bypass 前问一次;本次运行内确认过之后不再问;启动时已是 Bypass(命令行
6
+ * `--permission-mode full-auto`、配置、profile)视为已确认——那是用户的显式选择。
7
+ * - 离开 Bypass、切到其它模式都不问。
8
+ * - 只管「要不要问」与记住确认;怎么问(对话框 / 行式 y/N)由各界面提供 `ask`。
9
+ */
10
+ import type { PermissionMode } from "./types.js";
11
+ export declare const BYPASS_MODE: PermissionMode;
12
+ export declare const BYPASS_CONFIRM_TITLE = "\u8FDB\u5165 Bypass permissions\uFF1F";
13
+ /** 确认框正文:Bypass 意味着什么(一两条关键风险)。 */
14
+ export declare const BYPASS_RISK_LINES: readonly string[];
15
+ /** 选项:缺省选中「取消」。 */
16
+ export declare const BYPASS_CHOICES: readonly [{
17
+ readonly label: "进入 Bypass";
18
+ readonly keys: "y";
19
+ }, {
20
+ readonly label: "取消";
21
+ readonly keys: "n Esc";
22
+ }];
23
+ export declare const BYPASS_DEFAULT_CHOICE = 1;
24
+ /** 行式界面的问句(单键 y/N,缺省 N)。 */
25
+ export declare const BYPASS_LINE_QUESTION = "\u786E\u8BA4\u8FDB\u5165 Bypass\uFF1F[y/N] ";
26
+ /** 不用问时同步返回 true(切换立即生效);要问时返回等用户回答的 Promise。 */
27
+ export type BypassGate = (mode: PermissionMode) => boolean | Promise<boolean>;
28
+ /**
29
+ * 返回「切到 `mode` 前要不要放行」的闸门:不是 Bypass 直接放行;Bypass 首次调用 `ask`,
30
+ * 确认后本闸门内不再问。`confirmed` 为 true 表示启动时已在 Bypass(视为已确认)。
31
+ */
32
+ export declare function createBypassGate(ask: () => Promise<boolean>, confirmed?: boolean): BypassGate;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 进入 Bypass permissions(`full-auto`)前的确认(docs/permissions.md「进入 Bypass」)。
3
+ *
4
+ * - 交互界面里的切换(Tab / Shift+Tab 循环、`/permission` 选择器、`/permission full-auto` 命令)
5
+ * 进入 Bypass 前问一次;本次运行内确认过之后不再问;启动时已是 Bypass(命令行
6
+ * `--permission-mode full-auto`、配置、profile)视为已确认——那是用户的显式选择。
7
+ * - 离开 Bypass、切到其它模式都不问。
8
+ * - 只管「要不要问」与记住确认;怎么问(对话框 / 行式 y/N)由各界面提供 `ask`。
9
+ */
10
+ export const BYPASS_MODE = "full-auto";
11
+ export const BYPASS_CONFIRM_TITLE = "进入 Bypass permissions?";
12
+ /** 确认框正文:Bypass 意味着什么(一两条关键风险)。 */
13
+ export const BYPASS_RISK_LINES = [
14
+ "所有工具调用都不再询问:写文件、执行命令、联网直接放行",
15
+ "只有危险命令仍会询问,deny 规则照常生效;只建议在一次性沙箱、容器里用",
16
+ ];
17
+ /** 选项:缺省选中「取消」。 */
18
+ export const BYPASS_CHOICES = [
19
+ { label: "进入 Bypass", keys: "y" },
20
+ { label: "取消", keys: "n Esc" },
21
+ ];
22
+ export const BYPASS_DEFAULT_CHOICE = 1;
23
+ /** 行式界面的问句(单键 y/N,缺省 N)。 */
24
+ export const BYPASS_LINE_QUESTION = "确认进入 Bypass?[y/N] ";
25
+ /**
26
+ * 返回「切到 `mode` 前要不要放行」的闸门:不是 Bypass 直接放行;Bypass 首次调用 `ask`,
27
+ * 确认后本闸门内不再问。`confirmed` 为 true 表示启动时已在 Bypass(视为已确认)。
28
+ */
29
+ export function createBypassGate(ask, confirmed = false) {
30
+ let ok = confirmed;
31
+ return (mode) => {
32
+ if (mode !== BYPASS_MODE || ok)
33
+ return true;
34
+ return ask().then((answer) => (ok = answer));
35
+ };
36
+ }
@@ -36,7 +36,7 @@ export declare const DEFAULT_KEYBINDINGS: {
36
36
  readonly "app.exit": readonly ["ctrl+d"];
37
37
  readonly "app.message.followUp": readonly ["alt+enter"];
38
38
  readonly "app.message.dequeue": readonly ["alt+up"];
39
- readonly "app.permission.cycle": readonly ["shift+tab"];
39
+ readonly "app.permission.cycle": readonly ["shift+tab", "tab"];
40
40
  readonly "app.tools.expand": readonly ["ctrl+o"];
41
41
  readonly "app.model.select": readonly ["ctrl+l"];
42
42
  readonly "app.thinking.select": readonly ["ctrl+t"];
@@ -38,7 +38,7 @@ export const DEFAULT_KEYBINDINGS = {
38
38
  "app.exit": ["ctrl+d"],
39
39
  "app.message.followUp": ["alt+enter"],
40
40
  "app.message.dequeue": ["alt+up"],
41
- "app.permission.cycle": ["shift+tab"],
41
+ "app.permission.cycle": ["shift+tab", "tab"],
42
42
  "app.tools.expand": ["ctrl+o"],
43
43
  "app.model.select": ["ctrl+l"],
44
44
  "app.thinking.select": ["ctrl+t"],
@@ -30,6 +30,7 @@
30
30
  - 状态栏显示显示名:`mode:Auto`;`Bypass permissions` 标黄。
31
31
  - `/permission` 不带参数打开选择器:标题 `Mode`,每项「显示名 + 一行说明」,右侧是数字快捷键 1–6,当前模式打勾,配置里的缺省模式标 `Default`,Auto 标 `Recommended`。line 模式 `/permission` 打印同样的列表。
32
32
  - `Shift+Tab` 循环:Manual → Accept edits → Plan → Auto → Bypass permissions → Manual。`Allowlist only` 不在循环里,只能显式选。
33
+ - **进入 Bypass**:交互界面里切到 Bypass(Tab / Shift+Tab 循环、`/permission` 选择器、`/permission full-auto`)先弹确认框,缺省选中「取消」;循环时取消则跳过 Bypass 回到 Manual,选择器与命令取消则保持原模式。本次运行确认过一次后不再问。命令行 `--permission-mode full-auto`、用户级配置与 profile 指定的不弹(那是显式选择);line 模式问 `确认进入 Bypass?[y/N]`;管道输入、RPC `set_permission_mode`、ACP `session/set_mode` 由调用方负责,不弹。详见 [tui.md](tui.md)「进入 Bypass」。
33
34
 
34
35
  ### 审批对话框的来源标注
35
36
 
package/docs/tui.md CHANGED
@@ -111,23 +111,23 @@ Accept edits claude-opus-5-5 medium | Ctx 3.0% | proj ⎇ main 5ae9e54 (+12,
111
111
 
112
112
  ## 按键
113
113
 
114
- | 按键 | 作用 |
115
- | -------------------- | --------------------------------------------------------------------- |
116
- | Enter | 发送;运行中 = steer(插到当前轮) |
117
- | Alt+Enter | 运行中排到本轮之后(followUp);空闲时等同 Enter |
118
- | Shift+Enter / Ctrl+J | 换行 |
119
- | Esc | 中断:排队的消息回填到输入框,然后停止当前运行;补全打开时先关补全 |
120
- | Esc Esc(空闲) | 输入框为空:打开回滚列表(同 `/rewind`);有字:清空并存进输入历史 |
121
- | Alt+↑ | 取回最后一条排队消息 |
122
- | Shift+Tab | 循环权限模式 Manual → Accept edits → Plan → Auto → Bypass permissions |
123
- | Ctrl+O | 展开 / 折叠工具输出与思考块 |
124
- | Ctrl+L / Ctrl+T | 选择模型 / 思考级别 |
125
- | Ctrl+G | 底部信息行 两行(full)↔ 一行(compact),只影响本会话 |
126
- | Ctrl+V | 粘贴剪贴板里的图片:存进数据目录,光标处插入 `@<路径>`(同 `/paste`) |
127
- | Ctrl+C | 清空输入;输入为空时 1.5 秒内再按一次退出(退出码 130) |
128
- | Ctrl+D | 输入为空时退出 |
129
- | Tab | 补全 |
130
- | ↑ / ↓ | 单行时浏览历史(`<数据目录>/history`,500 条) |
114
+ | 按键 | 作用 |
115
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | Enter | 发送;运行中 = steer(插到当前轮) |
117
+ | Alt+Enter | 运行中排到本轮之后(followUp);空闲时等同 Enter |
118
+ | Shift+Enter / Ctrl+J | 换行 |
119
+ | Esc | 中断:排队的消息回填到输入框,然后停止当前运行;补全打开时先关补全 |
120
+ | Esc Esc(空闲) | 输入框为空:打开回滚列表(同 `/rewind`);有字:清空并存进输入历史 |
121
+ | Alt+↑ | 取回最后一条排队消息 |
122
+ | Shift+Tab / Tab | 循环权限模式 Manual → Accept edits → Plan → Auto → Bypass permissions(Tab 只在输入为空、补全未打开时,否则仍是补全;进入 Bypass 前确认,见下文「进入 Bypass」) |
123
+ | Ctrl+O | 展开 / 折叠工具输出与思考块 |
124
+ | Ctrl+L / Ctrl+T | 选择模型 / 思考级别 |
125
+ | Ctrl+G | 底部信息行 两行(full)↔ 一行(compact),只影响本会话 |
126
+ | Ctrl+V | 粘贴剪贴板里的图片:存进数据目录,光标处插入 `@<路径>`(同 `/paste`) |
127
+ | Ctrl+C | 清空输入;输入为空时 1.5 秒内再按一次退出(退出码 130) |
128
+ | Ctrl+D | 输入为空时退出 |
129
+ | Tab | 补全 |
130
+ | ↑ / ↓ | 单行时浏览历史(`<数据目录>/history`,500 条) |
131
131
 
132
132
  按键可在 `~/.config/ama/keybindings.json` 覆盖,键是动作 id(`app.interrupt`、`app.rewind`、`app.message.followUp`、`app.statusLine.toggle`、`app.paste.image`、`tui.editor.newLine` ……),值是按键或按键数组,空数组表示禁用。`app.rewind` 是空闲时双击的那个键(缺省 Esc,两次间隔 ≤ 800 ms)。
133
133
 
@@ -189,6 +189,45 @@ Accept edits claude-opus-5-5 medium | Ctx 3.0% | proj ⎇ main 5ae9e54 (+12,
189
189
  - `/tasks`:子 Agent 任务列表;`/agents`:可用的子 Agent 类型(见下文「子 Agent」)。
190
190
  - `/paste`:同 `Ctrl+V`。
191
191
 
192
+ ## 进入 Bypass
193
+
194
+ 切到 Bypass permissions(`full-auto`)之前底部弹一个确认框,样式同审批对话框(边框黄):
195
+
196
+ ```
197
+ ╭─ 进入 Bypass permissions? ──────────────────────────────────────────────────╮
198
+ │ 所有工具调用都不再询问:写文件、执行命令、联网直接放行 │
199
+ │ 只有危险命令仍会询问,deny 规则照常生效;只建议在一次性沙箱、容器里用 │
200
+ │ │
201
+ │ 1. 进入 Bypass y │
202
+ │ › 2. 取消 n Esc │
203
+ │ │
204
+ │ ↑↓ 选择 · Enter 确认 · 1-2 直接选 · Esc 取消 │
205
+ ╰──────────────────────────────────────────────────────────────────────────────╯
206
+ ```
207
+
208
+ - 触发:Tab / Shift+Tab 循环到 Bypass、`/permission` 选择器选 Bypass、`/permission full-auto`。缺省选中「取消」:↑↓ 移动、Enter 确认、`1` / `2` 或 `y` / `n` 直选,Esc / Ctrl+C 取消(Ctrl+C 在这里不算退出的第一下)。
209
+ - 取消:循环时**跳过 Bypass 回到循环起点 Manual**(底部提示「未进入 Bypass · 权限模式:Manual」),这样不进 Bypass 也能一路 Tab 绕回去,且落点总比 Auto 更严;选择器与命令取消时保持原模式(消息区「已取消,权限模式仍为 …」)。
210
+ - 本次运行确认过一次后,再切到 Bypass 不再问;启动时已是 Bypass(`--permission-mode full-auto`、配置、profile)视为已确认,不弹。
211
+ - 宽 < 56 列时去掉空行、按键提示缩短;ASCII 模式边框 `+ - |`、选中符 `>`、箭头 `^v`。帧黄金 `test/fixtures/tui/bypass-confirm-*.txt`。
212
+ - line 模式(`--no-tui`):`/permission full-auto` 先打印上面两行说明,再问 `确认进入 Bypass?[y/N]`(单键,Enter = N);管道输入没有这一步。
213
+
214
+ ## 选择与确认的按键
215
+
216
+ 所有让人做选择的地方都支持 ↑↓ 移动 + Enter 确认,原有快捷键保留:
217
+
218
+ | 位置 | 按键 |
219
+ | --------------------------------------- | -------------------------------------------------------- |
220
+ | 审批对话框(含外部 Agent 首次运行确认) | ↑↓ Enter · 1–3 · y / a / n · Esc 拒绝 · v 完整输入 |
221
+ | 计划审批框(主选项、执行模式) | ↑↓ Enter · 1–4 / 1–3 · e 编辑计划 · Esc 留在 Plan / 返回 |
222
+ | 回滚确认面板、冲突覆盖二次确认 | ↑↓ Enter · 数字直接执行 · Esc 取消 / 返回 |
223
+ | 进入 Bypass 确认 | ↑↓ Enter · 1–2 · y / n · Esc 取消 |
224
+ | 权限模式、思考级别选择器 | ↑↓ Enter · 数字直接选 · Esc 取消 |
225
+ | 模型、会话、树、任务选择器 | ↑↓ Enter · 输入过滤 · Esc 取消(可过滤的列表不用数字键) |
226
+ | 启动时信任目录 | ↑↓ Enter · 1–4 直接选 · Esc / Ctrl+C = 本次不信任 |
227
+ | CLI 子命令计费 / 写入确认(TTY) | ↑↓ Enter · 1–2 · y / n · Esc / Ctrl+C 取消(缺省取消) |
228
+
229
+ CLI 子命令(`ama providers add` / `refresh` 的写入与 `--probe` 计费确认、`ama models cache-probe`)在 TTY 下用同一套方向键选择,结束后收成一行「? 继续? 继续」并恢复终端;stdin 不是 TTY(管道、CI)时仍是文本 `继续?[y/N]`(这些命令非交互时本就要求 `--yes`),`--yes` 跳过确认。
230
+
192
231
  ## 补全
193
232
 
194
233
  - 首行行首输入 `/`:命令、提示模板(`/<名字>`)、Skill(`/skill:<名字>`)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armadra/agent",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "可独立使用、可嵌入 Armadra 的编码 / 协调 Agent",
5
5
  "type": "module",
6
6
  "keywords": [