@armadra/agent 0.3.0 → 0.4.0

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 (198) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +93 -44
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +1 -1
  6. package/dist/agent/session-classifier.d.ts +18 -0
  7. package/dist/agent/session-classifier.js +103 -0
  8. package/dist/agent/session-core.d.ts +13 -0
  9. package/dist/agent/session-tools.js +24 -3
  10. package/dist/agent/session.d.ts +3 -0
  11. package/dist/agent/session.js +12 -3
  12. package/dist/agent/tool-runner.js +15 -2
  13. package/dist/agent/types.d.ts +9 -1
  14. package/dist/ai/apis/anthropic-messages.js +3 -2
  15. package/dist/ai/apis/google-generative-ai.js +3 -2
  16. package/dist/ai/apis/openai-completions.js +3 -2
  17. package/dist/ai/apis/openai-responses.js +3 -2
  18. package/dist/ai/http.d.ts +28 -6
  19. package/dist/ai/http.js +41 -8
  20. package/dist/ai/providers/registry.d.ts +8 -1
  21. package/dist/ai/providers/registry.js +30 -22
  22. package/dist/ai/providers/suggest.d.ts +18 -0
  23. package/dist/ai/providers/suggest.js +72 -0
  24. package/dist/ai/sse.d.ts +6 -2
  25. package/dist/ai/sse.js +22 -2
  26. package/dist/ai/types.d.ts +9 -2
  27. package/dist/bundle/ama.cjs +15279 -10151
  28. package/dist/cli/args.d.ts +19 -4
  29. package/dist/cli/args.js +83 -10
  30. package/dist/cli/bootstrap.js +27 -3
  31. package/dist/cli/codemode-notice.d.ts +20 -0
  32. package/dist/cli/codemode-notice.js +55 -0
  33. package/dist/cli/compose-session.d.ts +5 -0
  34. package/dist/cli/compose-session.js +32 -1
  35. package/dist/cli/compose-store.d.ts +1 -1
  36. package/dist/cli/compose-store.js +3 -1
  37. package/dist/cli/compose.d.ts +14 -4
  38. package/dist/cli/compose.js +29 -10
  39. package/dist/cli/default-model.d.ts +38 -1
  40. package/dist/cli/default-model.js +96 -9
  41. package/dist/cli/deps.d.ts +36 -0
  42. package/dist/cli/exit-codes.d.ts +2 -0
  43. package/dist/cli/exit-codes.js +3 -0
  44. package/dist/cli/fake-visibility.d.ts +13 -0
  45. package/dist/cli/fake-visibility.js +26 -0
  46. package/dist/cli/from-prompt.d.ts +18 -0
  47. package/dist/cli/from-prompt.js +49 -0
  48. package/dist/cli/main.d.ts +9 -2
  49. package/dist/cli/main.js +77 -3
  50. package/dist/cli/proxy.d.ts +51 -0
  51. package/dist/cli/proxy.js +135 -0
  52. package/dist/cli/startup-screen.d.ts +29 -0
  53. package/dist/cli/startup-screen.js +47 -0
  54. package/dist/cli/startup-steps.d.ts +1 -1
  55. package/dist/cli/startup-steps.js +14 -12
  56. package/dist/cli/subcommands/config.d.ts +19 -3
  57. package/dist/cli/subcommands/config.js +108 -21
  58. package/dist/cli/subcommands/context.js +4 -1
  59. package/dist/cli/subcommands/doctor.js +12 -1
  60. package/dist/cli/subcommands/init.js +2 -1
  61. package/dist/cli/subcommands/models-discover.d.ts +15 -9
  62. package/dist/cli/subcommands/models-discover.js +53 -46
  63. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  64. package/dist/cli/subcommands/probe-runner.js +264 -0
  65. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  66. package/dist/cli/subcommands/providers-probe.js +87 -0
  67. package/dist/cli/subcommands/providers.d.ts +3 -2
  68. package/dist/cli/subcommands/providers.js +48 -45
  69. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  70. package/dist/cli/subcommands/sessions-export.js +59 -0
  71. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  72. package/dist/cli/subcommands/sessions-search.js +103 -0
  73. package/dist/cli/subcommands/sessions.d.ts +3 -2
  74. package/dist/cli/subcommands/sessions.js +22 -1
  75. package/dist/cli/subcommands/stats.d.ts +17 -0
  76. package/dist/cli/subcommands/stats.js +198 -0
  77. package/dist/cli/system-prompt-arg.d.ts +11 -0
  78. package/dist/cli/system-prompt-arg.js +34 -0
  79. package/dist/codemode/modes.d.ts +4 -11
  80. package/dist/codemode/modes.js +5 -27
  81. package/dist/codemode/tool.d.ts +18 -14
  82. package/dist/codemode/tool.js +71 -28
  83. package/dist/config/init.d.ts +6 -2
  84. package/dist/config/init.js +12 -5
  85. package/dist/config/json-schema.d.ts +1 -0
  86. package/dist/config/json-schema.js +36 -3
  87. package/dist/config/key-docs.d.ts +22 -0
  88. package/dist/config/key-docs.js +105 -0
  89. package/dist/config/merge.d.ts +9 -8
  90. package/dist/config/merge.js +22 -7
  91. package/dist/config/schema.d.ts +1 -1
  92. package/dist/config/schema.js +24 -5
  93. package/dist/config/types.d.ts +34 -6
  94. package/dist/config/types.js +17 -1
  95. package/dist/modes/commands-core.js +6 -5
  96. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  97. package/dist/modes/interactive/approval-dialog.js +106 -27
  98. package/dist/modes/interactive/commands.d.ts +11 -2
  99. package/dist/modes/interactive/commands.js +56 -16
  100. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  101. package/dist/modes/interactive/interactive-mode.js +65 -81
  102. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  103. package/dist/modes/interactive/key-dispatch.js +5 -6
  104. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  105. package/dist/modes/interactive/line/line-mode.js +6 -4
  106. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  107. package/dist/modes/interactive/line/line-render.js +28 -6
  108. package/dist/modes/interactive/message-view.d.ts +48 -9
  109. package/dist/modes/interactive/message-view.js +238 -44
  110. package/dist/modes/interactive/panels.d.ts +18 -0
  111. package/dist/modes/interactive/panels.js +143 -0
  112. package/dist/modes/interactive/pickers.d.ts +23 -2
  113. package/dist/modes/interactive/pickers.js +48 -15
  114. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  115. package/dist/modes/interactive/run-indicator.js +189 -0
  116. package/dist/modes/interactive/startup-header.d.ts +40 -0
  117. package/dist/modes/interactive/startup-header.js +169 -0
  118. package/dist/modes/interactive/status-bar.d.ts +18 -15
  119. package/dist/modes/interactive/status-bar.js +98 -56
  120. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  121. package/dist/modes/interactive/tool-summary.js +218 -0
  122. package/dist/modes/interactive/tool-view.d.ts +48 -15
  123. package/dist/modes/interactive/tool-view.js +203 -145
  124. package/dist/modes/print/print-mode.d.ts +31 -4
  125. package/dist/modes/print/print-mode.js +116 -7
  126. package/dist/modes/rpc/commands.js +5 -0
  127. package/dist/permissions/auto-safe.d.ts +60 -0
  128. package/dist/permissions/auto-safe.js +529 -0
  129. package/dist/permissions/classifier.d.ts +64 -0
  130. package/dist/permissions/classifier.js +184 -0
  131. package/dist/permissions/dangerous.d.ts +5 -0
  132. package/dist/permissions/dangerous.js +1 -1
  133. package/dist/permissions/modes.d.ts +30 -0
  134. package/dist/permissions/modes.js +78 -0
  135. package/dist/permissions/pipeline.d.ts +31 -4
  136. package/dist/permissions/pipeline.js +196 -6
  137. package/dist/permissions/protected.d.ts +19 -0
  138. package/dist/permissions/protected.js +74 -0
  139. package/dist/permissions/rules.js +3 -0
  140. package/dist/permissions/types.d.ts +50 -3
  141. package/dist/permissions/types.js +3 -0
  142. package/dist/sdk.d.ts +9 -3
  143. package/dist/sdk.js +10 -2
  144. package/dist/session/export.d.ts +32 -0
  145. package/dist/session/export.js +187 -0
  146. package/dist/session/redact.d.ts +15 -0
  147. package/dist/session/redact.js +55 -0
  148. package/dist/session/reuse.d.ts +33 -0
  149. package/dist/session/reuse.js +86 -0
  150. package/dist/session/scan.d.ts +34 -0
  151. package/dist/session/scan.js +140 -0
  152. package/dist/session/search.d.ts +52 -0
  153. package/dist/session/search.js +211 -0
  154. package/dist/session/stats-aggregate.d.ts +63 -0
  155. package/dist/session/stats-aggregate.js +163 -0
  156. package/dist/session/stats-index.d.ts +26 -0
  157. package/dist/session/stats-index.js +91 -0
  158. package/dist/session/stats-scan.d.ts +54 -0
  159. package/dist/session/stats-scan.js +236 -0
  160. package/dist/tools/presets.d.ts +35 -8
  161. package/dist/tools/presets.js +56 -17
  162. package/dist/tui/component.d.ts +8 -2
  163. package/dist/tui/component.js +3 -1
  164. package/dist/tui/components/box.d.ts +6 -1
  165. package/dist/tui/components/box.js +16 -6
  166. package/dist/tui/components/card.d.ts +23 -0
  167. package/dist/tui/components/card.js +37 -0
  168. package/dist/tui/components/editor-history.d.ts +6 -0
  169. package/dist/tui/components/editor-history.js +45 -0
  170. package/dist/tui/components/editor-paste.d.ts +1 -1
  171. package/dist/tui/components/editor-paste.js +4 -4
  172. package/dist/tui/components/editor.d.ts +11 -5
  173. package/dist/tui/components/editor.js +52 -58
  174. package/dist/tui/components/key-value.d.ts +3 -0
  175. package/dist/tui/components/key-value.js +16 -6
  176. package/dist/tui/components/loader.d.ts +37 -7
  177. package/dist/tui/components/loader.js +84 -21
  178. package/dist/tui/components/markdown.d.ts +5 -1
  179. package/dist/tui/components/markdown.js +45 -21
  180. package/dist/tui/components/meter.d.ts +3 -3
  181. package/dist/tui/components/meter.js +13 -11
  182. package/dist/tui/components/select-list.d.ts +23 -1
  183. package/dist/tui/components/select-list.js +76 -13
  184. package/dist/tui/glyphs.d.ts +68 -0
  185. package/dist/tui/glyphs.js +114 -0
  186. package/dist/tui/theme.d.ts +30 -6
  187. package/dist/tui/theme.js +103 -17
  188. package/dist/tui.d.ts +4 -2
  189. package/dist/tui.js +3 -1
  190. package/docs/codemode.md +23 -9
  191. package/docs/hooks.md +10 -10
  192. package/docs/permissions.md +148 -0
  193. package/docs/providers.md +33 -10
  194. package/docs/rpc.md +36 -36
  195. package/docs/session-format.md +2 -1
  196. package/docs/sessions.md +134 -0
  197. package/docs/tui.md +139 -62
  198. package/package.json +3 -1
