@armadra/agent 0.6.2 → 0.6.4

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.
Files changed (147) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/CHANGELOG.zh-CN.md +59 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/queue.d.ts +9 -0
  6. package/dist/agent/queue.js +27 -0
  7. package/dist/agent/session-subagent.d.ts +1 -0
  8. package/dist/agent/session-subagent.js +9 -0
  9. package/dist/agent/session.d.ts +11 -0
  10. package/dist/agent/session.js +29 -2
  11. package/dist/agent/subagent-background.d.ts +75 -0
  12. package/dist/agent/subagent-background.js +209 -0
  13. package/dist/agent/subagent-direct.d.ts +5 -2
  14. package/dist/agent/subagent-direct.js +34 -1
  15. package/dist/agent/subagent-registry.d.ts +26 -6
  16. package/dist/agent/subagent-registry.js +66 -94
  17. package/dist/agent/types-w5.d.ts +11 -1
  18. package/dist/agent/types.d.ts +12 -0
  19. package/dist/agents/builtin.js +0 -1
  20. package/dist/agents/external.js +0 -1
  21. package/dist/agents/parse.js +4 -3
  22. package/dist/agents/result.d.ts +7 -1
  23. package/dist/agents/result.js +20 -1
  24. package/dist/agents/task-control.d.ts +13 -2
  25. package/dist/agents/task-record.d.ts +16 -4
  26. package/dist/agents/task-record.js +32 -0
  27. package/dist/agents/types.d.ts +5 -1
  28. package/dist/ai/apis/chatgpt-rate-limits.js +7 -2
  29. package/dist/ai/providers/discovered-cache.d.ts +10 -4
  30. package/dist/ai/providers/discovered-cache.js +30 -14
  31. package/dist/ai/types.d.ts +2 -1
  32. package/dist/auth/chatgpt/backend-client.d.ts +6 -5
  33. package/dist/auth/chatgpt/backend-client.js +8 -7
  34. package/dist/bundle/ama.cjs +2034 -552
  35. package/dist/cli/compose-agents.d.ts +2 -1
  36. package/dist/cli/compose-agents.js +11 -2
  37. package/dist/cli/subcommands/models-discover.d.ts +1 -0
  38. package/dist/cli/subcommands/models-discover.js +12 -2
  39. package/dist/config/json-schema.js +10 -2
  40. package/dist/config/key-docs.js +9 -2
  41. package/dist/config/merge.d.ts +1 -1
  42. package/dist/config/merge.js +20 -3
  43. package/dist/config/schema-w5.js +4 -2
  44. package/dist/config/schema.js +4 -0
  45. package/dist/config/settings-registry.js +5 -0
  46. package/dist/config/types-w5.d.ts +10 -0
  47. package/dist/config/types-w5.js +2 -0
  48. package/dist/config/types.d.ts +5 -1
  49. package/dist/drivers/runner.js +29 -2
  50. package/dist/git/info.d.ts +19 -0
  51. package/dist/git/info.js +64 -8
  52. package/dist/i18n/catalog.d.ts +58 -8
  53. package/dist/i18n/messages/agents.d.ts +60 -0
  54. package/dist/i18n/messages/agents.js +62 -2
  55. package/dist/i18n/messages/config-keys.d.ts +8 -0
  56. package/dist/i18n/messages/config-keys.js +18 -10
  57. package/dist/i18n/messages/config.d.ts +8 -0
  58. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  59. package/dist/i18n/messages/interactive-startup.js +0 -16
  60. package/dist/i18n/messages/interactive-view.d.ts +8 -0
  61. package/dist/i18n/messages/interactive-view.js +10 -0
  62. package/dist/i18n/messages/interactive.d.ts +33 -16
  63. package/dist/i18n/messages/interactive.js +31 -0
  64. package/dist/i18n/messages/print.d.ts +4 -0
  65. package/dist/i18n/messages/print.js +4 -0
  66. package/dist/i18n/messages/report.d.ts +8 -0
  67. package/dist/i18n/messages/report.js +12 -4
  68. package/dist/i18n/messages/settings.d.ts +8 -0
  69. package/dist/i18n/messages/settings.js +8 -0
  70. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  71. package/dist/i18n/messages/subcommands-config.js +4 -0
  72. package/dist/i18n/messages/subcommands.d.ts +4 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/modes/commands-core.js +19 -4
  75. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  76. package/dist/modes/interactive/agent-bar.js +12 -5
  77. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  78. package/dist/modes/interactive/agent-ui.js +82 -12
  79. package/dist/modes/interactive/agent-view.d.ts +9 -0
  80. package/dist/modes/interactive/agent-view.js +28 -4
  81. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  82. package/dist/modes/interactive/approval-dock.js +112 -0
  83. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  84. package/dist/modes/interactive/approval-ui.js +64 -0
  85. package/dist/modes/interactive/commands.js +4 -1
  86. package/dist/modes/interactive/event-notices.d.ts +6 -1
  87. package/dist/modes/interactive/event-notices.js +7 -1
  88. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  89. package/dist/modes/interactive/interactive-mode.js +46 -39
  90. package/dist/modes/interactive/interrupt-send.d.ts +25 -0
  91. package/dist/modes/interactive/interrupt-send.js +32 -0
  92. package/dist/modes/interactive/key-dispatch.d.ts +29 -5
  93. package/dist/modes/interactive/key-dispatch.js +97 -8
  94. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  95. package/dist/modes/interactive/line/line-mode.js +34 -7
  96. package/dist/modes/interactive/run-indicator.d.ts +34 -2
  97. package/dist/modes/interactive/run-indicator.js +62 -8
  98. package/dist/modes/interactive/session-events.js +5 -1
  99. package/dist/modes/interactive/startup-header.d.ts +29 -14
  100. package/dist/modes/interactive/startup-header.js +93 -59
  101. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  102. package/dist/modes/interactive/startup-logo.js +183 -0
  103. package/dist/modes/interactive/status-area.d.ts +18 -0
  104. package/dist/modes/interactive/status-area.js +67 -1
  105. package/dist/modes/interactive/status-bar.d.ts +13 -2
  106. package/dist/modes/interactive/status-bar.js +60 -17
  107. package/dist/modes/interactive/status-line.d.ts +11 -8
  108. package/dist/modes/interactive/status-line.js +52 -39
  109. package/dist/modes/interactive/status-quota.d.ts +58 -0
  110. package/dist/modes/interactive/status-quota.js +155 -0
  111. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  112. package/dist/modes/interactive/subagent-view.js +8 -0
  113. package/dist/modes/interactive/task-background.d.ts +31 -0
  114. package/dist/modes/interactive/task-background.js +68 -0
  115. package/dist/modes/interactive/tool-view.d.ts +7 -1
  116. package/dist/modes/interactive/tool-view.js +24 -1
  117. package/dist/modes/print/print-mode.d.ts +11 -0
  118. package/dist/modes/print/print-mode.js +36 -1
  119. package/dist/modes/rpc/commands.d.ts +4 -1
  120. package/dist/modes/rpc/commands.js +24 -2
  121. package/dist/rpc.d.ts +20 -0
  122. package/dist/rpc.js +3 -0
  123. package/dist/tools/task-ctl.d.ts +2 -0
  124. package/dist/tools/task-ctl.js +7 -2
  125. package/dist/tools/task.d.ts +11 -0
  126. package/dist/tools/task.js +20 -2
  127. package/dist/tools/types.d.ts +6 -0
  128. package/dist/tui/components/editor.d.ts +2 -0
  129. package/dist/tui/components/editor.js +4 -0
  130. package/dist/tui/components/loader.d.ts +5 -1
  131. package/dist/tui/components/loader.js +18 -5
  132. package/dist/tui/keybindings.d.ts +15 -3
  133. package/dist/tui/keybindings.js +15 -3
  134. package/docs/agents.md +52 -28
  135. package/docs/en/host-api.md +5 -1
  136. package/docs/en/providers.md +1 -1
  137. package/docs/en/rpc.md +28 -14
  138. package/docs/en/sessions.md +3 -1
  139. package/docs/en/tui.md +106 -70
  140. package/docs/host-api.md +5 -1
  141. package/docs/providers.md +4 -2
  142. package/docs/rpc.md +28 -14
  143. package/docs/session-format.md +1 -1
  144. package/docs/sessions.md +2 -1
  145. package/docs/tui-design.md +42 -29
  146. package/docs/tui.md +106 -70
  147. package/package.json +1 -1
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * - W5-I:`entry_appended` 里 `reason: "image_budget"` 的 `context_edit`(一次降级会连写多条,每条一个消息)
5
5
  * 合并成一条「已省略 N 张早期图片以符合请求上限」,N 按占位文本数;
