@armadra/agent 0.6.1 → 0.6.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/CHANGELOG.zh-CN.md +56 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/session-settings.d.ts +4 -1
  6. package/dist/agent/session-settings.js +7 -2
  7. package/dist/agent/session.d.ts +6 -0
  8. package/dist/agent/session.js +8 -0
  9. package/dist/agent/subagent-background.d.ts +75 -0
  10. package/dist/agent/subagent-background.js +209 -0
  11. package/dist/agent/subagent-registry.d.ts +24 -5
  12. package/dist/agent/subagent-registry.js +64 -92
  13. package/dist/agent/types-w5.d.ts +11 -1
  14. package/dist/agent/types.d.ts +5 -0
  15. package/dist/agents/builtin.js +0 -1
  16. package/dist/agents/external.js +0 -1
  17. package/dist/agents/parse.js +4 -3
  18. package/dist/agents/result.d.ts +7 -1
  19. package/dist/agents/result.js +20 -1
  20. package/dist/agents/task-control.d.ts +13 -2
  21. package/dist/agents/task-record.d.ts +13 -1
  22. package/dist/agents/task-record.js +32 -0
  23. package/dist/agents/types.d.ts +5 -1
  24. package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
  25. package/dist/ai/apis/chatgpt-backend.js +12 -4
  26. package/dist/ai/providers/discovered-cache.d.ts +29 -12
  27. package/dist/ai/providers/discovered-cache.js +72 -12
  28. package/dist/ai/providers/registry.js +7 -6
  29. package/dist/ai/types.d.ts +5 -0
  30. package/dist/auth/chatgpt/backend-client.d.ts +14 -4
  31. package/dist/auth/chatgpt/backend-client.js +41 -8
  32. package/dist/auth/chatgpt/cli.d.ts +2 -1
  33. package/dist/auth/chatgpt/cli.js +14 -7
  34. package/dist/auth/chatgpt/follow.d.ts +16 -0
  35. package/dist/auth/chatgpt/follow.js +51 -0
  36. package/dist/auth/chatgpt/presets.d.ts +8 -0
  37. package/dist/auth/chatgpt/presets.js +12 -0
  38. package/dist/auth/testing/fake-oauth.d.ts +4 -1
  39. package/dist/auth/testing/fake-oauth.js +24 -1
  40. package/dist/bundle/ama.cjs +2016 -664
  41. package/dist/cli/compose-agents.d.ts +2 -1
  42. package/dist/cli/compose-agents.js +11 -2
  43. package/dist/cli/compose-providers.d.ts +2 -1
  44. package/dist/cli/compose-providers.js +6 -3
  45. package/dist/cli/startup-steps.js +4 -1
  46. package/dist/cli/subcommands/models-discover.d.ts +5 -2
  47. package/dist/cli/subcommands/models-discover.js +52 -10
  48. package/dist/config/json-schema.js +10 -2
  49. package/dist/config/key-docs.js +9 -3
  50. package/dist/config/merge.d.ts +1 -1
  51. package/dist/config/merge.js +20 -3
  52. package/dist/config/schema-w5.js +4 -2
  53. package/dist/config/schema-w6.d.ts +1 -1
  54. package/dist/config/schema-w6.js +2 -1
  55. package/dist/config/schema.js +2 -0
  56. package/dist/config/settings-registry.js +4 -0
  57. package/dist/config/types-w5.d.ts +10 -0
  58. package/dist/config/types-w5.js +2 -0
  59. package/dist/config/types-w6.d.ts +5 -0
  60. package/dist/config/types.d.ts +3 -1
  61. package/dist/git/info.d.ts +19 -0
  62. package/dist/git/info.js +64 -8
  63. package/dist/i18n/catalog.d.ts +52 -8
  64. package/dist/i18n/messages/agents.d.ts +56 -0
  65. package/dist/i18n/messages/agents.js +58 -2
  66. package/dist/i18n/messages/auth.d.ts +12 -0
  67. package/dist/i18n/messages/auth.js +20 -0
  68. package/dist/i18n/messages/config-keys.d.ts +8 -0
  69. package/dist/i18n/messages/config-keys.js +18 -10
  70. package/dist/i18n/messages/config.d.ts +8 -0
  71. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  72. package/dist/i18n/messages/interactive-startup.js +0 -16
  73. package/dist/i18n/messages/interactive.d.ts +23 -16
  74. package/dist/i18n/messages/interactive.js +25 -0
  75. package/dist/i18n/messages/print.d.ts +4 -0
  76. package/dist/i18n/messages/print.js +4 -0
  77. package/dist/i18n/messages/report.js +4 -4
  78. package/dist/i18n/messages/settings.d.ts +6 -0
  79. package/dist/i18n/messages/settings.js +6 -0
  80. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  81. package/dist/i18n/messages/subcommands-config.js +4 -0
  82. package/dist/i18n/messages/subcommands.d.ts +4 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/modes/commands-core.js +8 -4
  85. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  86. package/dist/modes/interactive/agent-bar.js +12 -5
  87. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  88. package/dist/modes/interactive/agent-ui.js +81 -12
  89. package/dist/modes/interactive/agent-view.d.ts +4 -0
  90. package/dist/modes/interactive/agent-view.js +14 -1
  91. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  92. package/dist/modes/interactive/approval-dock.js +112 -0
  93. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  94. package/dist/modes/interactive/approval-ui.js +64 -0
  95. package/dist/modes/interactive/commands.js +4 -1
  96. package/dist/modes/interactive/event-notices.d.ts +6 -1
  97. package/dist/modes/interactive/event-notices.js +7 -1
  98. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  99. package/dist/modes/interactive/interactive-mode.js +33 -38
  100. package/dist/modes/interactive/key-dispatch.d.ts +21 -4
  101. package/dist/modes/interactive/key-dispatch.js +60 -8
  102. package/dist/modes/interactive/line/line-mode.js +5 -0
  103. package/dist/modes/interactive/run-indicator.d.ts +15 -0
  104. package/dist/modes/interactive/run-indicator.js +37 -5
  105. package/dist/modes/interactive/session-events.js +5 -1
  106. package/dist/modes/interactive/startup-header.d.ts +29 -14
  107. package/dist/modes/interactive/startup-header.js +93 -59
  108. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  109. package/dist/modes/interactive/startup-logo.js +183 -0
  110. package/dist/modes/interactive/status-area.d.ts +18 -0
  111. package/dist/modes/interactive/status-area.js +67 -1
  112. package/dist/modes/interactive/status-bar.d.ts +9 -1
  113. package/dist/modes/interactive/status-bar.js +43 -11
  114. package/dist/modes/interactive/status-line.d.ts +2 -0
  115. package/dist/modes/interactive/status-line.js +11 -6
  116. package/dist/modes/interactive/status-quota.d.ts +44 -0
  117. package/dist/modes/interactive/status-quota.js +135 -0
  118. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  119. package/dist/modes/interactive/subagent-view.js +8 -0
  120. package/dist/modes/interactive/task-background.d.ts +31 -0
  121. package/dist/modes/interactive/task-background.js +68 -0
  122. package/dist/modes/interactive/tool-view.d.ts +7 -1
  123. package/dist/modes/interactive/tool-view.js +24 -1
  124. package/dist/modes/print/print-mode.d.ts +11 -0
  125. package/dist/modes/print/print-mode.js +36 -1
  126. package/dist/modes/rpc/commands.d.ts +2 -1
  127. package/dist/modes/rpc/commands.js +9 -1
  128. package/dist/rpc.d.ts +13 -0
  129. package/dist/rpc.js +3 -0
  130. package/dist/tools/task-ctl.d.ts +2 -0
  131. package/dist/tools/task-ctl.js +7 -2
  132. package/dist/tools/task.d.ts +11 -0
  133. package/dist/tools/task.js +20 -2
  134. package/dist/tui/components/editor.d.ts +2 -0
  135. package/dist/tui/components/editor.js +4 -0
  136. package/dist/tui/components/loader.d.ts +5 -1
  137. package/dist/tui/components/loader.js +18 -5
  138. package/dist/tui/keybindings.d.ts +8 -3
  139. package/dist/tui/keybindings.js +8 -3
  140. package/docs/agents.md +52 -28
  141. package/docs/en/host-api.md +5 -1
  142. package/docs/en/providers.md +8 -4
  143. package/docs/en/rpc.md +19 -7
  144. package/docs/en/sessions.md +3 -1
  145. package/docs/en/tui.md +84 -66
  146. package/docs/host-api.md +5 -1
  147. package/docs/providers.md +29 -12
  148. package/docs/rpc.md +19 -7
  149. package/docs/sessions.md +2 -1
  150. package/docs/tui-design.md +42 -29
  151. package/docs/tui.md +67 -49
  152. package/package.json +1 -1