@@ -7,14 +7,19 @@
7
7
  * - 执行前预览(W3-B9a-2,`request.preview`):输入摘要之后列出会碰到的路径与规模,danger 红、
8
8
  * warn 黄、info 暗色;`other` 类预览与输入摘要重复,不显示;
9
9
  * - 原因:mode(当前权限模式需要确认)/ dangerous / hook + Hook 给的文本;
10
- * - 按键:`y` 允许、`n` / Esc / Ctrl+C 拒绝、`a` 本会话允许同类;
10
+ * - 选项(终端界面视觉设计 v1 §3.11):编号列表 `1. 允许 y` / `2. 本会话允许同类 a` / `3. 拒绝 n Esc`,
11
+ * 选中行 `›` + selection 底色;缺省选中:危险命令 → 拒绝,其余 → 允许。按键 `y / a / n / Esc / Ctrl+C /
12
+ * 1 2 3 / ↑↓ Enter / v` 都有效;
13
+ * - 标题按原因:需要确认 / 危险命令 / Hook 要求确认(子 Agent 发起前缀 `[task] `);边框颜色随预览严重度
14
+ * (danger error、warn warning);宽 < 56 时紧凑:工具名单独一行、去掉空行、按键提示缩短;
11
15
  * - 超时与中断:broker 链在超时(缺省 10 分钟)或运行被中断时 abort `signal`,对话框关闭并返回