6
- * - W5-H1:`compaction_end.error` 的「compaction did not shrink」换成中文说明(其余错误原样)。
6
+ * - W5-H1:`compaction_end.error` 的「compaction did not shrink」换成中文说明(其余错误原样);
7
+ * - W7-C:`subagent_background`(超时自动 / 宿主转后台)一行提示。
7
8
  */
8
9
  import type { SessionEvent } from "../../agent/types.js";
9
10
  /** `compaction_end.error` 的显示文本。 */
@@ -30,3 +31,7 @@ export declare function modelFallbackText(event: Extract<SessionEvent, {
30
31
  export declare function backgroundJobText(event: Extract<SessionEvent, {
31
32
  type: "background_job";
32
33
  }>): string;
34
+ /** [W7-C] `subagent_background`:超时自动 / 宿主转后台的一行提示(人按 Ctrl+B 的已有底部提示)。 */
35
+ export declare function subagentBackgroundText(event: Extract<SessionEvent, {
36
+ type: "subagent_background";
37
+ }>, agent: string): string;
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * - W5-I:`entry_appended` 里 `reason: "image_budget"` 的 `context_edit`(一次降级会连写多条,每条一个消息)
5
5
  * 合并成一条「已省略 N 张早期图片以符合请求上限」,N 按占位文本数;
6
- * - W5-H1:`compaction_end.error` 的「compaction did not shrink」换成中文说明(其余错误原样)。
6
+ * - W5-H1:`compaction_end.error` 的「compaction did not shrink」换成中文说明(其余错误原样);
7
+ * - W7-C:`subagent_background`(超时自动 / 宿主转后台)一行提示。
7
8
  */
8
9
  import { msg } from "../../i18n/index.js";
9
10
  import { IMAGE_OMITTED_FOR_BUDGET, IMAGE_OMITTED_TOO_LARGE, } from "../../compaction/image-budget.js";
@@ -79,3 +80,8 @@ export function backgroundJobText(event) {
79
80
  return m.backgroundStopped(event.jobId, short);
80
81
  }
81
82
  }
83
+ /** [W7-C] `subagent_background`:超时自动 / 宿主转后台的一行提示(人按 Ctrl+B 的已有底部提示)。 */
84
+ export function subagentBackgroundText(event, agent) {
85
+ const m = msg().agents.background;
86
+ return event.reason === "timeout" ? m.timeout(event.taskId, agent) : m.host(event.taskId, agent);
87
+ }
@@ -16,8 +16,10 @@
16
16
  * 切换(只影响本会话)。
17
17
  * - 第五波(W5-U,agent-ui.ts):计划审批框、子 Agent 折叠视图、`/tasks` `/agents` `/paste` 与 Ctrl+V、审批来源
18
18
  * 标注与外部 Agent 首次运行的合并确认(approval-merge.ts)、harness 提示。
19
- * - 启动头按 `ui.quietStartup`(startup-header.ts):normal 框 + 模型 / 目录 / 模式 / 资源清单(窄屏或
20
- * `ui.compact` 去框),header 一行,silent 不输出。
19
+ * - [W7-C] 审批接线拆到 approval-ui.ts(合并 → 后台任务停靠 → 对话框);`Ctrl+B` 转后台与 Esc 提示的数据来自
20
+ * task-background.ts。
21
+ * - 启动头按 `ui.quietStartup`(startup-header.ts):normal「AMA」字符画 + 信息列(启动时点亮扫描一次,
22
+ * startup-logo.ts;窄屏 / `ui.logo: off` / `ui.compact` 不画字符画),header 一行,silent 不输出。
21
23
  */
22
24
  import type { AgentSession } from "../../agent/types.js";
23
25
  import type { ModeContext } from "../../cli/deps.js";
@@ -16,13 +16,14 @@
16
16
  * 切换(只影响本会话)。
17
17
  * - 第五波(W5-U,agent-ui.ts):计划审批框、子 Agent 折叠视图、`/tasks` `/agents` `/paste` 与 Ctrl+V、审批来源
18
18
  * 标注与外部 Agent 首次运行的合并确认(approval-merge.ts)、harness 提示。
19
- * - 启动头按 `ui.quietStartup`(startup-header.ts):normal 框 + 模型 / 目录 / 模式 / 资源清单(窄屏或
20
- * `ui.compact` 去框),header 一行,silent 不输出。
19
+ * - [W7-C] 审批接线拆到 approval-ui.ts(合并 → 后台任务停靠 → 对话框);`Ctrl+B` 转后台与 Esc 提示的数据来自
20
+ * task-background.ts。
21
+ * - 启动头按 `ui.quietStartup`(startup-header.ts):normal「AMA」字符画 + 信息列(启动时点亮扫描一次,
22
+ * startup-logo.ts;窄屏 / `ui.logo: off` / `ui.compact` 不画字符画),header 一行,silent 不输出。
21
23
  */
22
24
  import { promptImages, sessionModel } from "../image-input.js";
23
25
  import { join } from "node:path";
24
26
  import { AgentSessionImpl } from "../../agent/session.js";
25
- import { taskRegistryView } from "../../agent/subagent-registry.js";
26
27
  import { currentSession, switchSession } from "../../cli/compose-session.js";
27
28
  import { startupInfo, startupScreenLevel, tildePath } from "../../cli/startup-screen.js";
28
29
  import { AmaError, isAmaError } from "../../errors.js";
@@ -31,26 +32,28 @@ import { Container, Editor, Loader, Spacer, TUI, Text, createTheme, detectCapabi
31
32
  import { BYPASS_MODE, createBypassGate } from "../../permissions/bypass.js";
32
33
  import { onTerminationSignals } from "../shared.js";
33
34
  import { AgentUi, agentCommandHooks } from "./agent-ui.js";
34
- import { ApprovalDialogBroker, approvalOutcomeText } from "./approval-dialog.js";
35
- import { mergingBroker } from "./approval-merge.js";
35
+ import { approvalOverlayHooks, createApprovalUi } from "./approval-ui.js";
36
36
  import { ALL_COMMANDS, runInteractiveCommand } from "./commands.js";
37
37
  import { memoryPanelFor } from "./memory-panel.js";
38
38
  import { InteractiveCompletion } from "./completion.js";
39
39
  import { confirmBypass } from "./confirm-dialog.js";
40
40
  import { configUiFor } from "./config-ui.js";
41
41
  import { createKeyDispatch } from "./key-dispatch.js";
42
+ import { enterModeOf, interruptKeys, interruptSender } from "./interrupt-send.js";
42
43
  import { MessageView, exitSummaryLines } from "./message-view.js";
43
44
  import { openPicker } from "./pickers.js";
44
45
  import { modelPickerFor } from "./model-picker.js";
45
46
  import { createRewindFlow } from "./rewind-flow.js";
46
47
  import { openTraceView } from "./trace-view.js";
47
48
  import { StartupHeader } from "./startup-header.js";
49
+ import { playStartupAnimation } from "./startup-logo.js";
48
50
  import { QueueView, RunIndicator } from "./run-indicator.js";
49
51
  import { StatusArea, statusLineSlash } from "./status-area.js";
50
52
  import { createSessionEventHandler } from "./session-events.js";
51
53
  import { SubagentTracker } from "./subagent-view.js";
52
54
  import { loadKeys, processTerminal } from "./terminal-setup.js";
53
55
  import { ToolTracker } from "./tool-view.js";
56
+ import { backgroundTasks, blockingTasks, keyLabel, runningBackground } from "./task-background.js";
54
57
  const HINT_MS = 2500;
55
58
  export function runInteractiveMode(runtime, context, options = {}) {
56
59
  const terminal = options.terminal ?? processTerminal();
@@ -82,7 +85,9 @@ export function runInteractiveMode(runtime, context, options = {}) {
82
85
  spinner: () => loader.frame,
83
86
  subagent: (toolCallId) => subagents.forToolCall(toolCallId),
84
87
  });
85
- const queueView = new QueueView(theme);
88
+ const enterMode = () => enterModeOf(runtime);
89
+ const sendKeys = interruptKeys(keys, enterMode);
90
+ const queueView = new QueueView(theme, sendKeys.now);
86
91
  const loaderSlot = new Container();
87
92
  const loader = new Loader(() => tui.requestRender(), {
88
93
  theme,
@@ -122,6 +127,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
122
127
  requestRender: () => tui.requestRender(),
123
128
  ...(historyFile !== undefined ? { historyFile } : {}),
124
129
  onSubmit: (text) => submit(text, "enter"),
130
+ onChange: () => indicator.refreshHints(),
125
131
  });
126
132
  tui.addChild(view);
127
133
  tui.addChild(new Spacer());
@@ -131,6 +137,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
131
137
  tui.addChild(hint);
132
138
  tui.addChild(area.rate);
133
139
  tui.addChild(status);
140
+ tui.addChild(area.quota);
134
141
  // ---- 小工具 ---------------------------------------------------------------
135
142
  let finished = false;
136
143
  let hintTimer;
@@ -154,7 +161,13 @@ export function runInteractiveMode(runtime, context, options = {}) {
154
161
  }
155
162
  render();
156
163
  };
157
- const indicator = new RunIndicator({ theme, loader, slot: loaderSlot, tools, render });
164
+ const backgroundKey = keyLabel(keys, "app.tasks.background");
165
+ const indicator = new RunIndicator({
166
+ ...{ theme, loader, slot: loaderSlot, tools, render },
167
+ ...(backgroundKey !== undefined ? { backgroundKey } : {}),
168
+ ...(sendKeys.key !== undefined ? { interruptKey: sendKeys.key } : {}),
169
+ });
170
+ Object.assign(indicator, { draft: () => !editor.isEmpty(), enterMode });
158
171
  const setQueue = (steering, followUp) => {
159
172
  status.setQueue(steering.length, followUp.length);
160
173
  queueView.setQueue(steering, followUp);
@@ -177,7 +190,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
177
190
  const startupLevel = startupScreenLevel(runtime);
178
191
  const home = env["HOME"] ?? env["USERPROFILE"];
179
192
  const info = startupInfo(runtime, home);
180
- const header = (level) => new StartupHeader(info, { theme, level, ...(ui.compact === true ? { compact: true } : {}) });
193
+ const header = (level) => new StartupHeader(info, { theme, level, logo: ui.logo, compact: ui.compact });
181
194
  /** 清空消息区(切换会话、/tree):只留一行头。 */
182
195
  const resetView = () => {
183
196
  view.reset();
@@ -354,25 +367,22 @@ export function runInteractiveMode(runtime, context, options = {}) {
354
367
  onPasteImage: () => void agentUi.paste(),
355
368
  confirmMode,
356
369
  submit: (text, via) => submit(text, via),
370
+ enterWhileRunning: enterMode,
371
+ interruptSend: interruptSender({
372
+ session: () => session,
373
+ takeImages: () => draftImages.splice(0),
374
+ showHint,
375
+ }),
357
376
  runCommand: (line) => void runCommand(line),
358
377
  exit: (code) => exit(code),
359
378
  onInterrupted: (empty) => void rewind.afterInterrupt(empty),
360
379
  agents: () => agentUi.keys,
380
+ downGlyph: theme.glyphs.arrowDown,
381
+ backgroundTasks: () => backgroundTasks(session),
382
+ runningBackground: () => runningBackground(session),
361
383
  }));
362
384
  // ---- 晚绑定 ---------------------------------------------------------------
363
- const overlayHooks = {
364
- showOverlay: (component) => tui.showOverlay(component, { anchor: "bottom" }),
365
- onOpen: () => {
366
- editor.disableSubmit = true;
367
- tools.setAwaiting(true);
368
- indicator.setApproval(true);
369
- },
370
- onClose: () => {
371
- editor.disableSubmit = false;
372
- tools.setAwaiting(false);
373
- indicator.setApproval(false);
374
- },
375
- };
385
+ const overlayHooks = approvalOverlayHooks({ tui, editor, tools: () => tools, indicator });
376
386
  const agentUi = new AgentUi({
377
387
  theme,
378
388
  tui,
@@ -395,28 +405,20 @@ export function runInteractiveMode(runtime, context, options = {}) {
395
405
  ...(options.clipboard !== undefined ? { clipboard: options.clipboard } : {}),
396
406
  area,
397
407
  });
398
- const taskAgent = (taskId) => taskRegistryView(session.state.sessionId)?.get(taskId)?.agent;
399
- const broker = mergingBroker(new ApprovalDialogBroker({
400
- theme,
401
- keybindings: keys,
402
- cwd: session.state.cwd,
403
- permissionMode: () => session.state.permissionMode,
404
- taskAgent,
405
- externalRunner: (agent) => agentUi.merge.externalRunner(agent),
406
- ...overlayHooks,
407
- report: (request, outcome) => {
408
- if (outcome === "deny" || outcome === "cancelled") {
409
- const text = approvalOutcomeText(request, outcome, taskAgent);
410
- notice(outcome === "deny" ? "info" : "warn", text);
411
- }
412
- },
413
- }), agentUi.merge, (request) => notice("info", msg().interactive.app.followed(approvalOutcomeText(request, "allow", taskAgent))));
408
+ indicator.agents = () => agentUi.reachable;
409
+ const { broker, dock } = createApprovalUi({
410
+ ...{ theme, keys, tui, editor, indicator, agentUi, notice, render },
411
+ hooks: overlayHooks,
412
+ session: () => session,
413
+ });
414
+ indicator.background = () => blockingTasks(session).length > 0;
414
415
  // ---- 启动与退出 -----------------------------------------------------------
415
416
  let offSignals = () => undefined;
416
417
  function exit(code) {
417
418
  if (finished)
418
419
  return;
419
420
  finished = true;
421
+ logoAnimation?.finish();
420
422
  if (hintTimer !== undefined)
421
423
  clearTimeout(hintTimer);
422
424
  loader.stop();
@@ -427,6 +429,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
427
429
  tui.addChild(view);
428
430
  offSignals();
429
431
  agentUi.detach();
432
+ dock.dispose();
430
433
  runtime.approvals.setUiBroker(undefined);
431
434
  runtime.notifier.set(undefined);
432
435
  tui.stop();
@@ -439,8 +442,10 @@ export function runInteractiveMode(runtime, context, options = {}) {
439
442
  resolveExit(code);
440
443
  });
441
444
  }
442
- if (startupLevel !== "silent")
443
- view.add(header(startupLevel));
445
+ const startHeader = startupLevel === "silent" ? undefined : header(startupLevel);
446
+ let logoAnimation;
447
+ if (startHeader !== undefined)
448
+ view.add(startHeader);
444
449
  for (const warning of startupWarnings)
445
450
  view.addNotice("warn", warning);
446
451
  replay();
@@ -460,6 +465,8 @@ export function runInteractiveMode(runtime, context, options = {}) {
460
465
  agentUi.attach(session);
461
466
  offSignals = onTerminationSignals((code) => exit(code));
462
467
  tui.setFocus(editor);
468
+ const gate = { animation: ui.animation, host: info.host !== undefined, env, ...context };
469
+ logoAnimation = playStartupAnimation(tui, startHeader, theme, gate);
463
470
  options.onReady?.({
464
471
  tui,
465
472
  editor,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
3
+ * "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
4
+ * abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
5
+ */
6
+ import type { AgentSession } from "../../agent/types.js";
7
+ import type { ImageBlock } from "../../ai/types.js";
8
+ import type { Runtime } from "../../cli/runtime.js";
9
+ import type { Keybindings } from "../../tui.js";
10
+ import type { EnterMode } from "./run-indicator.js";
11
+ export interface InterruptSendDeps {
12
+ session(): AgentSession;
13
+ /** 取走输入框附带的图片(粘贴的剪贴板图片)。 */
14
+ takeImages(): ImageBlock[];
15
+ showHint(text: string): void;
16
+ }
17
+ /** 返回 `interruptSend(text)`:文字可为空(只把排队的插话立即送出);没有可发的内容给一行提示。 */
18
+ export declare function interruptSender(deps: InterruptSendDeps): (text: string) => void;
19
+ /** `ui.enterWhileRunning`(每次按键时读,`/config` 改了立即生效)。 */
20
+ export declare function enterModeOf(runtime: Pick<Runtime, "config">): EnterMode;
21
+ /** 专用键的标签与「立即送出排队插话」的键(interrupt 模式下是 Enter)。 */
22
+ export declare function interruptKeys(keys: Keybindings, mode: () => EnterMode): {
23
+ key: string | undefined;
24
+ now(): string | undefined;
25
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 打断并立即发送(交互模式):运行中按 `app.message.interrupt`(缺省 `Ctrl+X`;`ui.enterWhileRunning:
3
+ * "interrupt"` 时是 Enter)→ `prompt(text, { interrupt: true })`:取走排队的插话、中止当前回合(工具按
4
+ * abort 收尾),以「插话… + 本条」开新回合(会话里 origin `interrupt`)。不走「中断即撤回」。
5
+ */
6
+ import { isAmaError } from "../../errors.js";
7
+ import { msg } from "../../i18n/index.js";
8
+ import { keyLabel } from "./task-background.js";
9
+ /** 返回 `interruptSend(text)`:文字可为空(只把排队的插话立即送出);没有可发的内容给一行提示。 */
10
+ export function interruptSender(deps) {
11
+ return (text) => {
12
+ const images = deps.takeImages();
13
+ deps.showHint(msg().interactive.keys.interruptSent);
14
+ const options = images.length > 0 ? { interrupt: true, images } : { interrupt: true };
15
+ void deps
16
+ .session()
17
+ .prompt(text, options)
18
+ .catch((error) => {
19
+ if (isAmaError(error) && error.code === "invalid_arguments")
20
+ deps.showHint(msg().interactive.keys.nothingToSend);
21
+ });
22
+ };
23
+ }
24
+ /** `ui.enterWhileRunning`(每次按键时读,`/config` 改了立即生效)。 */
25
+ export function enterModeOf(runtime) {
26
+ return runtime.config.ui?.enterWhileRunning === "interrupt" ? "interrupt" : "queue";
27
+ }
28
+ /** 专用键的标签与「立即送出排队插话」的键(interrupt 模式下是 Enter)。 */
29
+ export function interruptKeys(keys, mode) {
30
+ const key = keyLabel(keys, "app.message.interrupt");
31
+ return { key, now: () => (mode() === "interrupt" ? "Enter" : key) };
32
+ }
@@ -10,20 +10,34 @@
10
10
  * [RW-C] 空闲时双击 Esc(`app.rewind`,double-esc.ts):输入框为空打开回滚列表,有字则清空并存进
11
11
  * 输入历史;运行中 Esc 仍为中断,中断后交给 `onInterrupted`(中断即撤回)。
12
12
  *
13
- * [W6-A] Agent 栏(agent-ui.ts):栏聚焦时它先收键(Esc 是「返回」,不中断、不回滚);输入为空、补全没开时
14
- * `app.agents.focus`(缺省 `Ctrl+B` / `↓`)先问它要不要进栏,不要就照常交给编辑器(光标左移、历史下一条)。
13
+ * [W6-A] Agent 栏(agent-ui.ts):栏聚焦时它先收键(Esc 是「返回」,不中断、不回滚);补全没开、没在浏览
14
+ * 历史时 `app.agents.focus`(缺省 `↓`)先问它要不要进栏,不要就照常交给编辑器(下移、历史下一条)。
15
+ * [W7-A] 落空给一行提示:输入有字(每段草稿一次,光标在末行时)、栏关闭、没有任务。
16
+ * [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
17
+ * 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
18
+ * 写进中断提示。
19
+ * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
20
+ * 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
21
+ * 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
15
22
  */
16
23
  import type { AgentSession } from "../../agent/types.js";
17
24
  import type { PermissionMode } from "../../permissions/types.js";
18
25
  import type { Editor, Keybindings } from "../../tui.js";
19
26
  import type { StatusBar } from "./status-bar.js";
20
27
  import type { ToolTracker } from "./tool-view.js";
28
+ /** [W7-A] 进栏落空提示的停留时间。 */
29
+ export declare const FOCUS_HINT_MS = 3000;
30
+ /**
31
+ * [W7-A] 进栏键的结果:true 已进栏;false 不管(交给编辑器,不提示);其余是落空原因——
32
+ * `busy-input` 输入框有字、`disabled` `ui.agentBar: "off"`(有任务时)、`empty` 没有任务。
33
+ */
34
+ export type AgentFocusResult = boolean | "busy-input" | "disabled" | "empty";
21
35
  /** [W6-A] Agent 栏的按键入口。 */
22
36
  export interface AgentKeys {
23
37
  /** 栏聚焦时先消费按键;返回 true 已处理。 */
24
38
  handleKey(data: string): boolean;
25
- /** `app.agents.focus`(输入为空):进栏返回 true,否则交给编辑器。 */
26
- focus(data: string): boolean;
39
+ /** `app.agents.focus`:输入为空且有任务时进栏返回 true,否则返回落空原因(按键仍交给编辑器)。 */
40
+ focus(data: string): AgentFocusResult;
27
41
  }
28
42
  export interface KeyDispatchDeps {
29
43
  keys: Keybindings;
@@ -49,7 +63,11 @@ export interface KeyDispatchDeps {
49
63
  confirmMode?(mode: PermissionMode): boolean | Promise<boolean>;
50
64
  /** Ctrl+V:粘贴剪贴板图片(W5-U)。 */
51
65
  onPasteImage?(): void;
52
- submit(text: string, via: "followUp"): void;
66
+ submit(text: string, via: "enter" | "followUp"): void;
67
+ /** 运行中 Enter 的语义(`ui.enterWhileRunning`,缺省 `queue`)。 */
68
+ enterWhileRunning?(): "queue" | "interrupt";
69
+ /** 打断并立即发送(文字可为空:只把排队的插话立即送出)。没有时专用键不处理。 */
70
+ interruptSend?(text: string): void;
53
71
  runCommand(line: string): void;
54
72
  exit(code: number): void;
55
73
  /** Esc 中断之后(参数:中断后输入框是否为空)。 */
@@ -58,6 +76,12 @@ export interface KeyDispatchDeps {
58
76
  doubleEscMs?: number;
59
77
  /** [W6-A] Agent 栏(晚绑定)。 */
60
78
  agents?(): AgentKeys | undefined;
79
+ /** [W7-A] 提示里的 `↓` 字形(ASCII 主题 `v`;缺省 `↓`)。 */
80
+ downGlyph?: string;
81
+ /** [W7-C] 有阻塞中的前台任务时全部转后台,返回转了的 taskId;没有返回空(按键交给编辑器)。 */
82
+ backgroundTasks?(): string[];
83
+ /** [W7-C] 仍在运行的后台任务(Esc 中断的提示里写明不受影响)。 */
84
+ runningBackground?(): string[];
61
85
  }
62
86
  /** 返回输入监听器:已处理返回 true,交给编辑器返回 false。 */
63
87
  export declare function createKeyDispatch(deps: KeyDispatchDeps): (data: string) => boolean;
@@ -10,20 +10,34 @@
10
10
  * [RW-C] 空闲时双击 Esc(`app.rewind`,double-esc.ts):输入框为空打开回滚列表,有字则清空并存进
11
11
  * 输入历史;运行中 Esc 仍为中断,中断后交给 `onInterrupted`(中断即撤回)。
12
12
  *
13
- * [W6-A] Agent 栏(agent-ui.ts):栏聚焦时它先收键(Esc 是「返回」,不中断、不回滚);输入为空、补全没开时
14
- * `app.agents.focus`(缺省 `Ctrl+B` / `↓`)先问它要不要进栏,不要就照常交给编辑器(光标左移、历史下一条)。
13
+ * [W6-A] Agent 栏(agent-ui.ts):栏聚焦时它先收键(Esc 是「返回」,不中断、不回滚);补全没开、没在浏览
14
+ * 历史时 `app.agents.focus`(缺省 `↓`)先问它要不要进栏,不要就照常交给编辑器(下移、历史下一条)。
15
+ * [W7-A] 落空给一行提示:输入有字(每段草稿一次,光标在末行时)、栏关闭、没有任务。
16
+ * [W7-C] `app.tasks.background`(缺省 `Ctrl+B`):有阻塞中的前台任务时全部转后台(不看输入框),底部提示
17
+ * 「已转后台:t2 …」;没有时落回编辑器(光标左移),不吞键。Esc 中断只连带前台任务,仍在运行的后台任务
18
+ * 写进中断提示。
19
+ * [打断并发送] 运行中 Enter 缺省排队(steer);`app.message.interrupt`(缺省 `Ctrl+X`)= 打断当前回合并立即
20
+ * 以输入框的文字(连同排队的插话)开新回合(`prompt{interrupt:true}`)。`ui.enterWhileRunning: "interrupt"`
21
+ * 时两者互换。补全打开、有覆盖层(审批 / 选择器)时不触发;斜杠命令照常当命令;空闲时专用键等同 Enter。
15
22
  */
16
23
  import { msg } from "../../i18n/index.js";
17
24
  import { ExitCode } from "../../cli/exit-codes.js";
18
25
  import { nextCycleMode, permissionModeLabel } from "../../permissions/modes.js";
19
26
  import { DOUBLE_ESC_HINT_MS, DoubleEscape } from "./double-esc.js";
27
+ import { backgroundedText } from "./task-background.js";
20
28
  import { statusLineText } from "./status-area.js";
21
29
  const DOUBLE_CTRL_C_MS = 1500;
30
+ /** [W7-A] 进栏落空提示的停留时间。 */
31
+ export const FOCUS_HINT_MS = 3000;
22
32
  /** 返回输入监听器:已处理返回 true,交给编辑器返回 false。 */
23
33
  export function createKeyDispatch(deps) {
24
34
  const { keys, editor, tools, status } = deps;
25
35
  let ctrlCArmedAt = Number.NEGATIVE_INFINITY;
26
36
  const esc = new DoubleEscape(deps.doubleEscMs);
37
+ /** 本段草稿已给过「输入框有字」提示(输入清空后重置)。 */
38
+ let busyInputHinted = false;
39
+ /** 底部正显示着进栏落空提示(进栏后撤掉)。 */
40
+ let focusHintShown = false;
27
41
  const doubleEscape = () => {
28
42
  switch (esc.press(deps.now(), editor.isEmpty())) {
29
43
  case "arm-rewind":
@@ -88,9 +102,75 @@ export function createKeyDispatch(deps) {
88
102
  else
89
103
  void answer.then(settle);
90
104
  };
105
+ /**
106
+ * 运行中的「排队 / 打断并发送」:专用键与(`interrupt` 模式下的)Enter。返回 false 交给编辑器
107
+ * (Enter 照常提交 = 排队;斜杠命令照常执行)。
108
+ */
109
+ const sendKeys = (is) => {
110
+ if (deps.interruptSend === undefined || editor.isCompletionOpen)
111
+ return false;
112
+ const viaKey = is("app.message.interrupt");
113
+ if (!deps.busy()) {
114
+ if (!viaKey || editor.isEmpty())
115
+ return false;
116
+ const text = editor.takeSubmission();
117
+ if (text !== null)
118
+ deps.submit(text, "enter");
119
+ return true;
120
+ }
121
+ const interruptMode = deps.enterWhileRunning?.() === "interrupt";
122
+ if (!viaKey && !(interruptMode && is("tui.editor.submit")))
123
+ return false;
124
+ const command = editor.getExpandedText().trimStart().startsWith("/");
125
+ if (command || viaKey === interruptMode) {
126
+ // 排队(interrupt 模式下的专用键)或斜杠命令:同 Enter 提交
127
+ if (!viaKey)
128
+ return false;
129
+ const text = editor.takeSubmission();
130
+ if (text !== null)
131
+ deps.submit(text, "enter");
132
+ return true;
133
+ }
134
+ deps.interruptSend(editor.takeSubmission() ?? "");
135
+ return true;
136
+ };
137
+ /** 光标在最后一行(`↓` 不再下移,只会到行尾)。 */
138
+ const atLastLine = () => editor.cursor.line >= editor.getText().split("\n").length - 1;
139
+ /** `app.agents.focus`:进栏返回 true;落空给提示后返回 false(按键照常交给编辑器)。 */
140
+ const focusAgents = (agents, data) => {
141
+ if (editor.isCompletionOpen || editor.isBrowsingHistory)
142
+ return false;
143
+ const result = agents.focus(data);
144
+ if (result === true) {
145
+ // 进栏了:撤掉之前的落空提示
146
+ if (focusHintShown)
147
+ deps.showHint("");
148
+ focusHintShown = false;
149
+ return true;
150
+ }
151
+ const m = msg().agents;
152
+ let text;
153
+ if (result === "busy-input") {
154
+ if (busyInputHinted || !atLastLine())
155
+ return false;
156
+ busyInputHinted = true;
157
+ text = m.focus.busyInput(deps.downGlyph ?? "↓");
158
+ }
159
+ else if (result === "disabled")
160
+ text = m.focus.disabled;
161
+ else if (result === "empty")
162
+ text = m.bar.empty;
163
+ if (text !== undefined) {
164
+ deps.showHint(text, FOCUS_HINT_MS);
165
+ focusHintShown = true;
166
+ }
167
+ return false;
168
+ };
91
169
  return (data) => {
92
170
  if (deps.inactive())
93
171
  return false;
172
+ if (editor.isEmpty())
173
+ busyInputHinted = false;
94
174
  const is = (action) => keys.matches(data, action);
95
175
  if (!is("app.rewind"))
96
176
  esc.reset();
@@ -99,12 +179,16 @@ export function createKeyDispatch(deps) {
99
179
  esc.reset();
100
180
  return true;
101
181
  }
102
- if (agents !== undefined &&
103
- is("app.agents.focus") &&
104
- editor.isEmpty() &&
105
- !editor.isCompletionOpen &&
106
- agents.focus(data))
182
+ if (agents !== undefined && is("app.agents.focus") && focusAgents(agents, data))
107
183
  return true;
184
+ if (is("app.tasks.background") && deps.backgroundTasks !== undefined) {
185
+ const moved = deps.backgroundTasks();
186
+ if (moved.length > 0) {
187
+ esc.reset();
188
+ deps.showHint(backgroundedText(moved), FOCUS_HINT_MS);
189
+ return true;
190
+ }
191
+ }
108
192
  if (is("app.clear")) {
109
193
  if (!editor.isEmpty()) {
110
194
  editor.clear();
@@ -121,10 +205,15 @@ export function createKeyDispatch(deps) {
121
205
  return true;
122
206
  }
123
207
  ctrlCArmedAt = Number.NEGATIVE_INFINITY;
208
+ if (sendKeys(is))
209
+ return true;
124
210
  if (is("app.interrupt") && !editor.isCompletionOpen && deps.busy()) {
125
211
  esc.reset();
126
212
  interrupt();
127
- deps.showHint(msg().interactive.keys.interrupted);
213
+ const left = deps.runningBackground?.() ?? [];
214
+ deps.showHint(left.length > 0
215
+ ? msg().agents.background.interrupted(left.join(", "))
216
+ : msg().interactive.keys.interrupted);
128
217
  deps.onInterrupted?.(editor.isEmpty());
129
218
  return true;
130
219
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * - stdin 是 TTY 且可开 raw:自带单行编辑器(line-editor.ts:历史、括号粘贴折叠),运行中回车
5
5
  * = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
6
+ * 运行中 `/interrupt <文本>` = 打断当前回合并立即以它开新回合(排队的插话在前)。
6
7
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
8
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
9
  * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * - stdin 是 TTY 且可开 raw:自带单行编辑器(line-editor.ts:历史、括号粘贴折叠),运行中回车
5
5
  * = steer;Ctrl+C 运行中中断、空闲时连按两次退出(130);空行 Ctrl+D 退出;审批 y / a / N 单键问答。
6
+ * 运行中 `/interrupt <文本>` = 打断当前回合并立即以它开新回合(排队的插话在前)。
6
7
  * 问句之前逐行打印执行前预览(`request.preview`,W3-B9a-2)。
7
8
  * - 否则(管道):逐行读 stdin,每行依次执行(等上一条运行结束);没有审批 UI,ask → deny。
8
9
  * 模型错误在运行结束时只打印一次(重试中只显示 ↻);有运行最终失败时退出码 1。
@@ -134,13 +135,14 @@ export async function runLineMode(runtime, context, options = {}) {
134
135
  const question = approvalQuestion(request, (taskId) => taskRegistryView(session.state.sessionId)?.get(taskId)?.agent);
135
136
  void ed.ask(`\n${preview.join("")}${question}`, "n").then((answer) => {
136
137
  resolve(answer === "y" ? "allow" : answer === "a" ? "allow_session" : "deny");
137
- if (!busy)
138
+ if (running === 0)
138
139
  ed.render();
139
140
  });
140
141
  }),
141
142
  });
142
143
  runtime.notifier.set((message, level) => err(`ama: [${level}] ${message}\n`));
143
- let busy = false;
144
+ /** 进行中的提示(`/interrupt` 开的新回合与被它中止的那一轮可能短暂重叠);> 0 = 运行中。 */
145
+ let running = 0;
144
146
  let lastInterrupt = 0;
145
147
  return new Promise((resolve) => {
146
148
  let finished = false;
@@ -164,12 +166,27 @@ export async function runLineMode(runtime, context, options = {}) {
164
166
  });
165
167
  };
166
168
  const run = (text) => {
167
- busy = true;
169
+ running++;
168
170
  void handle(text).then((outcome) => {
169
- busy = false;
171
+ running--;
170
172
  if (outcome === "exit")
171
173
  finish(ExitCode.Ok);
172
- else
174
+ else if (running === 0)
175
+ ed.render();
176
+ });
177
+ };
178
+ /** 运行中 `/interrupt <文本>`:打断当前回合并立即以它(排队的插话在前)开新回合。 */
179
+ const interruptWith = (text) => {
180
+ if (text === "")
181
+ return err(`ama: ${msg().report.command.interruptUsage}\n`);
182
+ out(`${msg().report.command.interruptSent(text)}\n`);
183
+ running++;
184
+ void session
185
+ .prompt(text, { interrupt: true })
186
+ .catch((error) => err(`ama: ${errorText(error)}\n`))
187
+ .finally(() => {
188
+ printer.endLine();
189
+ if (--running === 0)
173
190
  ed.render();
174
191
  });
175
192
  };
@@ -179,7 +196,7 @@ export async function runLineMode(runtime, context, options = {}) {
179
196
  if (action.kind === "eof")
180
197
  return finish(ExitCode.Ok);
181
198
  if (action.kind === "interrupt") {
182
- if (busy) {
199
+ if (running > 0) {
183
200
  void session.abort();
184
201
  err(`${msg().interactive.line.interrupted}\n`);
185
202
  }
@@ -193,7 +210,17 @@ export async function runLineMode(runtime, context, options = {}) {
193
210
  }
194
211
  continue;
195
212
  }
196
- if (busy) {
213
+ if (running > 0) {
214
+ const forced = /^\/interrupt(?:\s+([\s\S]*))?$/.exec(action.text.trim());
215
+ if (forced !== null) {
216
+ interruptWith((forced[1] ?? "").trim());
217
+ continue;
218
+ }
219
+ // [W7-C] 运行中 `/tasks bg [id]` 照样是命令(转后台阻塞中的前台任务),不当插话
220
+ if (/^\/tasks\s+bg(\s+\S+)?\s*$/.test(action.text.trim())) {
221
+ void handle(action.text.trim());
222
+ continue;
223
+ }
197
224
  if (action.text.trim() !== "") {
198
225
  void session.steer(action.text).catch(() => undefined);
199
226
  out(`${msg().interactive.line.steer(action.text)}\n`);