@armadra/agent 0.6.2 → 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 (130) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CHANGELOG.zh-CN.md +44 -0
  3. package/README.md +168 -575
  4. package/README.zh-CN.md +165 -596
  5. package/dist/agent/session.d.ts +6 -0
  6. package/dist/agent/session.js +8 -0
  7. package/dist/agent/subagent-background.d.ts +75 -0
  8. package/dist/agent/subagent-background.js +209 -0
  9. package/dist/agent/subagent-registry.d.ts +24 -5
  10. package/dist/agent/subagent-registry.js +64 -92
  11. package/dist/agent/types-w5.d.ts +11 -1
  12. package/dist/agent/types.d.ts +5 -0
  13. package/dist/agents/builtin.js +0 -1
  14. package/dist/agents/external.js +0 -1
  15. package/dist/agents/parse.js +4 -3
  16. package/dist/agents/result.d.ts +7 -1
  17. package/dist/agents/result.js +20 -1
  18. package/dist/agents/task-control.d.ts +13 -2
  19. package/dist/agents/task-record.d.ts +13 -1
  20. package/dist/agents/task-record.js +32 -0
  21. package/dist/agents/types.d.ts +5 -1
  22. package/dist/ai/providers/discovered-cache.d.ts +10 -4
  23. package/dist/ai/providers/discovered-cache.js +30 -14
  24. package/dist/auth/chatgpt/backend-client.d.ts +6 -5
  25. package/dist/auth/chatgpt/backend-client.js +8 -7
  26. package/dist/bundle/ama.cjs +1691 -500
  27. package/dist/cli/compose-agents.d.ts +2 -1
  28. package/dist/cli/compose-agents.js +11 -2
  29. package/dist/cli/subcommands/models-discover.d.ts +1 -0
  30. package/dist/cli/subcommands/models-discover.js +12 -2
  31. package/dist/config/json-schema.js +9 -2
  32. package/dist/config/key-docs.js +2 -2
  33. package/dist/config/merge.d.ts +1 -1
  34. package/dist/config/merge.js +20 -3
  35. package/dist/config/schema-w5.js +4 -2
  36. package/dist/config/schema.js +2 -0
  37. package/dist/config/settings-registry.js +4 -0
  38. package/dist/config/types-w5.d.ts +10 -0
  39. package/dist/config/types-w5.js +2 -0
  40. package/dist/config/types.d.ts +3 -1
  41. package/dist/git/info.d.ts +19 -0
  42. package/dist/git/info.js +64 -8
  43. package/dist/i18n/catalog.d.ts +45 -8
  44. package/dist/i18n/messages/agents.d.ts +56 -0
  45. package/dist/i18n/messages/agents.js +58 -2
  46. package/dist/i18n/messages/config-keys.d.ts +6 -0
  47. package/dist/i18n/messages/config-keys.js +16 -10
  48. package/dist/i18n/messages/config.d.ts +6 -0
  49. package/dist/i18n/messages/interactive-startup.d.ts +0 -16
  50. package/dist/i18n/messages/interactive-startup.js +0 -16
  51. package/dist/i18n/messages/interactive.d.ts +23 -16
  52. package/dist/i18n/messages/interactive.js +25 -0
  53. package/dist/i18n/messages/print.d.ts +4 -0
  54. package/dist/i18n/messages/print.js +4 -0
  55. package/dist/i18n/messages/report.js +4 -4
  56. package/dist/i18n/messages/settings.d.ts +6 -0
  57. package/dist/i18n/messages/settings.js +6 -0
  58. package/dist/i18n/messages/subcommands-config.d.ts +4 -0
  59. package/dist/i18n/messages/subcommands-config.js +4 -0
  60. package/dist/i18n/messages/subcommands.d.ts +4 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/modes/commands-core.js +8 -4
  63. package/dist/modes/interactive/agent-bar.d.ts +3 -1
  64. package/dist/modes/interactive/agent-bar.js +12 -5
  65. package/dist/modes/interactive/agent-ui.d.ts +21 -3
  66. package/dist/modes/interactive/agent-ui.js +81 -12
  67. package/dist/modes/interactive/agent-view.d.ts +4 -0
  68. package/dist/modes/interactive/agent-view.js +14 -1
  69. package/dist/modes/interactive/approval-dock.d.ts +51 -0
  70. package/dist/modes/interactive/approval-dock.js +112 -0
  71. package/dist/modes/interactive/approval-ui.d.ts +43 -0
  72. package/dist/modes/interactive/approval-ui.js +64 -0
  73. package/dist/modes/interactive/commands.js +4 -1
  74. package/dist/modes/interactive/event-notices.d.ts +6 -1
  75. package/dist/modes/interactive/event-notices.js +7 -1
  76. package/dist/modes/interactive/interactive-mode.d.ts +4 -2
  77. package/dist/modes/interactive/interactive-mode.js +33 -38
  78. package/dist/modes/interactive/key-dispatch.d.ts +21 -4
  79. package/dist/modes/interactive/key-dispatch.js +60 -8
  80. package/dist/modes/interactive/line/line-mode.js +5 -0
  81. package/dist/modes/interactive/run-indicator.d.ts +15 -0
  82. package/dist/modes/interactive/run-indicator.js +37 -5
  83. package/dist/modes/interactive/session-events.js +5 -1
  84. package/dist/modes/interactive/startup-header.d.ts +29 -14
  85. package/dist/modes/interactive/startup-header.js +93 -59
  86. package/dist/modes/interactive/startup-logo.d.ts +83 -0
  87. package/dist/modes/interactive/startup-logo.js +183 -0
  88. package/dist/modes/interactive/status-area.d.ts +18 -0
  89. package/dist/modes/interactive/status-area.js +67 -1
  90. package/dist/modes/interactive/status-bar.d.ts +9 -1
  91. package/dist/modes/interactive/status-bar.js +43 -11
  92. package/dist/modes/interactive/status-line.d.ts +2 -0
  93. package/dist/modes/interactive/status-line.js +11 -6
  94. package/dist/modes/interactive/status-quota.d.ts +44 -0
  95. package/dist/modes/interactive/status-quota.js +135 -0
  96. package/dist/modes/interactive/subagent-view.d.ts +1 -0
  97. package/dist/modes/interactive/subagent-view.js +8 -0
  98. package/dist/modes/interactive/task-background.d.ts +31 -0
  99. package/dist/modes/interactive/task-background.js +68 -0
  100. package/dist/modes/interactive/tool-view.d.ts +7 -1
  101. package/dist/modes/interactive/tool-view.js +24 -1
  102. package/dist/modes/print/print-mode.d.ts +11 -0
  103. package/dist/modes/print/print-mode.js +36 -1
  104. package/dist/modes/rpc/commands.d.ts +2 -1
  105. package/dist/modes/rpc/commands.js +9 -1
  106. package/dist/rpc.d.ts +13 -0
  107. package/dist/rpc.js +3 -0
  108. package/dist/tools/task-ctl.d.ts +2 -0
  109. package/dist/tools/task-ctl.js +7 -2
  110. package/dist/tools/task.d.ts +11 -0
  111. package/dist/tools/task.js +20 -2
  112. package/dist/tui/components/editor.d.ts +2 -0
  113. package/dist/tui/components/editor.js +4 -0
  114. package/dist/tui/components/loader.d.ts +5 -1
  115. package/dist/tui/components/loader.js +18 -5
  116. package/dist/tui/keybindings.d.ts +8 -3
  117. package/dist/tui/keybindings.js +8 -3
  118. package/docs/agents.md +52 -28
  119. package/docs/en/host-api.md +5 -1
  120. package/docs/en/providers.md +1 -1
  121. package/docs/en/rpc.md +19 -7
  122. package/docs/en/sessions.md +3 -1
  123. package/docs/en/tui.md +84 -66
  124. package/docs/host-api.md +5 -1
  125. package/docs/providers.md +4 -2
  126. package/docs/rpc.md +19 -7
  127. package/docs/sessions.md +2 -1
  128. package/docs/tui-design.md +42 -29
  129. package/docs/tui.md +67 -49
  130. package/package.json +1 -1