@@ -13,12 +13,14 @@
13
13
  * - 显示条件:有排队 / 运行中 / 等审批的任务,或本会话里看着结束、还没查看过的任务(结束后最多 10 分钟);
14
14
  * `ui.agentBar: "off"`(嵌入宿主缺省)整个不显示。
15
15
  * - 聚焦(`Ctrl+B` / 空输入 `↓` / `/tasks`):列出本会话全部任务,`›` 标当前项,窗口跟着选择滚动,
16
- * 末行是按键提示。按键处理在 agent-ui.ts。
16
+ * 末行是按键提示(`↑↓ 选择 · Enter 打开 · b 转后台 · x 停止 · Esc 返回`,放不下时丢 b / x 说明)。
17
+ * 按键处理在 agent-ui.ts。
18
+ * - [W7-C] 等审批的行(含停靠在栏里的后台任务审批)整行警告色。
17
19
  * - 状态来自任务注册表(`TaskInfo`、`live().queued`)、子 Agent 折叠视图的跟踪(轮数、最近工具、用时,界面时钟)
18
20
  * 与 `permission_request.context.taskId`(等审批)。
19
21
  */
20
22
  import { msg } from "../../i18n/index.js";
21
- import { formatElapsed, truncateToWidth } from "../../tui.js";
23
+ import { formatElapsed, truncateToWidth, visibleWidth, } from "../../tui.js";
22
24
  export const BAR_ROWS = 3;
23
25
  /** 结束后未查看的任务在栏里保留多久。 */
24
26
  export const BAR_RETAIN_MS = 10 * 60_000;