12
16
  * undefined(链按 deny 处理),消息区留一行说明。
13
17
  *
14
18
  * broker 链本身串行化审批,所以同一时刻最多一个对话框。
15
19
  */
20
+ import { AUTO_LAYER_TEXT, isPermissionMode, permissionModeLabel } from "../../permissions/modes.js";
16
21
  import { previewDisplayLines } from "../../permissions/preview.js";
17
- import { Box, Container, Text, defaultKeybindings, matchesKey, truncateToWidth, } from "../../tui.js";
22
+ import { Box, defaultKeybindings, matchesKey, padToWidth, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "../../tui.js";
18
23
  import { toolSummary } from "./tool-view.js";
19
24
  const COMMAND_LINES = 8;
20
25
  const EDIT_LINES = 3;
@@ -28,7 +33,17 @@ function lines(text, max) {
28
33
  function record(input) {
29
34
  return typeof input === "object" && input !== null ? input : {};
30
35
  }
31
- /** 对话框正文(不含按键行)。 */
36
+ /** 原因 → 标题文字(子 Agent 发起加 `[task] `)。 */
37
+ export function approvalTitle(request) {
38
+ const task = (request.context?.depth ?? 0) > 0 ? "[task] " : "";
39
+ const reason = request.reason === "dangerous"
40
+ ? "危险命令"
41
+ : request.reason === "hook"
42
+ ? "Hook 要求确认"
43
+ : "需要确认";
44
+ return task + reason;
45
+ }
46
+ /** 对话框正文(不含选项与按键行)。 */
32
47
  export function describeRequest(request, theme, options = {}) {
33
48
  const out = [];
34
49
  const task = (request.context?.depth ?? 0) > 0 ? theme.fg("warning", "[task] ") : "";
@@ -37,7 +52,8 @@ export function describeRequest(request, theme, options = {}) {
37
52
  : request.reason === "hook"
38
53
  ? theme.fg("warning", "Hook 要求确认")
39
54
  : theme.fg("dim", "需要确认");
40
- out.push(`${task}${theme.bold(theme.fg("tool", request.toolName))} ${tag}`);
55
+ const name = theme.bold(theme.fg("tool", request.toolName));
56
+ out.push(options.compact === true ? `${task}${name}` : `${task}${name} ${tag}`);
41
57
  const input = record(request.input);
42
58
  const more = (n) => theme.fg("dim", `… 另 ${n} 行(v 查看)`);
43
59
  if (options.expanded === true) {
@@ -84,42 +100,42 @@ export function describeRequest(request, theme, options = {}) {
84
100
  const severity = request.preview?.severity;
85
101
  const color = severity === "danger" ? "error" : severity === "warn" ? "warning" : "dim";
86
102
  out.push(...previewDisplayLines(request.preview).map((l) => theme.fg(color, l)));
103
+ const auto = request.autoDecision;
87
104
  if (request.reason === "hook") {
88
105
  out.push(theme.fg("warning", `Hook:${request.hookReason ?? "(无说明)"}`));
89
106
  }
90
107
  else if (request.reason === "dangerous") {
91
108
  out.push(theme.fg("error", "这条命令可能有破坏性,请确认"));
92
109
  }
110
+ else if (auto !== undefined) {
111
+ out.push(theme.fg("warning", `Auto ${AUTO_LAYER_TEXT[auto.layer]}:${auto.reason}`));
112
+ }
93
113
  else if (options.permissionMode !== undefined) {
94
- out.push(theme.fg("dim", `权限模式 ${options.permissionMode} 下需要确认`));
114
+ const mode = options.permissionMode;
115
+ const label = isPermissionMode(mode) ? permissionModeLabel(mode) : mode;
116
+ out.push(theme.fg("dim", `权限模式 ${label} 下需要确认`));
95
117
  }
96
118
  return out;
97
119
  }
98
- export const APPROVAL_KEYS_HINT = "[y] 允许 [n] 拒绝 [a] 本会话允许同类 [v] 完整输入";
120
+ export const APPROVAL_OPTIONS = [
121
+ { decision: "allow", label: "允许", keys: "y" },
122
+ { decision: "allow_session", label: "本会话允许同类", keys: "a" },
123
+ { decision: "deny", label: "拒绝", keys: "n Esc" },
124
+ ];
125
+ /** 宽度低于它(Box 外宽)时用紧凑布局。 */
126
+ const COMPACT_WIDTH = 56;
99
127
  class ApprovalDialog {
100
128
  request;
101
129
  host;
102
130
  decide;
103
131
  focused = false;
104
132
  expanded = false;
105
- body = new Container();
133
+ selected;
106
134
  constructor(request, host, decide) {
107
135
  this.request = request;
108
136
  this.host = host;
109
137
  this.decide = decide;
110
- this.rebuild();
111
- }
112
- rebuild() {
113
- const { theme } = this.host;
114
- this.body.clear();
115
- const options = {
116
- permissionMode: this.host.permissionMode(),
117
- expanded: this.expanded,
118
- };
119
- if (this.host.cwd !== undefined)
120
- options.cwd = this.host.cwd;
121
- this.body.addChild(new Text(describeRequest(this.request, theme, options).join("\n")));
122
- this.body.addChild(new Text(theme.fg("accent", APPROVAL_KEYS_HINT)));
138
+ this.selected = request.reason === "dangerous" ? 2 : 0;
123
139
  }
124
140
  handleInput(data) {
125
141
  const keys = this.host.keybindings ?? defaultKeybindings;
@@ -134,17 +150,74 @@ class ApprovalDialog {
134
150
  keys.matches(data, "tui.select.cancel")) {
135
151
  this.decide("deny");
136
152
  }
137
- else if (key === "v") {
138
- this.expanded = !this.expanded;
139
- this.rebuild();
153
+ else if (/^[1-3]$/.test(data))
154
+ this.decide(APPROVAL_OPTIONS[Number(data) - 1].decision);
155
+ else if (keys.matches(data, "tui.select.up")) {
156
+ this.selected = (this.selected + APPROVAL_OPTIONS.length - 1) % APPROVAL_OPTIONS.length;
140
157
  }
158
+ else if (keys.matches(data, "tui.select.down")) {
159
+ this.selected = (this.selected + 1) % APPROVAL_OPTIONS.length;
160
+ }
161
+ else if (matchesKey(data, "enter"))
162
+ this.decide(APPROVAL_OPTIONS[this.selected].decision);
163
+ else if (key === "v")
164
+ this.expanded = !this.expanded;
141
165
  }
166
+ /** `width` 是 Box 内宽;紧凑与否按外宽(+ 4)判断。 */
142
167
  render(width) {
143
- return this.body.render(width).map((l) => truncateToWidth(l, width));
168
+ const { theme } = this.host;
169
+ const compact = width + 4 < COMPACT_WIDTH;
170
+ const options = {
171
+ permissionMode: this.host.permissionMode(),
172
+ expanded: this.expanded,
173
+ compact,
174
+ };
175
+ if (this.host.cwd !== undefined)
176
+ options.cwd = this.host.cwd;
177
+ const body = describeRequest(this.request, theme, options);
178
+ const lines = [];
179
+ // 输入摘要(标题 + 命令)与预览 / 原因之间空一行
180
+ const head = this.inputLines(body);
181
+ for (const line of body.slice(0, head))
182
+ lines.push(...wrapTextWithAnsi(line, width));
183
+ if (!compact && head < body.length)
184
+ lines.push("");
185
+ for (const line of body.slice(head))
186
+ lines.push(...wrapTextWithAnsi(line, width));
187
+ if (!compact)
188
+ lines.push("");
189
+ lines.push(...this.optionLines(width, compact));
190
+ if (!compact)
191
+ lines.push("");
192
+ const arrows = theme.glyphs.arrowUp + theme.glyphs.arrowDown;
193
+ const hint = compact
194
+ ? `${arrows} Enter · v 完整输入`
195
+ : `${arrows} 选择 · Enter 确认 · v 完整输入`;
196
+ lines.push(theme.fg("dim", hint));
197
+ return lines.map((l) => truncateToWidth(l, width));
144
198
  }
145
- invalidate() {
146
- this.body.invalidate();
199
+ /** 正文里属于「输入摘要」的行数(首行 + 命令 / 路径 / 修改摘要,止于预览或原因行)。 */
200
+ inputLines(body) {
201
+ const preview = previewDisplayLines(this.request.preview).length;
202
+ const reasonLines = body.length > 0 ? 1 : 0;
203
+ return Math.max(1, body.length - preview - reasonLines);
204
+ }
205
+ optionLines(width, compact) {
206
+ const { theme } = this.host;
207
+ const g = theme.glyphs;
208
+ const keyWidth = compact ? 1 : Math.max(...APPROVAL_OPTIONS.map((o) => o.keys.length));
209
+ return APPROVAL_OPTIONS.map((option, i) => {
210
+ const selected = i === this.selected;
211
+ const keys = compact ? option.keys.slice(0, 1) : option.keys;
212
+ const left = `${selected ? g.prompt : " "} ${i + 1}. ${option.label}`;
213
+ const right = keys.padEnd(keyWidth);
214
+ const room = Math.max(1, width - visibleWidth(right) - 1);
215
+ const label = selected ? theme.fg("accent", theme.bold(left)) : left;
216
+ const line = padToWidth(truncateToWidth(label, room), room) + " " + theme.fg("dim", right);
217
+ return selected ? theme.bg("selection", line) : line;
218
+ });
147
219
  }
220
+ invalidate() { }
148
221
  }
149
222
  /** UI broker:每个请求开一个底部覆盖层,等用户按键或 signal abort。 */
150
223
  export class ApprovalDialogBroker {
@@ -177,9 +250,15 @@ export class ApprovalDialogBroker {
177
250
  const dialog = new ApprovalDialog(request, this.host, settle);
178
251
  this.open++;
179
252
  this.host.onOpen?.();
253
+ const severity = request.preview?.severity;
180
254
  handle = this.host.showOverlay(new Box(dialog, {
181
- title: (request.context?.depth ?? 0) > 0 ? "子任务审批" : "审批",
255
+ title: approvalTitle(request),
182
256
  theme: this.host.theme,
257
+ ...(severity === "danger"
258
+ ? { borderColor: "error" }
259
+ : severity === "warn"
260
+ ? { borderColor: "warning" }
261
+ : {}),
183
262
  }));
184
263
  signal.addEventListener("abort", onAbort, { once: true });
185
264
  });
@@ -2,16 +2,17 @@
2
2
  * 交互模式的斜杠命令:把 commands-core 的 `CommandResult` 变成界面动作。[B7]
3
3
  *
4
4
  * - `handled` → 消息区显示文本;`prompt` → 发提示;`exit` → 退出;
5
- * - `pick` → 打开对应选择器:模型(setModel)、会话(resume)、树(/fork 无参数:从选中的用户消息
5
+ * - `pick` → 打开对应选择器:模型(setModel;不列测试供应商 fake,`AMA_SHOW_FAKE=1` 或 `AMA_FAKE_SCRIPT` 时照列)、会话(resume)、树(/fork 无参数:从选中的用户消息
6
6
  * 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
7
7
  * - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
8
8
  * `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
9
+ * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本。
9
10
  * - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
10
11
  */
11
12
  import type { AgentSession } from "../../agent/types.js";
12
13
  import type { SwitchRequest } from "../../cli/compose-session.js";
13
14
  import type { Runtime } from "../../cli/runtime.js";
14
- import type { SelectItem } from "../../tui.js";
15
+ import type { Component, SelectItem, Theme } from "../../tui.js";
15
16
  import { type CommandInfo } from "../commands-core.js";
16
17
  import { type NoticeLevel } from "./message-view.js";
17
18
  import { type PickerSpec } from "./pickers.js";
@@ -26,6 +27,8 @@ export interface CommandUi {
26
27
  switchSession(request: SwitchRequest): Promise<AgentSession>;
27
28
  pick(spec: PickerSpec): Promise<SelectItem | undefined>;
28
29
  notice(level: NoticeLevel, text: string): void;
30
+ /** 消息区面板(/session、/cache、/permissions);缺省回落为文本通知。 */
31
+ panel?(component: Component): void;
29
32
  setEditorText(text: string): void;
30
33
  /** 发一条提示(不等运行结束)。 */
31
34
  prompt(text: string): void;
@@ -33,6 +36,12 @@ export interface CommandUi {
33
36
  reload(): void;
34
37
  exit(code: number): void;
35
38
  now(): number;
39
+ /** 面板用的主题(与 panel 一起提供)。 */
40
+ theme?(): Theme;
41
+ /** 家目录(面板里路径缩写为 ~)。 */
42
+ home?: string;
43
+ /** 进程环境:给出时 /model 选择器按 fake-visibility 规则藏起测试供应商 fake。 */
44
+ env?: Readonly<Record<string, string | undefined>>;
36
45
  }
37
46
  /** 当前分支上的条目 id(树选择器标 ●)。 */
38
47
  export declare function activeBranchIds(session: AgentSession): Set<string>;
@@ -2,18 +2,22 @@
2
2
  * 交互模式的斜杠命令:把 commands-core 的 `CommandResult` 变成界面动作。[B7]
3
3
  *
4
4
  * - `handled` → 消息区显示文本;`prompt` → 发提示;`exit` → 退出;
5
- * - `pick` → 打开对应选择器:模型(setModel)、会话(resume)、树(/fork 无参数:从选中的用户消息
5
+ * - `pick` → 打开对应选择器:模型(setModel;不列测试供应商 fake,`AMA_SHOW_FAKE=1` 或 `AMA_FAKE_SCRIPT` 时照列)、会话(resume)、树(/fork 无参数:从选中的用户消息
6
6
  * 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
7
7
  * - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
8
8
  * `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
9
+ * - `/session`、`/cache`(无参数)与 `/permissions` 在消息区画左竖条面板(panels.ts),不再拍成文本。
9
10
  * - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
10
11
  */
11
12
  import { formatModelRef } from "../../ai/providers/channels.js";
12
13
  import { AgentSessionImpl } from "../../agent/session.js";
13
14
  import { listSessions } from "../../cli/compose-store.js";
15
+ import { hideFakeProvider } from "../../cli/fake-visibility.js";
16
+ import { AUTO_LAYER_TEXT, permissionModeLabel } from "../../permissions/modes.js";
14
17
  import { BUILTIN_COMMANDS, parseSlash, runSlashCommand, } from "../commands-core.js";
15
18
  import { contentText } from "./message-view.js";
16
- import { modelItems, permissionItems, sessionItems, thinkingItems, treeItems, } from "./pickers.js";
19
+ import { cachePanel, permissionsPanel, sessionPanel } from "./panels.js";
20
+ import { modelItems, permissionPickerSpec, sessionItems, thinkingItems, treeItems, } from "./pickers.js";
17
21
  export const INTERACTIVE_COMMANDS = [
18
22
  { name: "tree", description: "浏览会话树,回到某条消息之前重写" },
19
23
  { name: "permissions", description: "权限模式、判定顺序与规则" },
@@ -21,11 +25,15 @@ export const INTERACTIVE_COMMANDS = [
21
25
  /** 补全与 /help 用的完整命令表。 */
22
26
  export const ALL_COMMANDS = [...BUILTIN_COMMANDS, ...INTERACTIVE_COMMANDS];
23
27
  export const KEY_HINTS = [
24
- "Enter 发送(运行中 = steer) Alt+Enter 排到本轮之后 Shift+Enter / Ctrl+J 换行",
28
+ "Enter 发送(运行中 = 插话) Alt+Enter 排到本轮之后 Shift+Enter / Ctrl+J 换行",
25
29
  "Esc 中断(排队消息回填编辑器) Alt+↑ 取回最后一条排队消息",
26
- "Shift+Tab 切换权限模式 Ctrl+L 模型 Ctrl+T 思考级别 Ctrl+O 展开工具输出",
30
+ "Shift+Tab 切换权限模式 Ctrl+L 模型 Ctrl+T 思考级别 Ctrl+O 展开工具输出与思考",
31
+ "审批:1–3 或 ↑↓ Enter 选择,y 允许 a 本会话允许同类 n / Esc 拒绝 v 完整输入",
27
32
  "Ctrl+C 清空输入(再按退出) Ctrl+D 空输入时退出 Tab 补全 @ 引用文件",
28
33
  ].join("\n");
34
+ function homeOf(ui) {
35
+ return ui.home !== undefined ? { home: ui.home } : {};
36
+ }
29
37
  function userEntryText(entry) {
30
38
  if (entry?.type !== "message" || entry.message.role !== "user")
31
39
  return undefined;
@@ -39,12 +47,26 @@ export function activeBranchIds(session) {
39
47
  }
40
48
  export function permissionsText(runtime, session) {
41
49
  const rules = runtime.permission.rules;
50
+ const mode = session.state.permissionMode;
51
+ const order = mode === "auto"
52
+ ? "deny 规则 → Hook deny → 危险命令确认 → 规则层(受保护路径、项目外写入、网络、删除类)→ Hook ask → allow 规则 / Hook allow / 本会话记忆 → 静态判定(只读、项目内写入、安全名单)→ 模型分类器 → 询问"
53
+ : mode === "allowlist"
54
+ ? "deny 规则 → Hook deny → 危险命令(拒绝)→ 只读工具 / allow 规则 / Hook allow 放行 → 其余拒绝(从不询问)"
55
+ : "deny 规则 → Hook deny → 危险命令确认 → 权限模式 → allow 规则 / Hook allow / 本会话记忆 → 询问";
42
56
  const lines = [
43
- `权限模式:${session.state.permissionMode}`,
44
- "判定顺序:deny 规则 → Hook deny → 危险命令确认 → 权限模式 → allow 规则 / Hook allow / 本会话记忆 → 询问",
57
+ `权限模式:${permissionModeLabel(mode)}(${mode})`,
58
+ `判定顺序:${order}`,
45
59
  rules.length === 0 ? "规则:(无)" : `规则(${rules.length}):`,
46
60
  ...rules.map((r) => ` ${r.effect === "deny" ? "deny " : "allow"} ${r.raw} [${r.source}]`),
47
61
  ];
62
+ const recent = runtime.permission.autoDecisions?.() ?? [];
63
+ if (recent.length > 0) {
64
+ lines.push(`最近的 auto 判定(${recent.length}):`);
65
+ for (const d of recent) {
66
+ const cached = d.cached === true ? "(缓存)" : "";
67
+ lines.push(` ${AUTO_LAYER_TEXT[d.layer]} ${d.decision} ${d.toolName} ${d.summary} — ${d.reason}${cached}`);
68
+ }
69
+ }
48
70
  return lines.join("\n");
49
71
  }
50
72
  async function pickTree(ui, title) {
@@ -86,11 +108,15 @@ async function handlePick(what, ui) {
86
108
  switch (what) {
87
109
  case "model": {
88
110
  const current = session.state.model;
111
+ const ref = current === undefined ? undefined : `${current.provider}/${current.id}`;
89
112
  const picked = await ui.pick({
90
113
  title: "选择模型",
91
- items: await modelItems(ui.runtime.providers),
114
+ items: await modelItems(ui.env === undefined
115
+ ? ui.runtime.providers
116
+ : hideFakeProvider(ui.runtime.providers, ui.env)),
92
117
  filterable: true,
93
- ...(current !== undefined ? { selected: `${current.provider}/${current.id}` } : {}),
118
+ showCount: true,
119
+ ...(ref !== undefined ? { selected: ref, currentValue: ref } : {}),
94
120
  });
95
121
  if (picked === undefined)
96
122
  return;
@@ -129,13 +155,12 @@ async function handlePick(what, ui) {
129
155
  return;
130
156
  }
131
157
  case "permission": {
132
- const picked = await ui.pick({
133
- title: "权限模式",
134
- items: permissionItems(),
135
- selected: session.state.permissionMode,
136
- });
137
- if (picked !== undefined)
138
- session.setPermissionMode(picked.value);
158
+ const picked = await ui.pick(permissionPickerSpec(session.state.permissionMode, ui.runtime.config.permission?.mode ?? "default"));
159
+ if (picked === undefined)
160
+ return;
161
+ const mode = picked.value;
162
+ session.setPermissionMode(mode);
163
+ ui.notice("info", `权限模式:${permissionModeLabel(mode)}`);
139
164
  return;
140
165
  }
141
166
  case "thinking": {
@@ -163,8 +188,23 @@ export async function runInteractiveCommand(line, ui) {
163
188
  await treeCommand(ui);
164
189
  return true;
165
190
  }
191
+ const theme = ui.theme?.();
166
192
  if (parsed.name === "permissions") {
167
- ui.notice("info", permissionsText(ui.runtime, ui.session()));
193
+ if (ui.panel !== undefined && theme !== undefined) {
194
+ ui.panel(permissionsPanel(ui.runtime, ui.session(), theme));
195
+ }
196
+ else
197
+ ui.notice("info", permissionsText(ui.runtime, ui.session()));
198
+ return true;
199
+ }
200
+ if ((parsed.name === "session" || parsed.name === "cache") &&
201
+ parsed.args.trim() === "" &&
202
+ ui.panel !== undefined &&
203
+ theme !== undefined) {
204
+ const session = ui.session();
205
+ ui.panel(parsed.name === "session"
206
+ ? sessionPanel(session, theme, { now: ui.now(), ...homeOf(ui) })
207
+ : cachePanel(session, theme, ui.now()));
168
208
  return true;
169
209
  }
170
210
  const result = await runSlashCommand(line, {
@@ -11,7 +11,8 @@
11
11
  * - 晚绑定:`runtime.approvals.setUiBroker`(审批对话框)与 `runtime.notifier.set`(宿主通知进消息区),
12
12
  * 退出时撤下。
13
13
  * - 键位:Enter 发送(运行中 = steer),其余应用级键位见 `key-dispatch.ts`。
14
- * - 启动画面按 `ui.quietStartup`:normal 标题 + 模型 / 信任 / 资源清单,header 只有标题,silent 不输出。
14
+ * - 启动头按 `ui.quietStartup`(startup-header.ts):normal 框 + 模型 / 目录 / 模式 / 资源清单(窄屏或
15
+ * `ui.compact` 去框),header 一行,silent 不输出。
15
16
  */
16
17
  import type { AgentSession } from "../../agent/types.js";
17
18
  import type { ModeContext } from "../../cli/deps.js";