@@ -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;
@@ -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,8 +32,7 @@ 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";
@@ -45,12 +45,14 @@ import { modelPickerFor } from "./model-picker.js";
45
45
  import { createRewindFlow } from "./rewind-flow.js";
46
46
  import { openTraceView } from "./trace-view.js";
47
47
  import { StartupHeader } from "./startup-header.js";
48
+ import { playStartupAnimation } from "./startup-logo.js";
48
49
  import { QueueView, RunIndicator } from "./run-indicator.js";
49
50
  import { StatusArea, statusLineSlash } from "./status-area.js";
50
51
  import { createSessionEventHandler } from "./session-events.js";
51
52
  import { SubagentTracker } from "./subagent-view.js";
52
53
  import { loadKeys, processTerminal } from "./terminal-setup.js";
53
54
  import { ToolTracker } from "./tool-view.js";
55
+ import { backgroundTasks, blockingTasks, keyLabel, runningBackground } from "./task-background.js";
54
56
  const HINT_MS = 2500;
55
57
  export function runInteractiveMode(runtime, context, options = {}) {
56
58
  const terminal = options.terminal ?? processTerminal();
@@ -131,6 +133,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
131
133
  tui.addChild(hint);
132
134
  tui.addChild(area.rate);
133
135
  tui.addChild(status);
136
+ tui.addChild(area.quota);
134
137
  // ---- 小工具 ---------------------------------------------------------------
135
138
  let finished = false;
136
139
  let hintTimer;
@@ -154,7 +157,11 @@ export function runInteractiveMode(runtime, context, options = {}) {
154
157
  }
155
158
  render();
156
159
  };
