@armadra/agent 0.9.1 → 0.9.3

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 CHANGED
@@ -5,6 +5,18 @@ English · [简体中文](CHANGELOG.zh-CN.md)
5
5
  > This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
6
6
  > [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
7
7
 
8
+ ## 0.9.3 (2026-10-22)
9
+
10
+ Version aligned with Armadra and armadra-cloud (all three ship 0.9.3); protocol constants unchanged. Nothing else was merged after 0.9.2.
11
+
12
+ ## 0.9.2 (2026-10-22)
13
+
14
+ Version aligned with Armadra and armadra-cloud (all three ship 0.9.2); protocol constants unchanged.
15
+
16
+ ### TUI
17
+
18
+ - **Interrupt-and-send moves to `Ctrl+Enter`, `Ctrl+X` stays as the fallback**: `app.message.interrupt` now defaults to `["ctrl+enter", "ctrl+x"]`; the hint line, `/help` and the `/interrupt` description show `Ctrl+Enter`. To make `Ctrl+Enter` distinguishable from Enter, the real terminal now writes the kitty keyboard protocol disambiguate flag and xterm modifyOtherKeys level 1 at start (enable sequences only, no query, so no replies mix into input) and restores them when it stops; `AMA_KEYBOARD_PROTOCOL=off` disables it. kitty, Ghostty and iTerm2 work out of the box; macOS Terminal, Windows Terminal and the xterm.js terminal node cannot tell it from Enter, so use `Ctrl+X` there. Custom `app.message.interrupt` bindings keep working. Docs: docs/guides/tui.md (#221).
19
+
8
20
  ## 0.9.1 (2026-10-21)
9
21
 
10
22
  ### ACP
@@ -4,6 +4,18 @@
4
4
 
5
5
  > 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
6
6
 
7
+ ## 0.9.3(2026-10-22)
8
+
9
+ 版本号与 Armadra、armadra-cloud 对齐(三个产品统一为 0.9.3),协议常量无变化。0.9.2 之后没有其他合入。
10
+
11
+ ## 0.9.2(2026-10-22)
12
+
13
+ 版本号与 Armadra、armadra-cloud 对齐(三个产品统一为 0.9.2),协议常量无变化。
14
+
15
+ ### TUI
16
+
17
+ - **打断并发送改为 `Ctrl+Enter`,`Ctrl+X` 作回退**:`app.message.interrupt` 缺省 `["ctrl+enter", "ctrl+x"]`;提示行、`/help` 与 `/interrupt` 说明显示 `Ctrl+Enter`。为让终端把 `Ctrl+Enter` 与 Enter 分开,真实终端启动时写入 kitty 键盘协议 disambiguate 标志与 xterm modifyOtherKeys 级别 1(只写开启序列、不查询,没有回包混进输入),停止时恢复;`AMA_KEYBOARD_PROTOCOL=off` 可关。kitty、Ghostty、iTerm2 开箱可用;macOS Terminal、Windows Terminal、xterm.js 终端节点分不出它与 Enter,用 `Ctrl+X`。自定义的 `app.message.interrupt` 继续生效。文档:docs/guides/tui.md(#221)。
18
+
7
19
  ## 0.9.1(2026-10-21)
8
20
 
9
21
  ### ACP
@@ -41,7 +41,7 @@ var AMA_VERSION;
41
41
  var init_version = __esm({
42
42
  "src/version.ts"() {
43
43
  "use strict";
44
- AMA_VERSION = true ? "0.9.1" : readPackageVersion();
44
+ AMA_VERSION = true ? "0.9.3" : readPackageVersion();
45
45
  }
46
46
  });
47
47
 
@@ -1891,7 +1891,7 @@ var init_interactive = __esm({
1891
1891
  keyHints: [
1892
1892
  "Enter send (while running = steer) Alt+Enter queue after this turn Shift+Enter / Ctrl+J newline",
1893
1893
  "Esc interrupt (queued messages go back to the editor) Alt+↑ take back the last queued message",
1894
- "Ctrl+X (while running) interrupt the current turn and send the input now, queued steers first; ui.enterWhileRunning: interrupt swaps it with Enter",
1894
+ "Ctrl+Enter (while running; Ctrl+X where the terminal cannot tell it from Enter) interrupt the current turn and send the input now, queued steers first; ui.enterWhileRunning: interrupt swaps it with Enter",
1895
1895
  "Idle Esc Esc: empty input = rewind (/rewind), with text = clear (↑ to restore)",
1896
1896
  "Shift+Tab / Tab (empty input) cycle permission mode (confirm before Bypass) Ctrl+L model Ctrl+T thinking level Ctrl+O expand tool output and thinking",
1897
1897
  "Approval: 1–3 or ↑↓ Enter to choose, y allow a allow for this session n / Esc deny v full input",
@@ -2031,7 +2031,7 @@ var init_interactive = __esm({
2031
2031
  keyHints: [
2032
2032
  "Enter 发送(运行中 = 插话) Alt+Enter 排到本轮之后 Shift+Enter / Ctrl+J 换行",
2033
2033
  "Esc 中断(排队消息回填编辑器) Alt+↑ 取回最后一条排队消息",
2034
- "Ctrl+X(运行中)打断当前回合并立即发送输入(排队的插话在前);ui.enterWhileRunning: interrupt 时与 Enter 互换",
2034
+ "Ctrl+Enter(运行中;终端分不出它与 Enter 时用 Ctrl+X)打断当前回合并立即发送输入(排队的插话在前);ui.enterWhileRunning: interrupt 时与 Enter 互换",
2035
2035
  "空闲时 Esc Esc:输入框为空 = 回滚(/rewind),有字 = 清空(↑ 取回)",
2036
2036
  "Shift+Tab / Tab(输入为空时)切换权限模式(进入 Bypass 前确认) Ctrl+L 模型 Ctrl+T 思考级别 Ctrl+O 展开工具输出与思考",
2037
2037
  "审批:1–3 或 ↑↓ Enter 选择,y 允许 a 本会话允许同类 n / Esc 拒绝 v 完整输入",
@@ -3120,7 +3120,7 @@ var init_report = __esm({
3120
3120
  agents: "Sub-agent types and external agents (installed, version)",
3121
3121
  paste: "Paste an image from the clipboard (Ctrl+V)",
3122
3122
  interruptArgs: "<text>",
3123
- interrupt: "Interrupt the current turn and send this now, queued steers first (Ctrl+X)",
3123
+ interrupt: "Interrupt the current turn and send this now, queued steers first (Ctrl+Enter)",
3124
3124
  exit: "Exit",
3125
3125
  config: "Settings panel; key=value sets one item directly (user level)",
3126
3126
  traceArgs: "[task id]",
@@ -3370,7 +3370,7 @@ Available: ${inactive.join(", ")}` : ""}`,
3370
3370
  agents: "子 Agent 类型与外部 Agent(安装状态、版本)",
3371
3371
  paste: "粘贴剪贴板里的图片(Ctrl+V)",
3372
3372
  interruptArgs: "<文本>",
3373
- interrupt: "打断当前回合并立即发送这段文字,排队的插话在前(Ctrl+X)",
3373
+ interrupt: "打断当前回合并立即发送这段文字,排队的插话在前(Ctrl+Enter)",
3374
3374
  exit: "退出",
3375
3375
  config: "设置面板;key=value 直接设一项(用户级)",
3376
3376
  traceArgs: "[任务 id]",
@@ -3758,7 +3758,7 @@ var init_config_keys = __esm({
3758
3758
  "ui.logo": "AMA logo in the startup header: auto draws it when the terminal is at least 48 columns wide, off shows only the info lines",
3759
3759
  "ui.animation": "false: the spinner stays still while running and redraws only when seconds change; the startup logo does not animate",
3760
3760
  "ui.restoreOnCancel": "When Esc interrupts a run before any reply or tool call in this turn, withdraw the turn and put the message back into the input box",
3761
- "ui.enterWhileRunning": "Enter while a run is in progress: queue (default) queues the message as a steer delivered at the next delivery point; interrupt stops the current turn and sends it at once. The app.message.interrupt key (default Ctrl+X) does the other one",
3761
+ "ui.enterWhileRunning": "Enter while a run is in progress: queue (default) queues the message as a steer delivered at the next delivery point; interrupt stops the current turn and sends it at once. The app.message.interrupt key (default Ctrl+Enter, fallback Ctrl+X) does the other one",
3762
3762
  "ui.programStatus": "Report run state to the terminal with OSC 7501 (idle / working / blocked / done / error): auto (default) sends only after the terminal answers the detection query (off inside tmux), on sends without detecting (inside tmux wrapped in passthrough; needs allow-passthrough on), off never sends. Interactive TUI only",
3763
3763
  "ui.statusLine": "Bottom info line: full shows two lines (rate line + status line), compact one; toggle at runtime with Ctrl+G or /statusline (effective from wave 5 W5-A)",
3764
3764
  "ui.language": "Interface language: auto decides from LC_ALL / LC_MESSAGES / LANG (zh* is Chinese, anything else English), zh, en; AMA_LANG and --lang override; affects the interface only, text sent to the model is always English",
@@ -3887,7 +3887,7 @@ var init_config_keys = __esm({
3887
3887
  "ui.logo": "启动头的 AMA 字符画:auto 在终端宽 48 列以上时画,off 只显示信息行",
3888
3888
  "ui.animation": "false:运行中 spinner 静止,只在秒数变化时重绘;启动字符画不播放动画",
3889
3889
  "ui.restoreOnCancel": "运行中 Esc 中断、本回合还没有任何回复或工具调用时,撤回该回合并把原消息放回输入框",
3890
- "ui.enterWhileRunning": "运行中按 Enter:queue(缺省)排队插话,等下一个投递点送达;interrupt 打断当前回合并立即发送。app.message.interrupt 键(缺省 Ctrl+X)做另一种",
3890
+ "ui.enterWhileRunning": "运行中按 Enter:queue(缺省)排队插话,等下一个投递点送达;interrupt 打断当前回合并立即发送。app.message.interrupt 键(缺省 Ctrl+Enter,回退 Ctrl+X)做另一种",
3891
3891
  "ui.programStatus": "用 OSC 7501 把运行状态报告给终端(idle / working / blocked / done / error):auto(缺省)终端回应检测查询后才发(tmux 里不发),on 不检测直接发(tmux 里经 passthrough 包裹,需要 allow-passthrough on),off 不发。只用于交互界面",
3892
3892
  "ui.statusLine": "底部信息行:full 两行(速率行 + 状态行),compact 一行;运行时 Ctrl+G 或 /statusline 切换(第五波 W5-A 起生效)",
3893
3893
  "ui.language": "界面语言:auto 按 LC_ALL / LC_MESSAGES / LANG 判断(zh* 为中文,其余英文),zh,en;AMA_LANG、--lang 覆盖;只影响界面,发给模型的文本固定英文",
@@ -41534,7 +41534,7 @@ var init_stdin_buffer = __esm({
41534
41534
  });
41535
41535
 
41536
41536
  // src/tui/terminal.ts
41537
- var BRACKETED_PASTE_ON, BRACKETED_PASTE_OFF, SHOW_CURSOR, HIDE_CURSOR, ProcessTerminal;
41537
+ var BRACKETED_PASTE_ON, BRACKETED_PASTE_OFF, KEYBOARD_ENHANCE_ON, KEYBOARD_ENHANCE_OFF, SHOW_CURSOR, HIDE_CURSOR, ProcessTerminal;
41538
41538
  var init_terminal = __esm({
41539
41539
  "src/tui/terminal.ts"() {
41540
41540
  "use strict";
@@ -41542,12 +41542,15 @@ var init_terminal = __esm({
41542
41542
  init_stdin_buffer();
41543
41543
  BRACKETED_PASTE_ON = "\x1B[?2004h";
41544
41544
  BRACKETED_PASTE_OFF = "\x1B[?2004l";
41545
+ KEYBOARD_ENHANCE_ON = "\x1B[>1u\x1B[>4;1m";
41546
+ KEYBOARD_ENHANCE_OFF = "\x1B[<u\x1B[>4m";
41545
41547
  SHOW_CURSOR = "\x1B[?25h";
41546
41548
  HIDE_CURSOR = "\x1B[?25l";
41547
41549
  ProcessTerminal = class {
41548
41550
  stdin;
41549
41551
  stdout;
41550
41552
  escTimeoutMs;
41553
+ enhancedKeys;
41551
41554
  buffer = null;
41552
41555
  wasRaw = false;
41553
41556
  dataListener = null;
@@ -41556,6 +41559,7 @@ var init_terminal = __esm({
41556
41559
  this.stdin = options.stdin ?? process.stdin;
41557
41560
  this.stdout = options.stdout ?? process.stdout;
41558
41561
  this.escTimeoutMs = options.escTimeoutMs;
41562
+ this.enhancedKeys = options.enhancedKeys ?? process.env["AMA_KEYBOARD_PROTOCOL"]?.toLowerCase() !== "off";
41559
41563
  }
41560
41564
  get columns() {
41561
41565
  return this.stdout.columns || 80;
@@ -41582,14 +41586,14 @@ var init_terminal = __esm({
41582
41586
  this.stdin.resume();
41583
41587
  this.resizeListener = onResize;
41584
41588
  this.stdout.on("resize", this.resizeListener);
41585
- this.write(BRACKETED_PASTE_ON);
41589
+ this.write(BRACKETED_PASTE_ON + (this.enhancedKeys ? KEYBOARD_ENHANCE_ON : ""));
41586
41590
  }
41587
41591
  stop() {
41588
41592
  if (this.buffer === null) return;
41589
41593
  this.buffer.flush();
41590
41594
  this.buffer.destroy();
41591
41595
  this.buffer = null;
41592
- this.write(BRACKETED_PASTE_OFF + SHOW_CURSOR);
41596
+ this.write((this.enhancedKeys ? KEYBOARD_ENHANCE_OFF : "") + BRACKETED_PASTE_OFF + SHOW_CURSOR);
41593
41597
  if (this.dataListener) this.stdin.off("data", this.dataListener);
41594
41598
  if (this.resizeListener) this.stdout.off("resize", this.resizeListener);
41595
41599
  this.dataListener = null;
@@ -42369,11 +42373,12 @@ var init_keybindings = __esm({
42369
42373
  "app.message.dequeue": ["alt+up"],
42370
42374
  /**
42371
42375
  * 运行中打断当前回合并立即发送输入框的文字(连同排队的插话);`ui.enterWhileRunning: "interrupt"` 时与
42372
- * Enter 互换(它变成排队)。缺省 `Ctrl+X`:各终端(macOS Terminal、iTerm2、tmux、Windows Terminal)都原样
42373
- * 送达 0x18,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF
42374
- * 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+<字母>` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘难按。
42376
+ * Enter 互换(它变成排队)。缺省 `Ctrl+Enter`(提示与帮助显示第一项)+ 回退 `Ctrl+X`:`ProcessTerminal` 启动时
42377
+ * 开增强键盘上报,支持的终端把 Ctrl+Enter 发成 `ESC[13;5u` / `ESC[27;5;13~`;不支持的终端(macOS Terminal、
42378
+ * Windows Terminal 稳定版等)里 Ctrl+Enter 与 Enter 同为 `\r`(或变成 `\n` = Ctrl+J 换行),这时用 `Ctrl+X`
42379
+ * (各终端原样送达 0x18,且没有别的绑定)。`Alt+Enter` 已是 followUp,`Shift+Enter` / `Ctrl+J` 是换行,不占用。
42375
42380
  */
42376
- "app.message.interrupt": ["ctrl+x"],
42381
+ "app.message.interrupt": ["ctrl+enter", "ctrl+x"],
42377
42382
  "app.permission.cycle": ["shift+tab", "tab"],
42378
42383
  "app.tools.expand": ["ctrl+o"],
42379
42384
  "app.model.select": ["ctrl+l"],
@@ -106,7 +106,7 @@ const keysEn = {
106
106
  "ui.logo": "AMA logo in the startup header: auto draws it when the terminal is at least 48 columns wide, off shows only the info lines",
107
107
  "ui.animation": "false: the spinner stays still while running and redraws only when seconds change; the startup logo does not animate",
108
108
  "ui.restoreOnCancel": "When Esc interrupts a run before any reply or tool call in this turn, withdraw the turn and put the message back into the input box",
109
- "ui.enterWhileRunning": "Enter while a run is in progress: queue (default) queues the message as a steer delivered at the next delivery point; interrupt stops the current turn and sends it at once. The app.message.interrupt key (default Ctrl+X) does the other one",
109
+ "ui.enterWhileRunning": "Enter while a run is in progress: queue (default) queues the message as a steer delivered at the next delivery point; interrupt stops the current turn and sends it at once. The app.message.interrupt key (default Ctrl+Enter, fallback Ctrl+X) does the other one",
110
110
  "ui.programStatus": "Report run state to the terminal with OSC 7501 (idle / working / blocked / done / error): auto (default) sends only after the terminal answers the detection query (off inside tmux), on sends without detecting (inside tmux wrapped in passthrough; needs allow-passthrough on), off never sends. Interactive TUI only",
111
111
  "ui.statusLine": "Bottom info line: full shows two lines (rate line + status line), compact one; toggle at runtime with Ctrl+G or /statusline (effective from wave 5 W5-A)",
112
112
  "ui.language": "Interface language: auto decides from LC_ALL / LC_MESSAGES / LANG (zh* is Chinese, anything else English), zh, en; AMA_LANG and --lang override; affects the interface only, text sent to the model is always English",
@@ -235,7 +235,7 @@ const keysZh = {
235
235
  "ui.logo": "启动头的 AMA 字符画:auto 在终端宽 48 列以上时画,off 只显示信息行",
236
236
  "ui.animation": "false:运行中 spinner 静止,只在秒数变化时重绘;启动字符画不播放动画",
237
237
  "ui.restoreOnCancel": "运行中 Esc 中断、本回合还没有任何回复或工具调用时,撤回该回合并把原消息放回输入框",
238
- "ui.enterWhileRunning": "运行中按 Enter:queue(缺省)排队插话,等下一个投递点送达;interrupt 打断当前回合并立即发送。app.message.interrupt 键(缺省 Ctrl+X)做另一种",
238
+ "ui.enterWhileRunning": "运行中按 Enter:queue(缺省)排队插话,等下一个投递点送达;interrupt 打断当前回合并立即发送。app.message.interrupt 键(缺省 Ctrl+Enter,回退 Ctrl+X)做另一种",
239
239
  "ui.programStatus": "用 OSC 7501 把运行状态报告给终端(idle / working / blocked / done / error):auto(缺省)终端回应检测查询后才发(tmux 里不发),on 不检测直接发(tmux 里经 passthrough 包裹,需要 allow-passthrough on),off 不发。只用于交互界面",
240
240
  "ui.statusLine": "底部信息行:full 两行(速率行 + 状态行),compact 一行;运行时 Ctrl+G 或 /statusline 切换(第五波 W5-A 起生效)",
241
241
  "ui.language": "界面语言:auto 按 LC_ALL / LC_MESSAGES / LANG 判断(zh* 为中文,其余英文),zh,en;AMA_LANG、--lang 覆盖;只影响界面,发给模型的文本固定英文",
@@ -20,7 +20,7 @@ export const en = {
20
20
  keyHints: [
21
21
  "Enter send (while running = steer) Alt+Enter queue after this turn Shift+Enter / Ctrl+J newline",
22
22
  "Esc interrupt (queued messages go back to the editor) Alt+↑ take back the last queued message",
23
- "Ctrl+X (while running) interrupt the current turn and send the input now, queued steers first; ui.enterWhileRunning: interrupt swaps it with Enter",
23
+ "Ctrl+Enter (while running; Ctrl+X where the terminal cannot tell it from Enter) interrupt the current turn and send the input now, queued steers first; ui.enterWhileRunning: interrupt swaps it with Enter",
24
24
  "Idle Esc Esc: empty input = rewind (/rewind), with text = clear (↑ to restore)",
25
25
  "Shift+Tab / Tab (empty input) cycle permission mode (confirm before Bypass) Ctrl+L model Ctrl+T thinking level Ctrl+O expand tool output and thinking",
26
26
  "Approval: 1–3 or ↑↓ Enter to choose, y allow a allow for this session n / Esc deny v full input",
@@ -160,7 +160,7 @@ export const zh = {
160
160
  keyHints: [
161
161
  "Enter 发送(运行中 = 插话) Alt+Enter 排到本轮之后 Shift+Enter / Ctrl+J 换行",
162
162
  "Esc 中断(排队消息回填编辑器) Alt+↑ 取回最后一条排队消息",
163
- "Ctrl+X(运行中)打断当前回合并立即发送输入(排队的插话在前);ui.enterWhileRunning: interrupt 时与 Enter 互换",
163
+ "Ctrl+Enter(运行中;终端分不出它与 Enter 时用 Ctrl+X)打断当前回合并立即发送输入(排队的插话在前);ui.enterWhileRunning: interrupt 时与 Enter 互换",
164
164
  "空闲时 Esc Esc:输入框为空 = 回滚(/rewind),有字 = 清空(↑ 取回)",
165
165
  "Shift+Tab / Tab(输入为空时)切换权限模式(进入 Bypass 前确认) Ctrl+L 模型 Ctrl+T 思考级别 Ctrl+O 展开工具输出与思考",
166
166
  "审批:1–3 或 ↑↓ Enter 选择,y 允许 a 本会话允许同类 n / Esc 拒绝 v 完整输入",
@@ -39,7 +39,7 @@ export const en = {
39
39
  agents: "Sub-agent types and external agents (installed, version)",
40
40
  paste: "Paste an image from the clipboard (Ctrl+V)",
41
41
  interruptArgs: "<text>",
42
- interrupt: "Interrupt the current turn and send this now, queued steers first (Ctrl+X)",
42
+ interrupt: "Interrupt the current turn and send this now, queued steers first (Ctrl+Enter)",
43
43
  exit: "Exit",
44
44
  config: "Settings panel; key=value sets one item directly (user level)",
45
45
  traceArgs: "[task id]",
@@ -293,7 +293,7 @@ export const zh = {
293
293
  agents: "子 Agent 类型与外部 Agent(安装状态、版本)",
294
294
  paste: "粘贴剪贴板里的图片(Ctrl+V)",
295
295
  interruptArgs: "<文本>",
296
- interrupt: "打断当前回合并立即发送这段文字,排队的插话在前(Ctrl+X)",
296
+ interrupt: "打断当前回合并立即发送这段文字,排队的插话在前(Ctrl+Enter)",
297
297
  exit: "退出",
298
298
  config: "设置面板;key=value 直接设一项(用户级)",
299
299
  traceArgs: "[任务 id]",
@@ -18,7 +18,7 @@
18
18
  * 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
19
19
  * [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
20
20
  * 视图里另认 `/tasks bg [id]`。
21
- * [打断并发送] `Ctrl+X`(`app.message.interrupt`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
21
+ * [打断并发送] `Ctrl+Enter`(`app.message.interrupt`,回退 `Ctrl+X`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
22
22
  * interrupt);外部 Agent 的驱动不能中断回合时退回排队并提示。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。
23
23
  */
24
24
  import { type Component, type Focusable, type Keybindings, type Theme } from "../../tui.js";
@@ -18,7 +18,7 @@
18
18
  * 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
19
19
  * [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
20
20
  * 视图里另认 `/tasks bg [id]`。
21
- * [打断并发送] `Ctrl+X`(`app.message.interrupt`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
21
+ * [打断并发送] `Ctrl+Enter`(`app.message.interrupt`,回退 `Ctrl+X`)= 打断该子 Agent 当前回合并立即发送(`registry.message` 的
22
22
  * interrupt);外部 Agent 的驱动不能中断回合时退回排队并提示。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。
23
23
  */
24
24
  import { msg } from "../../i18n/index.js";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
2
+ * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+Enter`,回退 `Ctrl+X`;`ui.enterWhileRunning:
3
3
  * "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
4
4
  * abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
2
+ * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+Enter`,回退 `Ctrl+X`;`ui.enterWhileRunning:
3
3
  * "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
4
4
  * abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
5
5
  */
@@ -16,7 +16,7 @@
16
16
  * [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
17
17
  * 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
18
18
  * 写进中断提示。
19
- * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
19
+ * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+Enter`,回退 `Ctrl+X`)= 打断当前回合并立即
20
20
  * 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
21
21
  * 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
22
22
  */
@@ -16,7 +16,7 @@
16
16
  * [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
17
17
  * 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
18
18
  * 写进中断提示。
19
- * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
19
+ * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+Enter`,回退 `Ctrl+X`)= 打断当前回合并立即
20
20
  * 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
21
21
  * 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
22
22
  */
@@ -14,11 +14,11 @@
14
14
  * [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
15
15
  * `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
16
16
  *
17
- * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
18
- * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
17
+ * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+Enter 打断并发送`(`ui.enterWhileRunning:
18
+ * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+Enter 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
19
19
  *
20
20
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
21
- * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
21
+ * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+Enter 立即发送`(interrupt 模式是 Enter)。
22
22
  */
23
23
  import type { SessionEvent } from "../../agent/types.js";
24
24
  import { Container, Text, type Loader, type Theme } from "../../tui.js";
@@ -33,7 +33,7 @@ export interface RunIndicatorDeps {
33
33
  render(): void;
34
34
  /** [W7-C] `app.tasks.background` 的按键标签(`Ctrl+B`);没有绑定时不提示。 */
35
35
  backgroundKey?: string;
36
- /** `app.message.interrupt` 的按键标签(`Ctrl+X`);没有绑定时不提示。 */
36
+ /** `app.message.interrupt` 的按键标签(`Ctrl+Enter`);没有绑定时不提示。 */
37
37
  interruptKey?: string;
38
38
  }
39
39
  /** 运行中 Enter 的语义(`ui.enterWhileRunning`)。 */
@@ -14,11 +14,11 @@
14
14
  * [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
15
15
  * `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
16
16
  *
17
- * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+X 打断并发送`(`ui.enterWhileRunning:
18
- * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+X 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
17
+ * [打断并发送] 输入框有字时可丢弃项最前面加 `Enter 排队 · Ctrl+Enter 打断并发送`(`ui.enterWhileRunning:
18
+ * "interrupt"` 时语义互换),顺序 `Esc 中断 · Enter 排队 · Ctrl+Enter 打断并发送 · Ctrl+B 转后台 · ↓ Agent 栏`。
19
19
  *
20
20
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
21
- * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+X 立即发送`(interrupt 模式是 Enter)。
21
+ * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`,有插话时中间加 `Ctrl+Enter 立即发送`(interrupt 模式是 Enter)。
22
22
  */
23
23
  import { msg } from "../../i18n/index.js";
24
24
  import { Container, Text } from "../../tui.js";
@@ -38,11 +38,12 @@ export declare const DEFAULT_KEYBINDINGS: {
38
38
  readonly "app.message.dequeue": readonly ["alt+up"];
39
39
  /**
40
40
  * 运行中打断当前回合并立即发送输入框的文字(连同排队的插话);`ui.enterWhileRunning: "interrupt"` 时与
41
- * Enter 互换(它变成排队)。缺省 `Ctrl+X`:各终端(macOS Terminal、iTerm2、tmux、Windows Terminal)都原样
42
- * 送达 0x18,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF
43
- * 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+<字母>` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘难按。
41
+ * Enter 互换(它变成排队)。缺省 `Ctrl+Enter`(提示与帮助显示第一项)+ 回退 `Ctrl+X`:`ProcessTerminal` 启动时
42
+ * 开增强键盘上报,支持的终端把 Ctrl+Enter 发成 `ESC[13;5u` / `ESC[27;5;13~`;不支持的终端(macOS Terminal、
43
+ * Windows Terminal 稳定版等)里 Ctrl+Enter 与 Enter 同为 `\r`(或变成 `\n` = Ctrl+J 换行),这时用 `Ctrl+X`
44
+ * (各终端原样送达 0x18,且没有别的绑定)。`Alt+Enter` 已是 followUp,`Shift+Enter` / `Ctrl+J` 是换行,不占用。
44
45
  */
45
- readonly "app.message.interrupt": readonly ["ctrl+x"];
46
+ readonly "app.message.interrupt": readonly ["ctrl+enter", "ctrl+x"];
46
47
  readonly "app.permission.cycle": readonly ["shift+tab", "tab"];
47
48
  readonly "app.tools.expand": readonly ["ctrl+o"];
48
49
  readonly "app.model.select": readonly ["ctrl+l"];
@@ -41,11 +41,12 @@ export const DEFAULT_KEYBINDINGS = {
41
41
  "app.message.dequeue": ["alt+up"],
42
42
  /**
43
43
  * 运行中打断当前回合并立即发送输入框的文字(连同排队的插话);`ui.enterWhileRunning: "interrupt"` 时与
44
- * Enter 互换(它变成排队)。缺省 `Ctrl+X`:各终端(macOS Terminal、iTerm2、tmux、Windows Terminal)都原样
45
- * 送达 0x18,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF
46
- * 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+<字母>` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘难按。
44
+ * Enter 互换(它变成排队)。缺省 `Ctrl+Enter`(提示与帮助显示第一项)+ 回退 `Ctrl+X`:`ProcessTerminal` 启动时
45
+ * 开增强键盘上报,支持的终端把 Ctrl+Enter 发成 `ESC[13;5u` / `ESC[27;5;13~`;不支持的终端(macOS Terminal、
46
+ * Windows Terminal 稳定版等)里 Ctrl+Enter 与 Enter 同为 `\r`(或变成 `\n` = Ctrl+J 换行),这时用 `Ctrl+X`
47
+ * (各终端原样送达 0x18,且没有别的绑定)。`Alt+Enter` 已是 followUp,`Shift+Enter` / `Ctrl+J` 是换行,不占用。
47
48
  */
48
- "app.message.interrupt": ["ctrl+x"],
49
+ "app.message.interrupt": ["ctrl+enter", "ctrl+x"],
49
50
  "app.permission.cycle": ["shift+tab", "tab"],
50
51
  "app.tools.expand": ["ctrl+o"],
51
52
  "app.model.select": ["ctrl+l"],
@@ -2,7 +2,7 @@
2
2
  * 终端抽象(设计 §12.2、§12.3、§12.8)。[B4]
3
3
  *
4
4
  * - `Terminal`:TUI 只依赖这个接口——尺寸、写入、输入回调、尺寸变化回调。
5
- * - `ProcessTerminal`:真实 TTY。raw 模式、括号粘贴 `?2004`、stdout `resize`(SIGWINCH);
5
+ * - `ProcessTerminal`:真实 TTY。raw 模式、括号粘贴 `?2004`、增强键盘上报(Ctrl+Enter 可区分)、stdout `resize`(SIGWINCH);
6
6
  * 输入经 `StdinBuffer` 切分,括号粘贴以 `ESC[200~正文ESC[201~` 一次交给 onInput。
7
7
  * 停止时关括号粘贴、显示光标、恢复 cooked 模式;**不清屏**(回滚保留对话)。
8
8
  * - `MemoryTerminal`:测试用。记录每次写入,并用 `VirtualScreen` 还原屏幕与回滚。
@@ -19,17 +19,27 @@ export interface Terminal {
19
19
  }
20
20
  export declare const BRACKETED_PASTE_ON = "\u001B[?2004h";
21
21
  export declare const BRACKETED_PASTE_OFF = "\u001B[?2004l";
22
+ /**
23
+ * 增强键盘上报(只写不查,没有回包):kitty 键盘协议 disambiguate 标志(入栈 `CSI > 1 u`,出栈 `CSI < u`)与 xterm
24
+ * modifyOtherKeys 级别 1(`CSI > 4;1 m`,复位 `CSI > 4 m`)。用来让终端把 Ctrl+Enter 发成 `ESC[13;5u` /
25
+ * `ESC[27;5;13~`,不然它和 Enter 同为 `\r`。不支持的终端忽略这两条;tmux 只在 `extended-keys` 开着时转发。
26
+ */
27
+ export declare const KEYBOARD_ENHANCE_ON = "\u001B[>1u\u001B[>4;1m";
28
+ export declare const KEYBOARD_ENHANCE_OFF = "\u001B[<u\u001B[>4m";
22
29
  export declare const SHOW_CURSOR = "\u001B[?25h";
23
30
  export declare const HIDE_CURSOR = "\u001B[?25l";
24
31
  export interface ProcessTerminalOptions {
25
32
  stdin?: NodeJS.ReadStream;
26
33
  stdout?: NodeJS.WriteStream;
27
34
  escTimeoutMs?: number;
35
+ /** 开增强键盘上报;缺省开,环境变量 `AMA_KEYBOARD_PROTOCOL=off` 关。 */
36
+ enhancedKeys?: boolean;
28
37
  }
29
38
  export declare class ProcessTerminal implements Terminal {
30
39
  private readonly stdin;
31
40
  private readonly stdout;
32
41
  private readonly escTimeoutMs;
42
+ private readonly enhancedKeys;
33
43
  private buffer;
34
44
  private wasRaw;
35
45
  private dataListener;
@@ -2,7 +2,7 @@
2
2
  * 终端抽象(设计 §12.2、§12.3、§12.8)。[B4]
3
3
  *
4
4
  * - `Terminal`:TUI 只依赖这个接口——尺寸、写入、输入回调、尺寸变化回调。
5
- * - `ProcessTerminal`:真实 TTY。raw 模式、括号粘贴 `?2004`、stdout `resize`(SIGWINCH);
5
+ * - `ProcessTerminal`:真实 TTY。raw 模式、括号粘贴 `?2004`、增强键盘上报(Ctrl+Enter 可区分)、stdout `resize`(SIGWINCH);
6
6
  * 输入经 `StdinBuffer` 切分,括号粘贴以 `ESC[200~正文ESC[201~` 一次交给 onInput。
7
7
  * 停止时关括号粘贴、显示光标、恢复 cooked 模式;**不清屏**(回滚保留对话)。
8
8
  * - `MemoryTerminal`:测试用。记录每次写入,并用 `VirtualScreen` 还原屏幕与回滚。
@@ -12,12 +12,20 @@ import { StdinBuffer } from "./stdin-buffer.js";
12
12
  import { VirtualScreen } from "./vt-screen.js";
13
13
  export const BRACKETED_PASTE_ON = "\x1b[?2004h";
14
14
  export const BRACKETED_PASTE_OFF = "\x1b[?2004l";
15
+ /**
16
+ * 增强键盘上报(只写不查,没有回包):kitty 键盘协议 disambiguate 标志(入栈 `CSI > 1 u`,出栈 `CSI < u`)与 xterm
17
+ * modifyOtherKeys 级别 1(`CSI > 4;1 m`,复位 `CSI > 4 m`)。用来让终端把 Ctrl+Enter 发成 `ESC[13;5u` /
18
+ * `ESC[27;5;13~`,不然它和 Enter 同为 `\r`。不支持的终端忽略这两条;tmux 只在 `extended-keys` 开着时转发。
19
+ */
20
+ export const KEYBOARD_ENHANCE_ON = "\x1b[>1u\x1b[>4;1m";
21
+ export const KEYBOARD_ENHANCE_OFF = "\x1b[<u\x1b[>4m";
15
22
  export const SHOW_CURSOR = "\x1b[?25h";
16
23
  export const HIDE_CURSOR = "\x1b[?25l";
17
24
  export class ProcessTerminal {
18
25
  stdin;
19
26
  stdout;
20
27
  escTimeoutMs;
28
+ enhancedKeys;
21
29
  buffer = null;
22
30
  wasRaw = false;
23
31
  dataListener = null;
@@ -26,6 +34,8 @@ export class ProcessTerminal {
26
34
  this.stdin = options.stdin ?? process.stdin;
27
35
  this.stdout = options.stdout ?? process.stdout;
28
36
  this.escTimeoutMs = options.escTimeoutMs;
37
+ this.enhancedKeys =
38
+ options.enhancedKeys ?? process.env["AMA_KEYBOARD_PROTOCOL"]?.toLowerCase() !== "off";
29
39
  }
30
40
  get columns() {
31
41
  return this.stdout.columns || 80;
@@ -52,7 +62,7 @@ export class ProcessTerminal {
52
62
  this.stdin.resume();
53
63
  this.resizeListener = onResize;
54
64
  this.stdout.on("resize", this.resizeListener);
55
- this.write(BRACKETED_PASTE_ON);
65
+ this.write(BRACKETED_PASTE_ON + (this.enhancedKeys ? KEYBOARD_ENHANCE_ON : ""));
56
66
  }
57
67
  stop() {
58
68
  if (this.buffer === null)
@@ -60,7 +70,7 @@ export class ProcessTerminal {
60
70
  this.buffer.flush();
61
71
  this.buffer.destroy();
62
72
  this.buffer = null;
63
- this.write(BRACKETED_PASTE_OFF + SHOW_CURSOR);
73
+ this.write((this.enhancedKeys ? KEYBOARD_ENHANCE_OFF : "") + BRACKETED_PASTE_OFF + SHOW_CURSOR);
64
74
  if (this.dataListener)
65
75
  this.stdin.off("data", this.dataListener);
66
76
  if (this.resizeListener)
package/dist/tui.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  export { CURSOR_MARKER, isFocusable } from "./tui/component.js";
9
9
  export type * from "./tui/component.js";
10
10
  export { TUI, SYNC_BEGIN, SYNC_END, WRITE_CHUNK_SIZE, MIN_RENDER_INTERVAL_MS, type TuiOptions, type OverlayHandle, type InputListener, type RenderStats, } from "./tui/tui.js";
11
- export { ProcessTerminal, MemoryTerminal, BRACKETED_PASTE_ON, BRACKETED_PASTE_OFF, SHOW_CURSOR, HIDE_CURSOR, type Terminal, type ProcessTerminalOptions, type MemoryTerminalOptions, } from "./tui/terminal.js";
11
+ export { ProcessTerminal, MemoryTerminal, BRACKETED_PASTE_ON, KEYBOARD_ENHANCE_ON, KEYBOARD_ENHANCE_OFF, BRACKETED_PASTE_OFF, SHOW_CURSOR, HIDE_CURSOR, type Terminal, type ProcessTerminalOptions, type MemoryTerminalOptions, } from "./tui/terminal.js";
12
12
  export { VirtualScreen, type ScreenModes } from "./tui/vt-screen.js";
13
13
  export { StdinBuffer, defaultEscTimeout, type StdinBufferOptions } from "./tui/stdin-buffer.js";
14
14
  export { parseKey, matchesKey, normalizeKeyId, isPasteData, unwrapPaste, isPrintableText, PASTE_START, PASTE_END, type KeyEvent, } from "./tui/keys.js";
package/dist/tui.js CHANGED
@@ -8,7 +8,7 @@
8
8
  export { CURSOR_MARKER, isFocusable } from "./tui/component.js";
9
9
  // ---- B4 组件库再导出 ----
10
10
  export { TUI, SYNC_BEGIN, SYNC_END, WRITE_CHUNK_SIZE, MIN_RENDER_INTERVAL_MS, } from "./tui/tui.js";
11
- export { ProcessTerminal, MemoryTerminal, BRACKETED_PASTE_ON, BRACKETED_PASTE_OFF, SHOW_CURSOR, HIDE_CURSOR, } from "./tui/terminal.js";
11
+ export { ProcessTerminal, MemoryTerminal, BRACKETED_PASTE_ON, KEYBOARD_ENHANCE_ON, KEYBOARD_ENHANCE_OFF, BRACKETED_PASTE_OFF, SHOW_CURSOR, HIDE_CURSOR, } from "./tui/terminal.js";
12
12
  export { VirtualScreen } from "./tui/vt-screen.js";
13
13
  export { StdinBuffer, defaultEscTimeout } from "./tui/stdin-buffer.js";
14
14
  export { parseKey, matchesKey, normalizeKeyId, isPasteData, unwrapPaste, isPrintableText, PASTE_START, PASTE_END, } from "./tui/keys.js";
@@ -120,7 +120,7 @@ Trade-offs: the status bar shows the latest hit rate (the session total lives in
120
120
  | Key | Effect |
121
121
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
122
  | Enter | Send; while running = steer (queued until the next delivery point: the end of this model reply or of this tool batch) |
123
- | Ctrl+X | While running, interrupt and send now: stop the current turn (model stream and running tools; tools record "aborted by user") and start a new turn with the input at once, queued steers first; with an empty input only the queued steers are sent now. When idle, same as Enter. Swapped with Enter under `ui.enterWhileRunning: "interrupt"` |
123
+ | Ctrl+Enter / Ctrl+X | While running, interrupt and send now: stop the current turn (model stream and running tools; tools record "aborted by user") and start a new turn with the input at once, queued steers first; with an empty input only the queued steers are sent now. When idle, same as Enter. Swapped with Enter under `ui.enterWhileRunning: "interrupt"` |
124
124
  | Alt+Enter | While running, queue after this turn (followUp); when idle, same as Enter |
125
125
  | Shift+Enter / Ctrl+J | New line |
126
126
  | Esc | Interrupt: queued messages go back into the input box, then the current run stops (with foreground sub-agent tasks; background tasks keep running, as the hint says); closes completion first when it is open |
@@ -142,7 +142,7 @@ Keys can be overridden in `~/.config/ama/keybindings.json`: keys are action ids
142
142
 
143
143
  ### Interrupt and send now
144
144
 
145
- While running, Enter **queues** by default (steer): the message waits for the next delivery point, which can take a long time while the model writes a long reply or a tool runs a five-minute command. To change course right away, write the message and press **`Ctrl+X`** (action `app.message.interrupt`):
145
+ While running, Enter **queues** by default (steer): the message waits for the next delivery point, which can take a long time while the model writes a long reply or a tool runs a five-minute command. To change course right away, write the message and press **`Ctrl+Enter`** (action `app.message.interrupt`; use the fallback `Ctrl+X` where the terminal cannot tell it from Enter):
146
146
 
147
147
  - The current turn stops at once: the model stream is cut, running tools finish as interrupted (every tool call has exactly one result, recorded as "aborted by user"), and the interrupted reply stays in the session under the abort rules. Then, **without pressing Enter again**, the text is sent as the user message of a new turn (`origin: "interrupt"` in the session, `↳ interrupt` in the message area).
148
148
  - Queued steers come along: they are joined in front of the text in queue order (blank line between); messages queued after this turn (Alt+Enter) stay queued and are delivered after the new turn as usual.
@@ -150,9 +150,23 @@ While running, Enter **queues** by default (steer): the message waits for the ne
150
150
  - Nothing happens while completion, an approval dialog or a picker is open; slash commands still run as commands; with an empty input only the queued steers are sent now (a hint says when there is nothing to send).
151
151
  - The request prefix does not change: the new turn's request starts with every message of the interrupted request, so the cache keeps hitting.
152
152
  - Esc is unchanged: it still interrupts and puts queued messages back into the input box (see interrupt to withdraw below); it never sends.
153
- - To make Enter interrupt instead: set `ui.enterWhileRunning` to `interrupt` in `/config` (applies at once); Enter then interrupts and sends and `Ctrl+X` queues.
153
+ - To make Enter interrupt instead: set `ui.enterWhileRunning` to `interrupt` in `/config` (applies at once); Enter then interrupts and sends and `Ctrl+Enter` (and `Ctrl+X`) queues.
154
154
 
155
- Why `Ctrl+X` by default: macOS Terminal, iTerm2, tmux and Windows Terminal all pass it through and nothing else is bound to it; `Ctrl+Enter` is indistinguishable from Enter without the kitty keyboard protocol, `Ctrl+S` may be swallowed by XOFF flow control, `Alt+Enter` is already followUp, `Alt+letter` sends no Meta by default on macOS, and `Ctrl+]` is awkward on non-US layouts. Rebind it in `keybindings.json`.
155
+ Why `Ctrl+Enter` by default, with `Ctrl+X` as the fallback: the request was a single modifier plus Enter, and `Alt+Enter` is already followUp while `Shift+Enter` / `Ctrl+J` are newline, so `Ctrl+Enter` is the only combination left without a conflict. Most terminals send it as the same `\r` as Enter by default, so at start ama writes (without querying, so there is no reply) the kitty keyboard protocol disambiguate flag (`CSI > 1 u`) and xterm modifyOtherKeys level 1 (`CSI > 4;1 m`), and restores them when the terminal stops (exit); terminals that do not support them ignore the sequences. Supporting terminals send Ctrl+Enter as `ESC[13;5u` or `ESC[27;5;13~`, which ama has long been able to parse. In terminals that cannot tell them apart, Ctrl+Enter is still Enter (or is read as Ctrl+J, a newline); use `Ctrl+X` there: every terminal passes 0x18 through and nothing else is bound to it. Both keys are in the default binding, so there is nothing to switch; the hint line and help show the first one (`Ctrl+Enter`). To turn the enhanced reporting off, set the environment variable `AMA_KEYBOARD_PROTOCOL=off`. Whatever you set for `app.message.interrupt` in `keybindings.json` wins (`["ctrl+x"]` restores the old behavior).
156
+
157
+ Compatibility (from each terminal's documented protocol support; not hand-tested in every terminal):
158
+
159
+ | Terminal | Enhanced keyboard protocol | Ctrl+Enter | Needs Ctrl+X |
160
+ | ------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------- | ------------ |
161
+ | kitty | kitty keyboard protocol | works | no |
162
+ | Ghostty | kitty keyboard protocol | works | no |
163
+ | iTerm2 (3.5+) | kitty keyboard protocol, modifyOtherKeys | works | no |
164
+ | WezTerm | kitty keyboard protocol, off by default (`enable_kitty_keyboard = true`) | off by default, works once enabled | by default |
165
+ | VS Code terminal | depends on version and settings | not guaranteed | yes |
166
+ | macOS Terminal | none | does not work (same as Enter) | yes |
167
+ | Windows Terminal | none in stable | does not work (sends LF, i.e. Ctrl+J newline) | yes |
168
+ | Armadra canvas terminal node (xterm.js 6.0) | no kitty / modifyOtherKeys | does not work | yes |
169
+ | tmux | forwarded when the outer terminal supports it and `extended-keys on` | depends on the outer terminal and config | otherwise |
156
170
 
157
171
  In line mode (`--no-tui`), typing `/interrupt <text>` while running does the same (when idle it is an ordinary prompt); the TUI accepts the command too. Over RPC it is `interrupt: true` on `prompt` / `steer`, see [rpc.md](../reference/rpc.md).
158
172
 
@@ -417,7 +431,7 @@ t2 explore · running 1m05s · 3 turns · ↑12k ↓3.4k · Esc back · /tasks s
417
431
  - The body follows live: for ama sub-agents it shows every message and tool call of the sub-session (rendered like the message area); when the sub-session handle has been released (at most 16 are kept) or the session was resumed, the sub-session file is loaded read-only and live events are attached when the task runs again. External agents (claude / codex / ACP) show the live output held in this process's memory (text, thinking, tool start / end, turns, notices; at most 2000 items / 1 MB, never written to disk); after ama restarts only one line remains, saying to use the original CLI's resume <session id> for the full text.
418
432
  - With an empty input box: `↑` / PgUp scroll up (pausing follow, with "follow paused · End to resume" at the bottom), `↓` / PgDn scroll down, End (or `f` while paused) resumes following; `←` `→` switch to the previous / next task; Esc returns to the main screen. With text in the input box, Esc clears it first.
419
433
  - Enter sends the input to this sub-agent (recorded in the sub-session as a user message with `origin: "direct"`, see [session-format.md](../../reference/session-format.md), Chinese): ama sub-agent running → delivered when its current turn ends; external agent running or task still queued → continued after this run ends; finished → continued in the background (like `task_ctl send`; the main session receives the `<task-notification>` as usual when it completes). A line at the bottom reports the result. The parent session's model does not know you talked to the sub-agent directly; the result comes back through the completion notification.
420
- - `Ctrl+X` interrupts this sub-agent and sends now: an ama sub-agent that is running stops its current turn (tools finish as interrupted) and starts a new turn with the message at once; the task completes and notifies the main session as usual. An external agent whose driver can interrupt a single turn (ACP `session/cancel`, Claude stream-json interrupt, Codex `turn/interrupt`) is interrupted and its next turn starts with the message (and any messages queued before it); when it cannot (oneshot, host drivers) or the task is still waiting in the concurrency pool, the message is queued instead with a hint; a finished task continues in the background as with Enter. Swapped with Enter under `ui.enterWhileRunning: "interrupt"`. The main session is not affected.
434
+ - `Ctrl+Enter` (fallback `Ctrl+X`) interrupts this sub-agent and sends now: an ama sub-agent that is running stops its current turn (tools finish as interrupted) and starts a new turn with the message at once; the task completes and notifies the main session as usual. An external agent whose driver can interrupt a single turn (ACP `session/cancel`, Claude stream-json interrupt, Codex `turn/interrupt`) is interrupted and its next turn starts with the message (and any messages queued before it); when it cannot (oneshot, host drivers) or the task is still waiting in the concurrency pool, the message is queued instead with a hint; a finished task continues in the background as with Enter. Swapped with Enter under `ui.enterWhileRunning: "interrupt"`. The main session is not affected.
421
435
  - Esc in the view interrupts nothing: it only goes back. To stop the task use `/tasks stop <id>`; to move it to the background use `Ctrl+B` or `/tasks bg [id]`. Both work in the view's input box (the only commands the view accepts).
422
436
  - When the viewed task waits for approval the title says "awaiting approval", and the approval dialog pops up over the view as usual (with the `[task:<type>]` origin); if its approval is docked in the bar, opening the view pops it up.
423
437
 
@@ -508,7 +522,7 @@ The interactive interface reports its run state to the terminal with the [OSC 75
508
522
  ## In tmux / Armadra terminal nodes
509
523
 
510
524
  - Bracketed paste: enabled at startup; pasted multi-line content enters the input box as a whole (folded into a paste placeholder with the line count beyond 10 lines or 1 000 characters), and an Enter right after a paste sends directly, which suits writes from external programs.
511
- - Apart from program status detection (see the previous section; none inside tmux), terminal capabilities are not queried, and mouse and the Kitty keyboard protocol are not enabled, so no replies get mixed into input; tmux ≥ 3.4 passes synchronized output through, and older versions display fine too.
525
+ - Apart from program status detection (see the previous section; none inside tmux), terminal capabilities are not queried, and mouse is not enabled, so no replies get mixed into input; the Kitty keyboard protocol and modifyOtherKeys are only switched on by writing the enable sequence, never queried (this makes Ctrl+Enter distinguishable; `AMA_KEYBOARD_PROTOCOL=off` turns it off); tmux ≥ 3.4 passes synchronized output through, and older versions display fine too.
512
526
  - tmux's default prefix `C-b` is taken by the tmux client: to move tasks to the background press `C-b C-b` (`send-prefix` passes it through), or `↓` into the agent bar and press `b`; entering the bar uses `↓`, which the prefix does not affect.
513
527
  - When the window size changes the last screen is redrawn in full; history in the scrollback is unaffected.
514
528
  - Automatic fallback: non-TTY, `TERM=dumb`, `--no-tui` or a failed terminal initialization use line mode, with the same commands and approval prompts.
@@ -114,7 +114,7 @@ Accept edits | shift+tab 切换 claude-opus-5-5 medium | Ctx 3.0% 8.2k/272k a
114
114
  | 按键 | 作用 |
115
115
  | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
116
  | Enter | 发送;运行中 = steer(排队,等下一个投递点:本次模型回复结束或本批工具结束) |
117
- | Ctrl+X | 运行中打断并立即发送:中止当前回合(模型流与正在跑的工具,工具记「aborted by user」),立刻以输入框的文字开新回合,排队的插话拼在前面;输入为空时只把排队的插话立即送出。空闲时等同 Enter。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换 |
117
+ | Ctrl+Enter / Ctrl+X | 运行中打断并立即发送:中止当前回合(模型流与正在跑的工具,工具记「aborted by user」),立刻以输入框的文字开新回合,排队的插话拼在前面;输入为空时只把排队的插话立即送出。空闲时等同 Enter。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换 |
118
118
  | Alt+Enter | 运行中排到本轮之后(followUp);空闲时等同 Enter |
119
119
  | Shift+Enter / Ctrl+J | 换行 |
120
120
  | Esc | 中断:排队的消息回填到输入框,然后停止当前运行(连带前台子 Agent 任务;后台任务不受影响,提示里写明);补全打开时先关补全 |
@@ -136,7 +136,7 @@ Accept edits | shift+tab 切换 claude-opus-5-5 medium | Ctx 3.0% 8.2k/272k a
136
136
 
137
137
  ### 打断并立即发送
138
138
 
139
- 运行中按 Enter 缺省是**排队**(steer):消息等下一个投递点才送达——模型在长篇输出、或工具在跑一个 5 分钟的命令时要等很久。要立刻改方向,在输入框写好后按 **`Ctrl+X`**(动作 `app.message.interrupt`):
139
+ 运行中按 Enter 缺省是**排队**(steer):消息等下一个投递点才送达——模型在长篇输出、或工具在跑一个 5 分钟的命令时要等很久。要立刻改方向,在输入框写好后按 **`Ctrl+Enter`**(动作 `app.message.interrupt`;终端分不出它与 Enter 时按回退键 `Ctrl+X`):
140
140
 
141
141
  - 当前回合立即中止:模型流断开,正在执行的工具按中断收尾(每个工具调用恰有一个结果,记「aborted by user」),被打断的回复按中断规则留在会话里;随后**不用再按 Enter**,这段文字直接作为新回合的用户消息发出(会话里 `origin: "interrupt"`,消息区标 `↳ 打断`)。
142
142
  - 已排队的插话一起带上:按入队顺序拼在这段文字前面(空行分隔);排到本轮之后的(Alt+Enter)留在队列,新回合结束后照常送达。
@@ -144,9 +144,23 @@ Accept edits | shift+tab 切换 claude-opus-5-5 medium | Ctx 3.0% 8.2k/272k a
144
144
  - 补全打开、审批框或选择器打开时不触发;斜杠命令照常当命令执行;输入为空时只把排队的插话立即送出(没有可发的内容时提示一行)。
145
145
  - 中断的请求前缀不变:新回合的请求以被打断那次请求的全部消息为前缀,缓存照常命中。
146
146
  - Esc 不变:仍是中断并把排队消息回填输入框(中断即撤回见下文),不会自动发送。
147
- - 想让 Enter 直接打断:`/config` 里把 `ui.enterWhileRunning` 设为 `interrupt`(立即生效),这时 Enter 打断并发送、`Ctrl+X` 排队。
147
+ - 想让 Enter 直接打断:`/config` 里把 `ui.enterWhileRunning` 设为 `interrupt`(立即生效),这时 Enter 打断并发送、`Ctrl+Enter`(和 `Ctrl+X`)排队。
148
148
 
149
- 缺省键选 `Ctrl+X` 的原因:macOS Terminal、iTerm2、tmux、Windows Terminal 都原样送达,且没有别的绑定;`Ctrl+Enter` 不开 kitty 键盘协议时与 Enter 无法区分,`Ctrl+S` 可能被 XOFF 流控吃掉,`Alt+Enter` 已是 followUp,`Alt+字母` 在 macOS 缺省不送 Meta,`Ctrl+]` 在非美式键盘上难按。可在 `keybindings.json` 改。
149
+ 缺省键选 `Ctrl+Enter`、回退 `Ctrl+X` 的原因:要「一个修饰键 + 回车」,而 `Alt+Enter` 已是 followUp、`Shift+Enter` / `Ctrl+J` 是换行,`Ctrl+Enter` 是剩下唯一不冲突的组合。多数终端默认把它和 Enter 发成同一个 `\r`,所以 ama 启动时向终端写入(不查询、没有回包)kitty 键盘协议的 disambiguate 标志(`CSI > 1 u`)和 xterm modifyOtherKeys 级别 1(`CSI > 4;1 m`),终端停止(退出)时恢复;不支持的终端直接忽略。支持的终端把 Ctrl+Enter 发成 `ESC[13;5u` 或 `ESC[27;5;13~`,ama 早就能解析。分不出的终端里 Ctrl+Enter 仍是 Enter(或被当作 Ctrl+J 换行),这时用 `Ctrl+X`:各终端原样送达 0x18,且没有别的绑定。两个键都在缺省绑定里,不用切换;提示行与帮助显示第一个键(`Ctrl+Enter`)。想关掉增强上报:环境变量 `AMA_KEYBOARD_PROTOCOL=off`。自己改 `keybindings.json` 里的 `app.message.interrupt` 后以你的为准(写 `["ctrl+x"]` 即恢复旧行为)。
150
+
151
+ 兼容表(按各终端文档的协议支持整理,未在每个终端逐一实测):
152
+
153
+ | 终端 | 增强键盘协议 | Ctrl+Enter | 用 Ctrl+X |
154
+ | ------------------------------------ | ------------------------------------------------------------- | --------------------------------- | --------- |
155
+ | kitty | kitty 键盘协议 | 可用 | 不需要 |
156
+ | Ghostty | kitty 键盘协议 | 可用 | 不需要 |
157
+ | iTerm2(3.5+) | kitty 键盘协议、modifyOtherKeys | 可用 | 不需要 |
158
+ | WezTerm | kitty 键盘协议,缺省关(`enable_kitty_keyboard = true` 打开) | 缺省不可用,打开后可用 | 缺省需要 |
159
+ | VS Code 终端 | 取决于版本与设置 | 不保证 | 需要 |
160
+ | macOS Terminal | 无 | 不可用(同 Enter) | 需要 |
161
+ | Windows Terminal | 稳定版无 | 不可用(送 LF,等于 Ctrl+J 换行) | 需要 |
162
+ | Armadra 画布终端节点(xterm.js 6.0) | 无 kitty / modifyOtherKeys | 不可用 | 需要 |
163
+ | tmux | 外层终端支持且 `extended-keys on` 时转发 | 视外层与配置 | 否则需要 |
150
164
 
151
165
  line 模式(`--no-tui`)运行中输入 `/interrupt <文本>` 效果相同(空闲时就是普通提示);交互界面里也认这条命令。RPC 是 `prompt` / `steer` 的 `interrupt: true`,见 [rpc.md](../reference/rpc.md)。
152
166
 
@@ -410,7 +424,7 @@ t2 explore · 运行中 1m05s · 3 轮 · ↑12k ↓3.4k · Esc 返回 · /tasks
410
424
  - 正文实时跟随:ama 子 Agent 显示子会话的全部消息与工具调用(与消息区同样的渲染);子会话句柄已被释放(保留上限 16 个)或会话是 resume 进来的,就只读加载子会话文件,任务再次运行时接上实时事件。外部 Agent(claude / codex / ACP)显示本进程内存里的实时输出(文本、思考、工具起止、回合、提示;最多 2000 条 / 1 MB,不落盘);ama 重启后只剩一行说明「用原 CLI resume <会话 id> 查看全文」。
411
425
  - 输入框为空时:`↑` / PgUp 上翻(暂停跟随,底部提示「已暂停跟随 · End 继续」),`↓` / PgDn 下翻,End(暂停时也可按 `f`)回到跟随;`←` `→` 切到上一个 / 下一个任务;Esc 返回主界面。输入框有字时 Esc 先清空。
412
426
  - Enter 把输入发给这个子 Agent(会话里记为 `origin: "direct"` 的 user 消息,见 [session-format.md](../reference/session-format.md)):ama 子 Agent 运行中 → 排到它本轮结束时送达;外部 Agent 运行中或任务还在排队 → 等本次运行结束后续聊;已结束 → 后台续聊(同 `task_ctl send`,完成后主会话照常收到 `<task-notification>`)。底部一行提示发送结果。父会话的模型不知道你直接和子 Agent 说过话,结果经结束通知自然带回。
413
- - `Ctrl+X` 打断并发送给这个子 Agent:ama 子 Agent 运行中 → 中止它当前这一轮(工具按中断收尾)并立即以这条消息开新一轮,任务照常完成、照常通知主会话;外部 Agent 的驱动能中断单个回合(ACP `session/cancel`、Claude stream-json interrupt、Codex `turn/interrupt`)→ 中断后立即以它(连同之前排着的消息)开下一回合;不能(oneshot、宿主驱动)或任务还在并发池排队 → 退回排队并提示「不支持打断,已排队」;已结束 → 同 Enter 的后台续聊。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。主会话不受影响。
427
+ - `Ctrl+Enter`(回退 `Ctrl+X`)打断并发送给这个子 Agent:ama 子 Agent 运行中 → 中止它当前这一轮(工具按中断收尾)并立即以这条消息开新一轮,任务照常完成、照常通知主会话;外部 Agent 的驱动能中断单个回合(ACP `session/cancel`、Claude stream-json interrupt、Codex `turn/interrupt`)→ 中断后立即以它(连同之前排着的消息)开下一回合;不能(oneshot、宿主驱动)或任务还在并发池排队 → 退回排队并提示「不支持打断,已排队」;已结束 → 同 Enter 的后台续聊。`ui.enterWhileRunning: "interrupt"` 时与 Enter 互换。主会话不受影响。
414
428
  - 视图里 Esc 不中断任何东西:Esc 只是返回。停止子任务用 `/tasks stop <id>`,转后台用 `Ctrl+B` 或 `/tasks bg [id]`——在视图输入框里也能用(视图里只认这两条命令)。
415
429
  - 正在看的任务等审批时标题显示「等待审批」,审批框照常弹在视图上面(带 `[task:<类型>]` 来源);它的审批停靠在栏里时,打开视图即弹出。
416
430
 
@@ -515,7 +529,7 @@ t2 explore · 运行中 1m05s · 3 轮 · ↑12k ↓3.4k · Esc 返回 · /tasks
515
529
  ## 在 tmux / Armadra 终端节点里
516
530
 
517
531
  - 括号粘贴:启动时开启;粘贴的多行内容整体进入输入框(超过 10 行或 1 000 字符折叠为 `[粘贴 #N · M 行]`),粘贴后紧跟的回车直接发送,适合由外部程序写入。
518
- - 除程序状态检测(见上一节,tmux 里不检测)外不查询终端能力、不开鼠标与 Kitty 键盘协议,避免回包混进输入;tmux ≥ 3.4 透传同步输出,旧版本也能正常显示。
532
+ - 除程序状态检测(见上一节,tmux 里不检测)外不查询终端能力、不开鼠标,避免回包混进输入;Kitty 键盘协议与 modifyOtherKeys 只写开启序列、不查询(让 Ctrl+Enter 可区分,`AMA_KEYBOARD_PROTOCOL=off` 关闭);tmux ≥ 3.4 透传同步输出,旧版本也能正常显示。
519
533
  - tmux 的缺省前缀 `C-b` 会被 tmux 客户端吃掉:转后台按 `C-b C-b`(`send-prefix` 透传),或 `↓` 进 Agent 栏按 `b`;进栏用 `↓`,不受前缀影响。
520
534
  - 窗口尺寸变化时整屏重画最后一屏,回滚里的历史不受影响。
521
535
  - 自动降级:非 TTY、`TERM=dumb`、`--no-tui` 或终端初始化失败时使用行式界面,命令与审批问答相同。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armadra/agent",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "A coding and coordination agent that runs standalone or embedded in Armadra",
5
5
  "type": "module",
6
6
  "keywords": [