@@ -224,8 +226,11 @@ export class AgentBar {
224
226
  const tail = [];
225
227
  if (hidden > 0)
226
228
  tail.push(m.bar.more(hidden));
227
- if (focused)
228
- tail.push(m.bar.keys(t.glyphs.arrowUp, t.glyphs.arrowDown));
229
+ if (focused) {
230
+ const [up, down] = [t.glyphs.arrowUp, t.glyphs.arrowDown];
231
+ const full = [...tail, m.bar.keysFull(up, down)].join(" · ");
232
+ tail.push(visibleWidth(full) <= width ? m.bar.keysFull(up, down) : m.bar.keys(up, down));
233
+ }
229
234
  if (tail.length > 0)
230
235
  out.push(truncateToWidth(t.fg("dim", tail.join(" · ")), width, t.glyphs.ellipsis));
231
236
  return out;
@@ -236,7 +241,9 @@ export class AgentBar {
236
241
  const agent = agentLabel(row);
237
242
  const lead = focused ? (selected ? t.fg("accent", g.prompt) : " ") + " " : "";
238
243
  const marker = t.fg(statusColor(row.status), g.tool);
239
- const id = selected ? t.bold(row.taskId) : row.taskId;
244
+ const approval = row.status === "approval";
245
+ const plainId = selected || approval ? t.bold(row.taskId) : row.taskId;
246
+ const id = approval ? t.fg("warning", plainId) : plainId;
240
247
  const description = row.description.replace(/\s+/g, " ").trim();
241
248
  const text = `${lead}${marker} ${id} ${t.fg("tool", agent)}` +
242
249
  t.fg("dim", " · ") +
@@ -13,10 +13,14 @@
13
13
  * 事件;这里在会话的 `options.log` 外面套一层,只把这类行转到消息区,其余照原样写日志。
14
14
  *
15
15
  * [W6-A] Agent 栏(agent-bar.ts,提示行之下、状态行之上)与子 Agent 视图(agent-view.ts,底部覆盖层):
16
- * - 键位:空输入时 `Ctrl+B`(有任务即可)或 `↓`(栏可见时)进栏(`app.agents.focus`,key-dispatch.ts 先问这里);
17
- * 栏内 ↑↓ 选、Enter 打开视图、Esc / Ctrl+B 返回,可打印字符回到输入框;
16
+ * - 键位:空输入时 `↓`(有任务即可,不要求栏可见;`app.agents.focus`,key-dispatch.ts 先问这里)进栏;
17
+ * 有字 / 栏关闭 / 没有任务时返回落空原因,由 key-dispatch 给提示 [W7-A];
18
+ * 栏内 ↑↓ 选、Enter 打开视图、Esc 返回,可打印字符回到输入框;
18
19
  * - `/tasks` 无参 = 聚焦栏(`ui.agentBar: "off"` 时仍是原来的选择器),`/tasks <id>` = 直接进视图;
19
20
  * - 等审批的任务按 `permission_request.context.taskId` 记下(栏与视图标题显示「等待审批」)。
21
+ * [W7-C] 栏内 `b` / `Ctrl+B` 转后台选中的任务、`x` 连按两次停止(task-background.ts);`subagent_background`
22
+ * 刷新工具行(后台样式)并对超时 / 宿主转的给一行提示;后台任务的审批停靠(approval-dock.ts):打开该任务
23
+ * 的视图即弹出,`permission_request` / `permission_resolved` 交给停靠判断。
20
24
  */
21
25
  import type { AgentSession, SessionEvent } from "../../agent/types.js";
22
26
  import type { SwitchRequest } from "../../cli/compose-session.js";
@@ -27,6 +31,7 @@ import type { CommandUi } from "./commands.js";
27
31
  import type { AgentKeys } from "./key-dispatch.js";
28
32
  import type { StatusArea } from "./status-area.js";
29
33
  import { FirstRunMerge } from "./approval-merge.js";
34
+ import type { ApprovalDock } from "./approval-dock.js";
30
35
  import type { NoticeLevel } from "./message-view.js";
31
36
  import type { PickerSpec } from "./pickers.js";
32
37
  import { PlanFlow } from "./plan-flow.js";
@@ -76,6 +81,10 @@ export declare class AgentUi {
76
81
  private restoreLog;
77
82
  /** 后台子 Agent 运行中:每秒重画跟随行的耗时(主会话空闲时 Loader 不转)。 */
78
83
  private ticker;
84
+ /** [W7-C] 后台任务的审批停靠(approval-ui.ts 建好后接上)。 */
85
+ dock: ApprovalDock | undefined;
86
+ /** [W7-C] 栏内 `x` 的双击确认。 */
87
+ private readonly stopArm;
79
88
  constructor(deps: AgentUiDeps);
80
89
  private keybindings;
81
90
  private barEnabled;
@@ -93,10 +102,19 @@ export declare class AgentUi {
93
102
  private showPlan;
94
103
  /** `/tasks`(无参):聚焦 Agent 栏;栏关闭(嵌入宿主缺省)时仍是选择器。 */
95
104
  focusBar(): Promise<void>;
96
- /** `app.agents.focus`(输入为空):`↓` 只在栏可见时进入,其它键(`Ctrl+B`)有任务即可。 */
105
+ /**
106
+ * `app.agents.focus`:输入为空且有任务即进栏(与 `/tasks` 一致,不要求栏可见)[W7-A];
107
+ * 否则返回落空原因——有字(有任务时)、栏关闭(有任务时)、没有任务(栏开着时)。
108
+ */
97
109
  private focusFromKey;
110
+ /** [W7-A] 运行提示行要不要带「↓ Agent 栏」:栏开着、有该显示的任务、没在栏里或视图里。 */
111
+ get reachable(): boolean;
98
112
  /** 栏聚焦时的按键;不在栏里返回 false。 */
99
113
  private barKey;
114
+ /** [W7-C] 栏内 `b` / `Ctrl+B`:转后台选中的任务(只对前台运行中的有效)。 */
115
+ private backgroundSelected;
116
+ /** [W7-C] 栏内 `x`:第一次提示「再按 x 停止」,1.5 s 内再按停止。 */
117
+ private stopSelected;
100
118
  /** `/tasks <id>` 与栏里 Enter:打开子 Agent 视图。 */
101
119
  openView(taskId: string): void;
102
120
  get viewing(): string | undefined;
@@ -13,24 +13,29 @@
13
13
  * 事件;这里在会话的 `options.log` 外面套一层,只把这类行转到消息区,其余照原样写日志。
14
14
  *
15
15
  * [W6-A] Agent 栏(agent-bar.ts,提示行之下、状态行之上)与子 Agent 视图(agent-view.ts,底部覆盖层):
16
- * - 键位:空输入时 `Ctrl+B`(有任务即可)或 `↓`(栏可见时)进栏(`app.agents.focus`,key-dispatch.ts 先问这里);
17
- * 栏内 ↑↓ 选、Enter 打开视图、Esc / Ctrl+B 返回,可打印字符回到输入框;
16
+ * - 键位:空输入时 `↓`(有任务即可,不要求栏可见;`app.agents.focus`,key-dispatch.ts 先问这里)进栏;
17
+ * 有字 / 栏关闭 / 没有任务时返回落空原因,由 key-dispatch 给提示 [W7-A];
18
+ * 栏内 ↑↓ 选、Enter 打开视图、Esc 返回,可打印字符回到输入框;
18
19
  * - `/tasks` 无参 = 聚焦栏(`ui.agentBar: "off"` 时仍是原来的选择器),`/tasks <id>` = 直接进视图;
19
20
  * - 等审批的任务按 `permission_request.context.taskId` 记下(栏与视图标题显示「等待审批」)。
21
+ * [W7-C] 栏内 `b` / `Ctrl+B` 转后台选中的任务、`x` 连按两次停止(task-background.ts);`subagent_background`
22
+ * 刷新工具行(后台样式)并对超时 / 宿主转的给一行提示;后台任务的审批停靠(approval-dock.ts):打开该任务
23
+ * 的视图即弹出,`permission_request` / `permission_resolved` 交给停靠判断。
20
24
  */
21
25
  import { registryOf, taskRegistryView } from "../../agent/subagent-registry.js";
22
26
  import { planController } from "../../plan/controller.js";
23
27
  import { msg } from "../../i18n/index.js";
24
- import { defaultKeybindings, isPrintableText, parseKey, } from "../../tui.js";
28
+ import { defaultKeybindings, isPrintableText, } from "../../tui.js";
25
29
  import { AgentBar } from "./agent-bar.js";
26
30
  import { AgentView } from "./agent-view.js";
27
31
  import { agentsPanel, planPanel, taskOutputPanel } from "./agent-panels.js";
28
32
  import { FirstRunMerge } from "./approval-merge.js";
29
33
  import { pasteImage } from "./clipboard-paste.js";
30
- import { backgroundJobText, limitReachedText, modelFallbackText } from "./event-notices.js";
34
+ import { backgroundJobText, limitReachedText, modelFallbackText, subagentBackgroundText, } from "./event-notices.js";
31
35
  import { editExternally } from "./external-editor.js";
32
36
  import { PlanFlow } from "./plan-flow.js";
33
37
  import { backgroundEndText, SubagentTracker } from "./subagent-view.js";
38
+ import { backgroundedText, StopArm } from "./task-background.js";
34
39
  import { listTasks, stopTask, taskLine, taskOutput } from "./tasks-report.js";
35
40
  /** [W6-A] `CommandUi` 的 Agent 栏 / 视图钩子(`/tasks`、`/tasks <id>`);晚绑定,装配时 agentUi 还没建。 */
36
41
  export function agentCommandHooks(get) {
@@ -49,7 +54,7 @@ export class AgentUi {
49
54
  /** [W6-A] key-dispatch.ts 的 Agent 栏入口。 */
50
55
  keys = {
51
56
  handleKey: (data) => this.barKey(data),
52
- focus: (data) => this.focusFromKey(data),
57
+ focus: () => this.focusFromKey(),
53
58
  };
54
59
  view;
55
60
  /** 等审批的请求 → 任务。 */
@@ -57,6 +62,10 @@ export class AgentUi {
57
62
  restoreLog;
58
63
  /** 后台子 Agent 运行中:每秒重画跟随行的耗时(主会话空闲时 Loader 不转)。 */
59
64
  ticker;
65
+ /** [W7-C] 后台任务的审批停靠(approval-ui.ts 建好后接上)。 */
66
+ dock;
67
+ /** [W7-C] 栏内 `x` 的双击确认。 */
68
+ stopArm = new StopArm();
60
69
  constructor(deps) {
61
70
  this.deps = deps;
62
71
  this.merge = new FirstRunMerge({ sessionId: () => deps.session().state.sessionId });
@@ -107,6 +116,7 @@ export class AgentUi {
107
116
  this.deps.notice("info", msg().agents.view.removed(viewing));
108
117
  this.bar.reset();
109
118
  this.approvals.clear();
119
+ this.dock?.reset();
110
120
  this.deps.subagents.clear();
111
121
  this.stopTicker();
112
122
  this.plan.attach(session);
@@ -171,10 +181,14 @@ export class AgentUi {
171
181
  switch (event.type) {
172
182
  case "subagent_start":
173
183
  case "subagent_update":
184
+ case "subagent_background":
174
185
  case "subagent_end": {
175
186
  const state = this.deps.subagents.onEvent(event);
176
187
  if (state === undefined)
177
188
  return true;
189
+ // [W7-C] 人按的已有底部提示;超时 / 宿主转的留一行
190
+ if (event.type === "subagent_background" && event.reason !== "user")
191
+ this.deps.notice("info", subagentBackgroundText(event, state.agent));
178
192
  this.deps.tools().get(state.parentToolCallId)?.refresh();
179
193
  // 视图里看的任务又开跑(续聊、被释放后重开):重新接上它的子会话
180
194
  if (event.type === "subagent_start" && this.view?.component.task === event.taskId)
@@ -198,11 +212,20 @@ export class AgentUi {
198
212
  const taskId = event.context?.taskId;
199
213
  if (taskId !== undefined)
200
214
  this.approvals.set(event.requestId, taskId);
215
+ this.dock?.observe(event);
201
216
  return false;
202
217
  }
203
218
  case "permission_resolved":
204
219
  this.approvals.delete(event.requestId);
220
+ this.dock?.observe(event);
205
221
  return false;
222
+ case "agent_settled": {
223
+ // 主会话空闲了:停靠的审批看能不能弹(运行指示要等这个事件分派完才变空闲)
224
+ const dock = this.dock;
225
+ if (dock !== undefined && dock.size > 0)
226
+ setTimeout(() => dock.poke(), 0);
227
+ return false;
228
+ }
206
229
  default:
207
230
  return false;
208
231
  }
@@ -250,23 +273,39 @@ export class AgentUi {
250
273
  }
251
274
  this.deps.render();
252
275
  }
253
- /** `app.agents.focus`(输入为空):`↓` 只在栏可见时进入,其它键(`Ctrl+B`)有任务即可。 */
254
- focusFromKey(data) {
255
- if (this.view !== undefined || !this.barEnabled())
256
- return false;
257
- if (parseKey(data)?.id === "down" && !this.bar.visible)
276
+ /**
277
+ * `app.agents.focus`:输入为空且有任务即进栏(与 `/tasks` 一致,不要求栏可见)[W7-A];
278
+ * 否则返回落空原因——有字(有任务时)、栏关闭(有任务时)、没有任务(栏开着时)。
279
+ */
280
+ focusFromKey() {
281
+ if (this.view !== undefined)
258
282
  return false;
283
+ const hasTasks = this.bar.all().length > 0;
284
+ if (!this.barEnabled())
285
+ return hasTasks && this.deps.editor.isEmpty() ? "disabled" : false;
286
+ if (!this.deps.editor.isEmpty())
287
+ return hasTasks ? "busy-input" : false;
259
288
  if (!this.bar.focus())
260
- return false;
289
+ return "empty";
261
290
  this.deps.render();
262
291
  return true;
263
292
  }
293
+ /** [W7-A] 运行提示行要不要带「↓ Agent 栏」:栏开着、有该显示的任务、没在栏里或视图里。 */
294
+ get reachable() {
295
+ return this.view === undefined && !this.bar.focused && this.bar.visible;
296
+ }
264
297
  /** 栏聚焦时的按键;不在栏里返回 false。 */
265
298
  barKey(data) {
266
299
  if (!this.bar.focused)
267
300
  return false;
268
301
  const keys = this.keybindings();
269
- if (keys.matches(data, "tui.select.up")) {
302
+ if (data !== "x")
303
+ this.stopArm.reset();
304
+ if (data === "b" || keys.matches(data, "app.tasks.background"))
305
+ this.backgroundSelected();
306
+ else if (data === "x")
307
+ void this.stopSelected();
308
+ else if (keys.matches(data, "tui.select.up")) {
270
309
  if (!this.bar.move(-1))
271
310
  this.bar.blur();
272
311
  }
@@ -288,6 +327,33 @@ export class AgentUi {
288
327
  this.deps.render();
289
328
  return true;
290
329
  }
330
+ /** [W7-C] 栏内 `b` / `Ctrl+B`:转后台选中的任务(只对前台运行中的有效)。 */
331
+ backgroundSelected() {
332
+ const taskId = this.bar.selected();
333
+ if (taskId === undefined)
334
+ return;
335
+ const moved = this.deps.session().backgroundTask(taskId, "user");
336
+ this.deps.hint(moved.length > 0 ? backgroundedText(moved) : msg().agents.background.notForeground(taskId));
337
+ }
338
+ /** [W7-C] 栏内 `x`:第一次提示「再按 x 停止」,1.5 s 内再按停止。 */
339
+ async stopSelected() {
340
+ const taskId = this.bar.selected();
341
+ if (taskId === undefined)
342
+ return;
343
+ const sessionId = this.deps.session().state.sessionId;
344
+ if (taskRegistryView(sessionId)?.get(taskId)?.status !== "running") {
345
+ this.stopArm.reset();
346
+ this.deps.hint(msg().agents.stop.ended(taskId));
347
+ return;
348
+ }
349
+ if (this.stopArm.press(taskId, this.deps.now()) === "arm") {
350
+ this.deps.hint(msg().agents.stop.confirm(taskId));
351
+ return;
352
+ }
353
+ this.deps.hint(msg().agents.view.stopped(taskId));
354
+ await stopTask(sessionId, taskId);
355
+ this.deps.render();
356
+ }
291
357
  /** `/tasks <id>` 与栏里 Enter:打开子 Agent 视图。 */
292
358
  openView(taskId) {
293
359
  const sessionId = this.deps.session().state.sessionId;
@@ -309,6 +375,7 @@ export class AgentUi {
309
375
  siblings: () => this.bar.all().map((row) => row.taskId),
310
376
  close: () => this.closeView(),
311
377
  stop: (id) => stopTask(this.deps.session().state.sessionId, id),
378
+ background: (id) => this.deps.session().backgroundTask(id, "user"),
312
379
  messages: {
313
380
  ...(ui.showThinking !== undefined ? { showThinking: ui.showThinking } : {}),
314
381
  ...(ui.markdown !== undefined ? { markdown: ui.markdown } : {}),
@@ -319,6 +386,8 @@ export class AgentUi {
319
386
  this.bar.blur();
320
387
  const handle = this.deps.tui.showOverlay(component, { anchor: "bottom" });
321
388
  this.view = { component, handle };
389
+ // [W7-C] 正在看的任务有停靠的审批:立即弹出(盖在视图上)
390
+ this.dock?.poke();
322
391
  this.deps.render();
323
392
  }
324
393
  get viewing() {
@@ -16,6 +16,8 @@
16
16
  * `↑` / PgUp 上翻(暂停跟随)、`↓` / PgDn 下翻、End(暂停时也可 `f`)回到跟随;输入非空时 Esc 清空。
17
17
  * Esc 不中断父会话,停止子任务用 `/tasks stop <id>`(视图里只认这一条命令)。
18
18
  * 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
19
+ * [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
20
+ * 视图里另认 `/tasks bg [id]`。
19
21
  */
20
22
  import { type Component, type Focusable, type Keybindings, type Theme } from "../../tui.js";
21
23
  import { type TaskSource } from "./agent-bar.js";
@@ -37,6 +39,8 @@ export interface AgentViewDeps {
37
39
  onSwitch?(taskId: string): void;
38
40
  close(): void;
39
41
  stop(taskId: string): Promise<void>;
42
+ /** [W7-C] 人工转后台;返回转了的 taskId。 */
43
+ background?(taskId: string): string[];
40
44
  /** 消息区的显示选项(思考块、Markdown、紧凑)。 */
41
45
  messages?: Omit<MessageViewOptions, "theme">;
42
46
  }
@@ -16,12 +16,15 @@
16
16
  * `↑` / PgUp 上翻(暂停跟随)、`↓` / PgDn 下翻、End(暂停时也可 `f`)回到跟随;输入非空时 Esc 清空。
17
17
  * Esc 不中断父会话,停止子任务用 `/tasks stop <id>`(视图里只认这一条命令)。
18
18
  * 所查看的任务在等审批时标题显示「等待审批」,审批框照常以覆盖层弹在视图上面。
19
+ * [W7-C] `Ctrl+B`(`app.tasks.background`)转后台正在看的任务(只对前台任务有效,否则落回编辑器);
20
+ * 视图里另认 `/tasks bg [id]`。
19
21
  */
20
22
  import { msg } from "../../i18n/index.js";
21
23
  import { Editor, isPrintableText, parseKey, truncateToWidth, visibleWidth, } from "../../tui.js";
22
24
  import { formatTokenCount } from "../session-report.js";
23
25
  import { agentLabel, rowFacts, statusColor, taskRow } from "./agent-bar.js";
24
26
  import { AmaTranscript, ExternalTranscript, readBranch, } from "./agent-transcript.js";
27
+ import { backgroundedText } from "./task-background.js";
25
28
  export class AgentView {
26
29
  taskId;
27
30
  deps;
@@ -142,7 +145,10 @@ export class AgentView {
142
145
  const taskId = this.taskId;
143
146
  if (trimmed.startsWith("/")) {
144
147
  const stop = /^\/tasks\s+stop(?:\s+(\S+))?\s*$/.exec(trimmed);
145
- if (stop === null)
148
+ const bg = /^\/tasks\s+bg(?:\s+(\S+))?\s*$/.exec(trimmed);
149
+ if (bg !== null && this.deps.background !== undefined)
150
+ this.flash = backgroundedText(this.deps.background(bg[1] ?? taskId));
151
+ else if (stop === null)
146
152
  this.flash = m.commandsHere;
147
153
  else {
148
154
  const target = stop[1] ?? taskId;
@@ -175,6 +181,13 @@ export class AgentView {
175
181
  const empty = this.editor.isEmpty();
176
182
  const id = parseKey(data)?.id;
177
183
  this.flash = undefined;
184
+ if (keys.matches(data, "app.tasks.background") && this.deps.background !== undefined) {
185
+ const moved = this.deps.background(this.taskId);
186
+ if (moved.length > 0) {
187
+ this.flash = backgroundedText(moved);
188
+ return;
189
+ }
190
+ }
178
191
  if (keys.matches(data, "tui.select.cancel") && !this.editor.isCompletionOpen) {
179
192
  if (!empty)
180
193
  this.editor.clear();
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 后台任务的审批停靠(docs/agents-concurrency-plan.md §2.7、D6)。[W7-C]
3
+ *
4
+ * 套在审批对话框 broker 外面:请求带 `context.taskId` 且该任务在后台运行(含转后台)时,主会话忙、输入框有
5
+ * 草稿或已有覆盖层就不弹框,先「停靠」——Agent 栏该任务行显示「等待审批」(agent-ui.ts 按
6
+ * `permission_request` 记下),运行提示行附「↓ 处理审批」;满足下列任一条件立即弹出:
7
+ * - 主会话空闲、输入为空、没有覆盖层(`ready`,每 250 ms 与相关事件后各对一次);
8
+ * - 正在子 Agent 视图里看这个任务(栏里 Enter 打开视图即弹出);
9
+ * - 停靠期间又来了不能停靠的请求(主会话自己的、前台任务的):审批链是串行的,它排在停靠的请求后面,
10
+ * 不放出来就会一直等,所以先弹停靠的那个——主会话与前台任务的审批照旧立即弹出。
11
+ *
12
+ * 停靠期间子任务阻塞在权限等待;超时 / 取消(链的 signal)照旧,停靠项随之撤掉、按 undefined 交回链。
13
+ * 前台任务与主会话的请求直接交给内层 broker,行为不变。
14
+ */
15
+ import type { SessionEvent } from "../../agent/types.js";
16
+ import type { ApprovalBroker, ApprovalDecision, ApprovalRequest } from "../../permissions/types.js";
17
+ export declare const DOCK_POLL_MS = 250;
18
+ export interface DockHost {
19
+ /** 任务在后台运行(停靠候选)。 */
20
+ dockable(taskId: string): boolean;
21
+ /** 现在可以弹出这个任务的审批(主会话空闲且空输入且无覆盖层,或正在看它的视图)。 */
22
+ ready(taskId: string): boolean;
23
+ /** 停靠集合变了(栏与运行提示行重画)。 */
24
+ changed(): void;
25
+ pollMs?: number;
26
+ }
27
+ export declare class ApprovalDock implements ApprovalBroker {
28
+ private readonly inner;
29
+ private readonly host;
30
+ private readonly docked;
31
+ /** 已发出、还没答复的不可停靠请求(主会话 / 前台任务)。 */
32
+ private readonly others;
33
+ private timer;
34
+ constructor(inner: ApprovalBroker, host: DockHost);
35
+ ask(request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalDecision | undefined>;
36
+ /** 停靠中的任务。 */
37
+ tasks(): ReadonlySet<string>;
38
+ get size(): number;
39
+ /** 会话事件:记下不可停靠的请求;停靠期间来了这种请求就放出停靠的(见文件头)。 */
40
+ observe(event: SessionEvent): void;
41
+ /** 对一次弹出条件(定时器、视图打开、回合结束)。 */
42
+ poke(): void;
43
+ /** 换会话:旧会话的未答复请求不再有 `permission_resolved` 到这里。 */
44
+ reset(): void;
45
+ /** 退出:停靠的请求按 undefined 交回链(链按拒绝处理)。 */
46
+ dispose(): void;
47
+ private release;
48
+ private remove;
49
+ private startPolling;
50
+ private stopPolling;
51
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 后台任务的审批停靠(docs/agents-concurrency-plan.md §2.7、D6)。[W7-C]
3
+ *
4
+ * 套在审批对话框 broker 外面:请求带 `context.taskId` 且该任务在后台运行(含转后台)时,主会话忙、输入框有
5
+ * 草稿或已有覆盖层就不弹框,先「停靠」——Agent 栏该任务行显示「等待审批」(agent-ui.ts 按
6
+ * `permission_request` 记下),运行提示行附「↓ 处理审批」;满足下列任一条件立即弹出:
7
+ * - 主会话空闲、输入为空、没有覆盖层(`ready`,每 250 ms 与相关事件后各对一次);
8
+ * - 正在子 Agent 视图里看这个任务(栏里 Enter 打开视图即弹出);
9
+ * - 停靠期间又来了不能停靠的请求(主会话自己的、前台任务的):审批链是串行的,它排在停靠的请求后面,
10
+ * 不放出来就会一直等,所以先弹停靠的那个——主会话与前台任务的审批照旧立即弹出。
11
+ *
12
+ * 停靠期间子任务阻塞在权限等待;超时 / 取消(链的 signal)照旧,停靠项随之撤掉、按 undefined 交回链。
13
+ * 前台任务与主会话的请求直接交给内层 broker,行为不变。
14
+ */
15
+ export const DOCK_POLL_MS = 250;
16
+ export class ApprovalDock {
17
+ inner;
18
+ host;
19
+ docked = [];
20
+ /** 已发出、还没答复的不可停靠请求(主会话 / 前台任务)。 */
21
+ others = new Set();
22
+ timer;
23
+ constructor(inner, host) {
24
+ this.inner = inner;
25
+ this.host = host;
26
+ }
27
+ ask(request, signal) {
28
+ const taskId = request.context?.taskId;
29
+ if (taskId === undefined ||
30
+ signal.aborted ||
31
+ !this.host.dockable(taskId) ||
32
+ this.others.size > 0 ||
33
+ this.host.ready(taskId))
34
+ return this.inner.ask(request, signal);
35
+ return new Promise((resolve) => {
36
+ const entry = {
37
+ taskId,
38
+ request,
39
+ signal,
40
+ resolve,
41
+ onAbort: () => {
42
+ this.remove(entry);
43
+ resolve(undefined);
44
+ },
45
+ };
46
+ signal.addEventListener("abort", entry.onAbort, { once: true });
47
+ this.docked.push(entry);
48
+ this.startPolling();
49
+ this.host.changed();
50
+ });
51
+ }
52
+ /** 停靠中的任务。 */
53
+ tasks() {
54
+ return new Set(this.docked.map((entry) => entry.taskId));
55
+ }
56
+ get size() {
57
+ return this.docked.length;
58
+ }
59
+ /** 会话事件:记下不可停靠的请求;停靠期间来了这种请求就放出停靠的(见文件头)。 */
60
+ observe(event) {
61
+ if (event.type === "permission_request") {
62
+ const taskId = event.context?.taskId;
63
+ if (taskId !== undefined && this.host.dockable(taskId))
64
+ return;
65
+ this.others.add(event.requestId);
66
+ this.poke();
67
+ }
68
+ else if (event.type === "permission_resolved")
69
+ this.others.delete(event.requestId);
70
+ }
71
+ /** 对一次弹出条件(定时器、视图打开、回合结束)。 */
72
+ poke() {
73
+ for (const entry of [...this.docked])
74
+ if (this.others.size > 0 || this.host.ready(entry.taskId))
75
+ this.release(entry);
76
+ }
77
+ /** 换会话:旧会话的未答复请求不再有 `permission_resolved` 到这里。 */
78
+ reset() {
79
+ this.others.clear();
80
+ }
81
+ /** 退出:停靠的请求按 undefined 交回链(链按拒绝处理)。 */
82
+ dispose() {
83
+ for (const entry of [...this.docked])
84
+ entry.onAbort();
85
+ this.others.clear();
86
+ }
87
+ release(entry) {
88
+ this.remove(entry);
89
+ entry.resolve(this.inner.ask(entry.request, entry.signal));
90
+ }
91
+ remove(entry) {
92
+ const at = this.docked.indexOf(entry);
93
+ if (at < 0)
94
+ return;
95
+ this.docked.splice(at, 1);
96
+ entry.signal.removeEventListener("abort", entry.onAbort);
97
+ if (this.docked.length === 0)
98
+ this.stopPolling();
99
+ this.host.changed();
100
+ }
101
+ startPolling() {
102
+ if (this.timer !== undefined)
103
+ return;
104
+ this.timer = setInterval(() => this.poke(), this.host.pollMs ?? DOCK_POLL_MS);
105
+ this.timer.unref?.();
106
+ }
107
+ stopPolling() {
108
+ if (this.timer !== undefined)
109
+ clearInterval(this.timer);
110
+ this.timer = undefined;
111
+ }
112
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 交互界面的审批接线,从 interactive-mode.ts 拆出(保持装配文件 ≤ 600 行)。[W7-C]
3
+ *
4
+ * broker 由外到内:首次运行合并(approval-merge.ts)→ 后台任务停靠(approval-dock.ts)→ 底部对话框
5
+ * (approval-dialog.ts)。对话框打开 / 关闭时禁用编辑器提交、标记运行中的工具「等待确认」、运行提示行换
6
+ * 「等待确认」。
7
+ */
8
+ import type { AgentSession } from "../../agent/types.js";
9
+ import type { ApprovalBroker } from "../../permissions/types.js";
10
+ import type { Component, Editor, Keybindings, OverlayHandle, TUI, Theme } from "../../tui.js";
11
+ import type { AgentUi } from "./agent-ui.js";
12
+ import { ApprovalDock } from "./approval-dock.js";
13
+ import type { NoticeLevel } from "./message-view.js";
14
+ import type { RunIndicator } from "./run-indicator.js";
15
+ import type { ToolTracker } from "./tool-view.js";
16
+ export interface OverlayHooks {
17
+ showOverlay(component: Component): OverlayHandle;
18
+ onOpen(): void;
19
+ onClose(): void;
20
+ }
21
+ export declare function approvalOverlayHooks(deps: {
22
+ tui: TUI;
23
+ editor: Editor;
24
+ tools(): ToolTracker;
25
+ indicator: RunIndicator;
26
+ }): OverlayHooks;
27
+ export interface ApprovalUiDeps {
28
+ theme: Theme;
29
+ keys: Keybindings;
30
+ tui: TUI;
31
+ editor: Editor;
32
+ indicator: RunIndicator;
33
+ agentUi: AgentUi;
34
+ hooks: OverlayHooks;
35
+ session(): AgentSession;
36
+ notice(level: NoticeLevel, text: string): void;
37
+ render(): void;
38
+ }
39
+ /** 组装界面 broker,并把停靠接到 Agent 栏与运行提示行。 */
40
+ export declare function createApprovalUi(deps: ApprovalUiDeps): {
41
+ broker: ApprovalBroker;
42
+ dock: ApprovalDock;
43
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 交互界面的审批接线,从 interactive-mode.ts 拆出(保持装配文件 ≤ 600 行)。[W7-C]
3
+ *
4
+ * broker 由外到内:首次运行合并(approval-merge.ts)→ 后台任务停靠(approval-dock.ts)→ 底部对话框
5
+ * (approval-dialog.ts)。对话框打开 / 关闭时禁用编辑器提交、标记运行中的工具「等待确认」、运行提示行换
6
+ * 「等待确认」。
7
+ */
8
+ import { taskRegistryView } from "../../agent/subagent-registry.js";
9
+ import { msg } from "../../i18n/index.js";
10
+ import { ApprovalDialogBroker, approvalOutcomeText } from "./approval-dialog.js";
11
+ import { ApprovalDock } from "./approval-dock.js";
12
+ import { mergingBroker } from "./approval-merge.js";
13
+ export function approvalOverlayHooks(deps) {
14
+ const { tui, editor, indicator } = deps;
15
+ return {
16
+ showOverlay: (component) => tui.showOverlay(component, { anchor: "bottom" }),
17
+ onOpen: () => {
18
+ editor.disableSubmit = true;
19
+ deps.tools().setAwaiting(true);
20
+ indicator.setApproval(true);
21
+ },
22
+ onClose: () => {
23
+ editor.disableSubmit = false;
24
+ deps.tools().setAwaiting(false);
25
+ indicator.setApproval(false);
26
+ },
27
+ };
28
+ }
29
+ /** 组装界面 broker,并把停靠接到 Agent 栏与运行提示行。 */
30
+ export function createApprovalUi(deps) {
31
+ const { theme, keys, tui, editor, indicator, agentUi, hooks, notice } = deps;
32
+ const taskInfo = (taskId) => taskRegistryView(deps.session().state.sessionId)?.get(taskId);
33
+ const taskAgent = (taskId) => taskInfo(taskId)?.agent;
34
+ const dialog = new ApprovalDialogBroker({
35
+ theme,
36
+ keybindings: keys,
37
+ cwd: deps.session().state.cwd,
38
+ permissionMode: () => deps.session().state.permissionMode,
39
+ taskAgent,
40
+ externalRunner: (agent) => agentUi.merge.externalRunner(agent),
41
+ ...hooks,
42
+ report: (request, outcome) => {
43
+ if (outcome === "deny" || outcome === "cancelled") {
44
+ const text = approvalOutcomeText(request, outcome, taskAgent);
45
+ notice(outcome === "deny" ? "info" : "warn", text);
46
+ }
47
+ },
48
+ });
49
+ const dock = new ApprovalDock(dialog, {
50
+ dockable: (taskId) => {
51
+ const info = taskInfo(taskId);
52
+ return info?.status === "running" && info.background === true;
53
+ },
54
+ ready: (taskId) => agentUi.viewing === taskId || (!tui.hasOverlay && !indicator.busy && editor.isEmpty()),
55
+ changed: () => {
56
+ indicator.sync();
57
+ deps.render();
58
+ },
59
+ });
60
+ agentUi.dock = dock;
61
+ indicator.docked = () => dock.size > 0;
62
+ const broker = mergingBroker(dock, agentUi.merge, (request) => notice("info", msg().interactive.app.followed(approvalOutcomeText(request, "allow", taskAgent))));
63
+ return { broker, dock };
64
+ }
@@ -72,7 +72,10 @@ export const PANEL_COMMANDS = {
72
72
  await ui.agentBar();
73
73
  return true;
74
74
  }
75
- if (parts.length === 1 && parts[0] !== "stop" && ui.agentView !== undefined) {
75
+ if (parts.length === 1 &&
76
+ parts[0] !== "stop" &&
77
+ parts[0] !== "bg" &&
78
+ ui.agentView !== undefined) {
76
79
  await ui.agentView(parts[0]);
77
80
  return true;
78
81
  }
@@ -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;