157
- const indicator = new RunIndicator({ theme, loader, slot: loaderSlot, tools, render });
160
+ const backgroundKey = keyLabel(keys, "app.tasks.background");
161
+ const indicator = new RunIndicator({
162
+ ...{ theme, loader, slot: loaderSlot, tools, render },
163
+ ...(backgroundKey !== undefined ? { backgroundKey } : {}),
164
+ });
158
165
  const setQueue = (steering, followUp) => {
159
166
  status.setQueue(steering.length, followUp.length);
160
167
  queueView.setQueue(steering, followUp);
@@ -177,7 +184,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
177
184
  const startupLevel = startupScreenLevel(runtime);
178
185
  const home = env["HOME"] ?? env["USERPROFILE"];
179
186
  const info = startupInfo(runtime, home);
180
- const header = (level) => new StartupHeader(info, { theme, level, ...(ui.compact === true ? { compact: true } : {}) });
187
+ const header = (level) => new StartupHeader(info, { theme, level, logo: ui.logo, compact: ui.compact });
181
188
  /** 清空消息区(切换会话、/tree):只留一行头。 */
182
189
  const resetView = () => {
183
190
  view.reset();
@@ -358,21 +365,12 @@ export function runInteractiveMode(runtime, context, options = {}) {
358
365
  exit: (code) => exit(code),
359
366
  onInterrupted: (empty) => void rewind.afterInterrupt(empty),
360
367
  agents: () => agentUi.keys,
368
+ downGlyph: theme.glyphs.arrowDown,
369
+ backgroundTasks: () => backgroundTasks(session),
370
+ runningBackground: () => runningBackground(session),
361
371
  }));
362
372
  // ---- 晚绑定 ---------------------------------------------------------------
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
- };
373
+ const overlayHooks = approvalOverlayHooks({ tui, editor, tools: () => tools, indicator });
376
374
  const agentUi = new AgentUi({
377
375
  theme,
378
376
  tui,
@@ -395,28 +393,20 @@ export function runInteractiveMode(runtime, context, options = {}) {
395
393
  ...(options.clipboard !== undefined ? { clipboard: options.clipboard } : {}),
396
394
  area,
397
395
  });
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))));
396
+ indicator.agents = () => agentUi.reachable;
397
+ const { broker, dock } = createApprovalUi({
398
+ ...{ theme, keys, tui, editor, indicator, agentUi, notice, render },
399
+ hooks: overlayHooks,
400
+ session: () => session,
401
+ });
402
+ indicator.background = () => blockingTasks(session).length > 0;
414
403
  // ---- 启动与退出 -----------------------------------------------------------
415
404
  let offSignals = () => undefined;
416
405
  function exit(code) {
417
406
  if (finished)
418
407
  return;
419
408
  finished = true;
409
+ logoAnimation?.finish();
420
410
  if (hintTimer !== undefined)
421
411
  clearTimeout(hintTimer);
422
412
  loader.stop();
@@ -427,6 +417,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
427
417
  tui.addChild(view);
428
418
  offSignals();
429
419
  agentUi.detach();
420
+ dock.dispose();
430
421
  runtime.approvals.setUiBroker(undefined);
431
422
  runtime.notifier.set(undefined);
432
423
  tui.stop();
@@ -439,8 +430,10 @@ export function runInteractiveMode(runtime, context, options = {}) {
439
430
  resolveExit(code);
440
431
  });
441
432
  }
442
- if (startupLevel !== "silent")
443
- view.add(header(startupLevel));
433
+ const startHeader = startupLevel === "silent" ? undefined : header(startupLevel);
434
+ let logoAnimation;
435
+ if (startHeader !== undefined)
436
+ view.add(startHeader);
444
437
  for (const warning of startupWarnings)
445
438
  view.addNotice("warn", warning);
446
439
  replay();
@@ -460,6 +453,8 @@ export function runInteractiveMode(runtime, context, options = {}) {
460
453
  agentUi.attach(session);
461
454
  offSignals = onTerminationSignals((code) => exit(code));
462
455
  tui.setFocus(editor);
456
+ const gate = { animation: ui.animation, host: info.host !== undefined, env, ...context };
457
+ logoAnimation = playStartupAnimation(tui, startHeader, theme, gate);
463
458
  options.onReady?.({
464
459
  tui,
465
460
  editor,
@@ -10,20 +10,31 @@
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
+ * 写进中断提示。
15
19
  */
16
20
  import type { AgentSession } from "../../agent/types.js";
17
21
  import type { PermissionMode } from "../../permissions/types.js";
18
22
  import type { Editor, Keybindings } from "../../tui.js";
19
23
  import type { StatusBar } from "./status-bar.js";
20
24
  import type { ToolTracker } from "./tool-view.js";
25
+ /** [W7-A] 进栏落空提示的停留时间。 */
26
+ export declare const FOCUS_HINT_MS = 3000;
27
+ /**
28
+ * [W7-A] 进栏键的结果:true 已进栏;false 不管(交给编辑器,不提示);其余是落空原因——
29
+ * `busy-input` 输入框有字、`disabled` `ui.agentBar: "off"`(有任务时)、`empty` 没有任务。
30
+ */
31
+ export type AgentFocusResult = boolean | "busy-input" | "disabled" | "empty";
21
32
  /** [W6-A] Agent 栏的按键入口。 */
22
33
  export interface AgentKeys {
23
34
  /** 栏聚焦时先消费按键;返回 true 已处理。 */
24
35
  handleKey(data: string): boolean;
25
- /** `app.agents.focus`(输入为空):进栏返回 true,否则交给编辑器。 */
26
- focus(data: string): boolean;
36
+ /** `app.agents.focus`:输入为空且有任务时进栏返回 true,否则返回落空原因(按键仍交给编辑器)。 */
37
+ focus(data: string): AgentFocusResult;
27
38
  }
28
39
  export interface KeyDispatchDeps {
29
40
  keys: Keybindings;
@@ -58,6 +69,12 @@ export interface KeyDispatchDeps {
58
69
  doubleEscMs?: number;
59
70
  /** [W6-A] Agent 栏(晚绑定)。 */
60
71
  agents?(): AgentKeys | undefined;
72
+ /** [W7-A] 提示里的 `↓` 字形(ASCII 主题 `v`;缺省 `↓`)。 */
73
+ downGlyph?: string;
74
+ /** [W7-C] 有阻塞中的前台任务时全部转后台,返回转了的 taskId;没有返回空(按键交给编辑器)。 */
75
+ backgroundTasks?(): string[];
76
+ /** [W7-C] 仍在运行的后台任务(Esc 中断的提示里写明不受影响)。 */
77
+ runningBackground?(): string[];
61
78
  }
62
79
  /** 返回输入监听器:已处理返回 true,交给编辑器返回 false。 */
63
80
  export declare function createKeyDispatch(deps: KeyDispatchDeps): (data: string) => boolean;
@@ -10,20 +10,31 @@
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
+ * 写进中断提示。
15
19
  */
16
20
  import { msg } from "../../i18n/index.js";
17
21
  import { ExitCode } from "../../cli/exit-codes.js";
18
22
  import { nextCycleMode, permissionModeLabel } from "../../permissions/modes.js";
19
23
  import { DOUBLE_ESC_HINT_MS, DoubleEscape } from "./double-esc.js";
24
+ import { backgroundedText } from "./task-background.js";
20
25
  import { statusLineText } from "./status-area.js";
21
26
  const DOUBLE_CTRL_C_MS = 1500;
27
+ /** [W7-A] 进栏落空提示的停留时间。 */
28
+ export const FOCUS_HINT_MS = 3000;
22
29
  /** 返回输入监听器:已处理返回 true,交给编辑器返回 false。 */
23
30
  export function createKeyDispatch(deps) {
24
31
  const { keys, editor, tools, status } = deps;
25
32
  let ctrlCArmedAt = Number.NEGATIVE_INFINITY;
26
33
  const esc = new DoubleEscape(deps.doubleEscMs);
34
+ /** 本段草稿已给过「输入框有字」提示(输入清空后重置)。 */
35
+ let busyInputHinted = false;
36
+ /** 底部正显示着进栏落空提示(进栏后撤掉)。 */
37
+ let focusHintShown = false;
27
38
  const doubleEscape = () => {
28
39
  switch (esc.press(deps.now(), editor.isEmpty())) {
29
40
  case "arm-rewind":
@@ -88,9 +99,43 @@ export function createKeyDispatch(deps) {
88
99
  else
89
100
  void answer.then(settle);
90
101
  };
102
+ /** 光标在最后一行(`↓` 不再下移,只会到行尾)。 */
103
+ const atLastLine = () => editor.cursor.line >= editor.getText().split("\n").length - 1;
104
+ /** `app.agents.focus`:进栏返回 true;落空给提示后返回 false(按键照常交给编辑器)。 */
105
+ const focusAgents = (agents, data) => {
106
+ if (editor.isCompletionOpen || editor.isBrowsingHistory)
107
+ return false;
108
+ const result = agents.focus(data);
109
+ if (result === true) {
110
+ // 进栏了:撤掉之前的落空提示
111
+ if (focusHintShown)
112
+ deps.showHint("");
113
+ focusHintShown = false;
114
+ return true;
115
+ }
116
+ const m = msg().agents;
117
+ let text;
118
+ if (result === "busy-input") {
119
+ if (busyInputHinted || !atLastLine())
120
+ return false;
121
+ busyInputHinted = true;
122
+ text = m.focus.busyInput(deps.downGlyph ?? "↓");
123
+ }
124
+ else if (result === "disabled")
125
+ text = m.focus.disabled;
126
+ else if (result === "empty")
127
+ text = m.bar.empty;
128
+ if (text !== undefined) {
129
+ deps.showHint(text, FOCUS_HINT_MS);
130
+ focusHintShown = true;
131
+ }
132
+ return false;
133
+ };
91
134
  return (data) => {
92
135
  if (deps.inactive())
93
136
  return false;
137
+ if (editor.isEmpty())
138
+ busyInputHinted = false;
94
139
  const is = (action) => keys.matches(data, action);
95
140
  if (!is("app.rewind"))
96
141
  esc.reset();
@@ -99,12 +144,16 @@ export function createKeyDispatch(deps) {
99
144
  esc.reset();
100
145
  return true;
101
146
  }
102
- if (agents !== undefined &&
103
- is("app.agents.focus") &&
104
- editor.isEmpty() &&
105
- !editor.isCompletionOpen &&
106
- agents.focus(data))
147
+ if (agents !== undefined && is("app.agents.focus") && focusAgents(agents, data))
107
148
  return true;
149
+ if (is("app.tasks.background") && deps.backgroundTasks !== undefined) {
150
+ const moved = deps.backgroundTasks();
151
+ if (moved.length > 0) {
152
+ esc.reset();
153
+ deps.showHint(backgroundedText(moved), FOCUS_HINT_MS);
154
+ return true;
155
+ }
156
+ }
108
157
  if (is("app.clear")) {
109
158
  if (!editor.isEmpty()) {
110
159
  editor.clear();
@@ -124,7 +173,10 @@ export function createKeyDispatch(deps) {
124
173
  if (is("app.interrupt") && !editor.isCompletionOpen && deps.busy()) {
125
174
  esc.reset();
126
175
  interrupt();
127
- deps.showHint(msg().interactive.keys.interrupted);
176
+ const left = deps.runningBackground?.() ?? [];
177
+ deps.showHint(left.length > 0
178
+ ? msg().agents.background.interrupted(left.join(", "))
179
+ : msg().interactive.keys.interrupted);
128
180
  deps.onInterrupted?.(editor.isEmpty());
129
181
  return true;
130
182
  }
@@ -194,6 +194,11 @@ export async function runLineMode(runtime, context, options = {}) {
194
194
  continue;
195
195
  }
196
196
  if (busy) {
197
+ // [W7-C] 运行中 `/tasks bg [id]` 照样是命令(转后台阻塞中的前台任务),不当插话
198
+ if (/^\/tasks\s+bg(\s+\S+)?\s*$/.test(action.text.trim())) {
199
+ void handle(action.text.trim());
200
+ continue;
201
+ }
197
202
  if (action.text.trim() !== "") {
198
203
  void session.steer(action.text).catch(() => undefined);
199
204
  out(`${msg().interactive.line.steer(action.text)}\n`);
@@ -9,6 +9,10 @@
9
9
  * 助手在流式输出 → 末块是思考 `思考中`,否则 `回复中 · ↓≈1.2k`(本条输出的估算 token,4 字符 ≈ 1)
10
10
  * 其余(等首个 token)→ `思考中`
11
11
  * 运行或压缩时 Loader 才挂进槽位;附加项 `Esc 中断`。
12
+ * [W7-A] 有该显示的子 Agent 任务时再带可丢弃的 `↓ Agent 栏`(窄屏先丢它);任务随时出现 / 结束,
13
+ * 每帧对一次,变了才换动词。
14
+ * [W7-C] 有阻塞中的前台任务时可丢弃项前面加 `Ctrl+B 转后台`;有停靠的审批时 `↓ Agent 栏` 换成
15
+ * `↓ 处理审批`。顺序 `Esc 中断 · Ctrl+B 转后台 · ↓ Agent 栏`,窄屏从后往前丢。
12
16
  *
13
17
  * 排队消息:缩进 2 列(挂在当前回合下),`↳ 插话 / 之后 文本` 整体 muted、标签 dim;超过 3 条首行
14
18
  * `… 另 N 条`;末行 `Alt+↑ 取回 · Esc 回填并中断`。
@@ -24,6 +28,8 @@ export interface RunIndicatorDeps {
24
28
  slot: Container;
25
29
  tools: ToolTracker;
26
30
  render(): void;
31
+ /** [W7-C] `app.tasks.background` 的按键标签(`Ctrl+B`);没有绑定时不提示。 */
32
+ backgroundKey?: string;
27
33
  }
28
34
  export declare class RunIndicator {
29
35
  private readonly deps;
@@ -32,7 +38,16 @@ export declare class RunIndicator {
32
38
  private approval;
33
39
  private retry;
34
40
  private stream;
41
+ /** [W7-A] 有该显示的子 Agent 任务(interactive-mode 在 AgentUi 建好后接上)。 */
42
+ agents: () => boolean;
43
+ /** [W7-C] 有阻塞中的前台任务(Ctrl+B 可转后台)。 */
44
+ background: () => boolean;
45
+ /** [W7-C] 有停靠在 Agent 栏的审批。 */
46
+ docked: () => boolean;
47
+ private hintKey;
35
48
  constructor(deps: RunIndicatorDeps);
49
+ /** 可丢弃的按键提示(按丢弃的逆序排列)。 */
50
+ private optionalHints;
36
51
  get busy(): boolean;
37
52
  setApproval(open: boolean): void;
38
53
  /** 会话切换后清零。 */