@armadra/agent 0.2.1 → 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 (232) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +142 -49
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +6 -3
  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-state.js +2 -1
  10. package/dist/agent/session-sync.js +9 -2
  11. package/dist/agent/session-tools.js +24 -3
  12. package/dist/agent/session.d.ts +3 -0
  13. package/dist/agent/session.js +20 -10
  14. package/dist/agent/tool-runner.js +15 -2
  15. package/dist/agent/types.d.ts +9 -1
  16. package/dist/ai/apis/anthropic-messages.js +3 -2
  17. package/dist/ai/apis/google-generative-ai.js +3 -2
  18. package/dist/ai/apis/openai-completions.js +3 -2
  19. package/dist/ai/apis/openai-responses.js +5 -3
  20. package/dist/ai/cache/fingerprint.d.ts +1 -1
  21. package/dist/ai/cache/fingerprint.js +1 -1
  22. package/dist/ai/cache/reporting.js +2 -1
  23. package/dist/ai/http.d.ts +28 -6
  24. package/dist/ai/http.js +41 -8
  25. package/dist/ai/providers/channels.d.ts +51 -0
  26. package/dist/ai/providers/channels.js +96 -0
  27. package/dist/ai/providers/enrich.d.ts +34 -0
  28. package/dist/ai/providers/enrich.js +86 -0
  29. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  30. package/dist/ai/providers/models-dev-cache.js +147 -0
  31. package/dist/ai/providers/models-dev.d.ts +99 -0
  32. package/dist/ai/providers/models-dev.js +315 -0
  33. package/dist/ai/providers/registry.d.ts +42 -4
  34. package/dist/ai/providers/registry.js +195 -34
  35. package/dist/ai/providers/suggest.d.ts +18 -0
  36. package/dist/ai/providers/suggest.js +72 -0
  37. package/dist/ai/sse.d.ts +6 -2
  38. package/dist/ai/sse.js +22 -2
  39. package/dist/ai/types.d.ts +30 -3
  40. package/dist/bundle/ama.cjs +17893 -10698
  41. package/dist/cli/args.d.ts +23 -5
  42. package/dist/cli/args.js +113 -13
  43. package/dist/cli/bootstrap.js +28 -3
  44. package/dist/cli/codemode-notice.d.ts +20 -0
  45. package/dist/cli/codemode-notice.js +55 -0
  46. package/dist/cli/compose-providers.js +3 -0
  47. package/dist/cli/compose-session.d.ts +8 -0
  48. package/dist/cli/compose-session.js +32 -1
  49. package/dist/cli/compose-store.d.ts +1 -1
  50. package/dist/cli/compose-store.js +3 -1
  51. package/dist/cli/compose.d.ts +17 -5
  52. package/dist/cli/compose.js +53 -19
  53. package/dist/cli/default-model.d.ts +38 -1
  54. package/dist/cli/default-model.js +96 -9
  55. package/dist/cli/deps.d.ts +38 -0
  56. package/dist/cli/exit-codes.d.ts +2 -0
  57. package/dist/cli/exit-codes.js +3 -0
  58. package/dist/cli/fake-visibility.d.ts +13 -0
  59. package/dist/cli/fake-visibility.js +26 -0
  60. package/dist/cli/from-prompt.d.ts +18 -0
  61. package/dist/cli/from-prompt.js +49 -0
  62. package/dist/cli/main.d.ts +10 -2
  63. package/dist/cli/main.js +88 -2
  64. package/dist/cli/proxy.d.ts +51 -0
  65. package/dist/cli/proxy.js +135 -0
  66. package/dist/cli/startup-screen.d.ts +29 -0
  67. package/dist/cli/startup-screen.js +47 -0
  68. package/dist/cli/startup-steps.d.ts +1 -1
  69. package/dist/cli/startup-steps.js +20 -13
  70. package/dist/cli/subcommands/config.d.ts +29 -4
  71. package/dist/cli/subcommands/config.js +188 -25
  72. package/dist/cli/subcommands/context.js +5 -1
  73. package/dist/cli/subcommands/doctor.js +12 -1
  74. package/dist/cli/subcommands/init.d.ts +6 -0
  75. package/dist/cli/subcommands/init.js +23 -0
  76. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  77. package/dist/cli/subcommands/model-meta.js +54 -0
  78. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  79. package/dist/cli/subcommands/models-discover.d.ts +18 -6
  80. package/dist/cli/subcommands/models-discover.js +87 -52
  81. package/dist/cli/subcommands/models.js +26 -18
  82. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  83. package/dist/cli/subcommands/probe-runner.js +264 -0
  84. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  85. package/dist/cli/subcommands/providers-list.js +97 -0
  86. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  87. package/dist/cli/subcommands/providers-plan.js +215 -0
  88. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  89. package/dist/cli/subcommands/providers-probe.js +87 -0
  90. package/dist/cli/subcommands/providers.d.ts +28 -0
  91. package/dist/cli/subcommands/providers.js +436 -0
  92. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  93. package/dist/cli/subcommands/sessions-export.js +59 -0
  94. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  95. package/dist/cli/subcommands/sessions-search.js +103 -0
  96. package/dist/cli/subcommands/sessions.d.ts +3 -2
  97. package/dist/cli/subcommands/sessions.js +22 -1
  98. package/dist/cli/subcommands/stats.d.ts +17 -0
  99. package/dist/cli/subcommands/stats.js +198 -0
  100. package/dist/cli/system-prompt-arg.d.ts +11 -0
  101. package/dist/cli/system-prompt-arg.js +34 -0
  102. package/dist/codemode/modes.d.ts +4 -11
  103. package/dist/codemode/modes.js +5 -27
  104. package/dist/codemode/tool.d.ts +18 -14
  105. package/dist/codemode/tool.js +71 -28
  106. package/dist/config/init.d.ts +34 -0
  107. package/dist/config/init.js +99 -0
  108. package/dist/config/json-schema.d.ts +15 -0
  109. package/dist/config/json-schema.js +215 -0
  110. package/dist/config/key-docs.d.ts +22 -0
  111. package/dist/config/key-docs.js +105 -0
  112. package/dist/config/merge.d.ts +9 -8
  113. package/dist/config/merge.js +22 -7
  114. package/dist/config/schema.d.ts +1 -1
  115. package/dist/config/schema.js +97 -15
  116. package/dist/config/types.d.ts +57 -11
  117. package/dist/config/types.js +19 -1
  118. package/dist/modes/commands-core.js +6 -5
  119. package/dist/modes/image-input.d.ts +27 -0
  120. package/dist/modes/image-input.js +78 -0
  121. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  122. package/dist/modes/interactive/approval-dialog.js +106 -27
  123. package/dist/modes/interactive/commands.d.ts +11 -2
  124. package/dist/modes/interactive/commands.js +58 -19
  125. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  126. package/dist/modes/interactive/interactive-mode.js +70 -82
  127. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  128. package/dist/modes/interactive/key-dispatch.js +5 -6
  129. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  130. package/dist/modes/interactive/line/line-mode.js +9 -5
  131. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  132. package/dist/modes/interactive/line/line-render.js +28 -6
  133. package/dist/modes/interactive/message-view.d.ts +48 -9
  134. package/dist/modes/interactive/message-view.js +238 -44
  135. package/dist/modes/interactive/panels.d.ts +18 -0
  136. package/dist/modes/interactive/panels.js +143 -0
  137. package/dist/modes/interactive/pickers.d.ts +23 -2
  138. package/dist/modes/interactive/pickers.js +48 -15
  139. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  140. package/dist/modes/interactive/run-indicator.js +189 -0
  141. package/dist/modes/interactive/startup-header.d.ts +40 -0
  142. package/dist/modes/interactive/startup-header.js +169 -0
  143. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  144. package/dist/modes/interactive/startup-ui.js +41 -11
  145. package/dist/modes/interactive/status-bar.d.ts +18 -15
  146. package/dist/modes/interactive/status-bar.js +98 -56
  147. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  148. package/dist/modes/interactive/tool-summary.js +218 -0
  149. package/dist/modes/interactive/tool-view.d.ts +48 -15
  150. package/dist/modes/interactive/tool-view.js +203 -145
  151. package/dist/modes/print/print-mode.d.ts +33 -4
  152. package/dist/modes/print/print-mode.js +128 -8
  153. package/dist/modes/rpc/commands.js +12 -2
  154. package/dist/permissions/auto-safe.d.ts +60 -0
  155. package/dist/permissions/auto-safe.js +529 -0
  156. package/dist/permissions/classifier.d.ts +64 -0
  157. package/dist/permissions/classifier.js +184 -0
  158. package/dist/permissions/dangerous.d.ts +5 -0
  159. package/dist/permissions/dangerous.js +1 -1
  160. package/dist/permissions/modes.d.ts +30 -0
  161. package/dist/permissions/modes.js +78 -0
  162. package/dist/permissions/pipeline.d.ts +31 -4
  163. package/dist/permissions/pipeline.js +196 -6
  164. package/dist/permissions/protected.d.ts +19 -0
  165. package/dist/permissions/protected.js +74 -0
  166. package/dist/permissions/rules.js +3 -0
  167. package/dist/permissions/types.d.ts +50 -3
  168. package/dist/permissions/types.js +3 -0
  169. package/dist/rpc.d.ts +2 -0
  170. package/dist/sdk.d.ts +9 -3
  171. package/dist/sdk.js +10 -2
  172. package/dist/session/export.d.ts +32 -0
  173. package/dist/session/export.js +187 -0
  174. package/dist/session/projection.js +5 -1
  175. package/dist/session/redact.d.ts +15 -0
  176. package/dist/session/redact.js +55 -0
  177. package/dist/session/reuse.d.ts +33 -0
  178. package/dist/session/reuse.js +86 -0
  179. package/dist/session/scan.d.ts +34 -0
  180. package/dist/session/scan.js +140 -0
  181. package/dist/session/search.d.ts +52 -0
  182. package/dist/session/search.js +211 -0
  183. package/dist/session/stats-aggregate.d.ts +63 -0
  184. package/dist/session/stats-aggregate.js +163 -0
  185. package/dist/session/stats-index.d.ts +26 -0
  186. package/dist/session/stats-index.js +91 -0
  187. package/dist/session/stats-scan.d.ts +54 -0
  188. package/dist/session/stats-scan.js +236 -0
  189. package/dist/session/types.d.ts +2 -0
  190. package/dist/tools/image-file.d.ts +31 -0
  191. package/dist/tools/image-file.js +114 -0
  192. package/dist/tools/presets.d.ts +35 -8
  193. package/dist/tools/presets.js +56 -17
  194. package/dist/tools/read.d.ts +4 -8
  195. package/dist/tools/read.js +15 -64
  196. package/dist/tui/component.d.ts +8 -2
  197. package/dist/tui/component.js +3 -1
  198. package/dist/tui/components/box.d.ts +6 -1
  199. package/dist/tui/components/box.js +16 -6
  200. package/dist/tui/components/card.d.ts +23 -0
  201. package/dist/tui/components/card.js +37 -0
  202. package/dist/tui/components/editor-history.d.ts +6 -0
  203. package/dist/tui/components/editor-history.js +45 -0
  204. package/dist/tui/components/editor-paste.d.ts +1 -1
  205. package/dist/tui/components/editor-paste.js +4 -4
  206. package/dist/tui/components/editor.d.ts +11 -5
  207. package/dist/tui/components/editor.js +52 -58
  208. package/dist/tui/components/key-value.d.ts +3 -0
  209. package/dist/tui/components/key-value.js +16 -6
  210. package/dist/tui/components/loader.d.ts +37 -7
  211. package/dist/tui/components/loader.js +84 -21
  212. package/dist/tui/components/markdown.d.ts +5 -1
  213. package/dist/tui/components/markdown.js +45 -21
  214. package/dist/tui/components/meter.d.ts +3 -3
  215. package/dist/tui/components/meter.js +13 -11
  216. package/dist/tui/components/select-list.d.ts +23 -1
  217. package/dist/tui/components/select-list.js +76 -13
  218. package/dist/tui/glyphs.d.ts +68 -0
  219. package/dist/tui/glyphs.js +114 -0
  220. package/dist/tui/theme.d.ts +30 -6
  221. package/dist/tui/theme.js +103 -17
  222. package/dist/tui.d.ts +4 -2
  223. package/dist/tui.js +3 -1
  224. package/docs/codemode.md +23 -9
  225. package/docs/hooks.md +10 -10
  226. package/docs/permissions.md +148 -0
  227. package/docs/providers.md +241 -3
  228. package/docs/rpc.md +38 -38
  229. package/docs/session-format.md +3 -2
  230. package/docs/sessions.md +134 -0
  231. package/docs/tui.md +140 -61
  232. package/package.json +3 -1
@@ -0,0 +1,169 @@
1
+ /**
2
+ * 交互模式启动头(终端界面视觉设计 v1 §3.1)。
3
+ *
4
+ * - `normal`:宽度 ≥ 56 且非 `ui.compact` 时画框(宽 `min(width, 64)`,左对齐)——标题 `✻ ama x.y.z`、
5
+ * 键值行(模型 / 目录 / 模式 / 已加载 / 宿主 / 警告)、按键提示;否则去框去键列,每项一行。
6
+ * - `header`:一行 `✻ ama x.y.z · 模型 · 模式 · /help`,无框。`silent` 不输出(调用方不加本组件)。
7
+ * - 着色:`✻` accent、标题粗体;键列 dim;模型 accent;已信任 success / 未信任 warning;Bypass 模式 warning;
8
+ * 按键提示 dim。路径过长时从左截断(`…/armadra-agent`)。宽度变化时按新宽重算是否去框。
9
+ */
10
+ import { permissionModeLabel } from "../../permissions/modes.js";
11
+ import { Box, KeyValue, padToWidth, truncateToWidth, visibleWidth, } from "../../tui.js";
12
+ /** 画框的最小宽度;更窄时去框去键列。 */
13
+ export const HEADER_BOX_MIN_WIDTH = 56;
14
+ /** 设计稿写 60;常见的「供应商/模型@渠道 · 思考 级别」要 62 列左右才不截断,取 64。 */
15
+ export const HEADER_BOX_MAX_WIDTH = 64;
16
+ /** 从左截断到 `width` 列(保留末尾,前面加省略号)。 */
17
+ export function truncateLeft(text, width, ellipsis = "…") {
18
+ if (visibleWidth(text) <= width)
19
+ return text;
20
+ const room = width - visibleWidth(ellipsis);
21
+ if (room <= 0)
22
+ return truncateToWidth(ellipsis, width);
23
+ const chars = [...text];
24
+ let out = "";
25
+ let used = 0;
26
+ for (let i = chars.length - 1; i >= 0; i--) {
27
+ const w = visibleWidth(chars[i]);
28
+ if (used + w > room)
29
+ break;
30
+ out = chars[i] + out;
31
+ used += w;
32
+ }
33
+ return ellipsis + out;
34
+ }
35
+ /** 去掉模型引用里的渠道(`@messages`)。 */
36
+ function withoutChannel(model) {
37
+ const at = model.indexOf("@");
38
+ return at === -1 ? model : model.slice(0, at);
39
+ }
40
+ export class StartupHeader {
41
+ info;
42
+ options;
43
+ cache = null;
44
+ constructor(info, options) {
45
+ this.info = info;
46
+ this.options = options;
47
+ }
48
+ get theme() {
49
+ return this.options.theme;
50
+ }
51
+ title() {
52
+ const t = this.theme;
53
+ return `${t.fg("accent", t.glyphs.thinking)} ${t.bold(`ama ${this.info.version}`)}`;
54
+ }
55
+ modeText() {
56
+ const label = permissionModeLabel(this.info.permissionMode);
57
+ return this.info.permissionMode === "full-auto" ? this.theme.fg("warning", label) : label;
58
+ }
59
+ trustText(withSource) {
60
+ const { trusted, trustSource } = this.info;
61
+ const text = `${trusted ? "已信任" : "未信任"}${withSource ? `(${trustSource})` : ""}`;
62
+ return this.theme.fg(trusted ? "success" : "warning", text);
63
+ }
64
+ loadedText() {
65
+ const { contextFiles, skills, prompts, hooks } = this.info;
66
+ const parts = [];
67
+ if (contextFiles.length > 0)
68
+ parts.push(contextFiles.join(", "));
69
+ if (skills > 0)
70
+ parts.push(`${skills} Skill`);
71
+ if (prompts > 0)
72
+ parts.push(`${prompts} 模板`);
73
+ if (hooks > 0)
74
+ parts.push(`${hooks} Hook`);
75
+ return parts.length === 0 ? undefined : parts.join(" · ");
76
+ }
77
+ codemodeText() {
78
+ return this.info.codemode === "off" ? undefined : `codemode ${this.info.codemode}`;
79
+ }
80
+ render(width) {
81
+ if (this.cache?.width === width)
82
+ return this.cache.lines;
83
+ const lines = this.options.level === "header"
84
+ ? [this.headerLine(width)]
85
+ : this.options.compact === true || width < HEADER_BOX_MIN_WIDTH
86
+ ? this.plainLines(width)
87
+ : this.boxLines(width);
88
+ this.cache = { width, lines };
89
+ return lines;
90
+ }
91
+ headerLine(width) {
92
+ const t = this.theme;
93
+ const sep = t.fg("dim", " · ");
94
+ const parts = [
95
+ this.title(),
96
+ t.fg("accent", withoutChannel(this.info.model)),
97
+ this.modeText(),
98
+ t.fg("dim", "/help"),
99
+ ];
100
+ return truncateToWidth(parts.join(sep), width);
101
+ }
102
+ plainLines(width) {
103
+ const t = this.theme;
104
+ const sep = t.fg("dim", " · ");
105
+ const ellipsis = t.glyphs.ellipsis;
106
+ const { info } = this;
107
+ const lines = [this.title()];
108
+ lines.push(t.fg("accent", withoutChannel(info.model)) + sep + info.thinking);
109
+ const trust = this.trustText(false);
110
+ const room = Math.max(4, width - visibleWidth(trust) - 3);
111
+ lines.push(truncateLeft(info.cwd, room, ellipsis) + sep + trust);
112
+ const mode = [this.modeText(), info.preset, this.codemodeText()].filter((p) => p !== undefined);
113
+ lines.push(mode.join(sep));
114
+ const loaded = this.loadedText();
115
+ if (loaded !== undefined)
116
+ lines.push(loaded);
117
+ if (info.host !== undefined)
118
+ lines.push(`宿主 ${info.host}`);
119
+ if (info.warnings > 0)
120
+ lines.push(t.fg("warning", `警告 ${info.warnings} 条(ama doctor)`));
121
+ lines.push(t.fg("dim", "/help · Shift+Tab 切模式"));
122
+ return lines.map((line) => truncateToWidth(line, width));
123
+ }
124
+ boxLines(width) {
125
+ const t = this.theme;
126
+ const sep = t.fg("dim", " · ");
127
+ const { info } = this;
128
+ const boxWidth = Math.min(width, HEADER_BOX_MAX_WIDTH);
129
+ /** 框内文本宽:边框 2 + 内边距 2;键列 6 + 间隔 1。 */
130
+ const valueWidth = boxWidth - 4 - 7;
131
+ const trust = this.trustText(true);
132
+ const cwdRoom = Math.max(4, valueWidth - visibleWidth(trust) - 3);
133
+ const rows = [
134
+ { key: "模型", value: t.fg("accent", info.model) + sep + `思考 ${info.thinking}` },
135
+ { key: "目录", value: truncateLeft(info.cwd, cwdRoom, t.glyphs.ellipsis) + sep + trust },
136
+ {
137
+ key: "模式",
138
+ value: [this.modeText(), `预设 ${info.preset}`, this.codemodeText()]
139
+ .filter((p) => p !== undefined)
140
+ .join(sep),
141
+ },
142
+ ];
143
+ const loaded = this.loadedText();
144
+ if (loaded !== undefined)
145
+ rows.push({ key: "已加载", value: loaded });
146
+ if (info.host !== undefined)
147
+ rows.push({ key: "宿主", value: info.host });
148
+ if (info.warnings > 0) {
149
+ rows.push({ key: "警告", value: t.fg("warning", `${info.warnings} 条(ama doctor 查看)`) });
150
+ }
151
+ // 键列固定 6 列(「已加载」的宽度),没有这一行时也对齐
152
+ const padded = rows.map((row) => ({ ...row, key: padToWidth(row.key, 6) }));
153
+ const kv = new KeyValue(padded, { theme: t, gap: 1, maxKeyRatio: 1 });
154
+ const body = {
155
+ render: (inner) => [
156
+ this.title(),
157
+ "",
158
+ ...kv.render(inner),
159
+ "",
160
+ t.fg("dim", "/help 命令 · Shift+Tab 切模式 · Ctrl+O 展开工具输出"),
161
+ ],
162
+ invalidate: () => kv.invalidate(),
163
+ };
164
+ return new Box(body, { theme: t }).render(boxWidth);
165
+ }
166
+ invalidate() {
167
+ this.cache = null;
168
+ }
169
+ }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * `modelItems` / `sessionItems` 同时供交互模式里的选择器(pickers.ts)使用。
9
9
  */
10
- import type { ProviderRegistryApi } from "../../ai/types.js";
10
+ import type { Model, ProviderRegistryApi } from "../../ai/types.js";
11
11
  import type { InteractiveUi } from "../../cli/deps.js";
12
12
  import type { SessionListItem } from "../../session/types.js";
13
13
  import { type Keybindings, type SelectItem, type Terminal, type Theme } from "../../tui.js";
@@ -19,7 +19,12 @@ export interface StartupUiOptions {
19
19
  /** 会话列表的相对时间基准(测试注入)。 */
20
20
  now?: () => number;
21
21
  }
22
- /** 模型按供应商分组;有 key(或本地)的供应商排前,组标题标 key 状态。 */
22
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
23
+ export declare function modelDescription(model: Model): string | undefined;
24
+ /**
25
+ * 模型按「供应商 · 渠道」分组;有 key(或本地)的供应商排前,组标题标 key 状态。多渠道供应商的模型在
26
+ * 每个挂载的渠道下各出现一次,非首选渠道的值带 `@渠道`。
27
+ */
23
28
  export declare function modelItems(providers: ProviderRegistryApi): Promise<SelectItem[]>;
24
29
  /** `刚刚 / 5 分钟前 / 3 小时前 / 2 天前 / 2026-09-01`。 */
25
30
  export declare function relativeTime(iso: string, now: number): string;
@@ -102,7 +102,24 @@ function askText(r, prompt) {
102
102
  // ---------------------------------------------------------------------------
103
103
  // 列表项(选择器共用)
104
104
  // ---------------------------------------------------------------------------
105
- /** 模型按供应商分组;有 key(或本地)的供应商排前,组标题标 key 状态。 */
105
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
106
+ export function modelDescription(model) {
107
+ const ctx = model.contextWindow;
108
+ const parts = [
109
+ model.name !== "" && model.name !== model.id ? model.name : undefined,
110
+ ctx === undefined
111
+ ? undefined
112
+ : ctx >= 1_000_000
113
+ ? `${Math.round(ctx / 100_000) / 10}M`
114
+ : `${Math.round(ctx / 1000)}k`,
115
+ model.input.includes("image") ? "img" : undefined,
116
+ ].filter((x) => x !== undefined);
117
+ return parts.length > 0 ? parts.join(" · ") : undefined;
118
+ }
119
+ /**
120
+ * 模型按「供应商 · 渠道」分组;有 key(或本地)的供应商排前,组标题标 key 状态。多渠道供应商的模型在
121
+ * 每个挂载的渠道下各出现一次,非首选渠道的值带 `@渠道`。
122
+ */
106
123
  export async function modelItems(providers) {
107
124
  const groups = [];
108
125
  for (const provider of providers.list()) {
@@ -117,16 +134,29 @@ export async function modelItems(providers) {
117
134
  ready = key.apiKey !== undefined;
118
135
  status = ready ? "key ✓" : "无 key";
119
136
  }
120
- const group = `${provider.id} · ${status}`;
121
- groups.push({
122
- ready,
123
- items: provider.models.map((model) => {
124
- const item = { value: `${provider.id}/${model.id}`, label: model.id, group };
125
- if (model.name !== "" && model.name !== model.id)
126
- item.description = model.name;
127
- return item;
128
- }),
129
- });
137
+ const item = (model, group, channel) => {
138
+ const suffix = channel !== undefined && channel !== model.channel ? `@${channel}` : "";
139
+ const out = {
140
+ value: `${provider.id}/${model.id}${suffix}`,
141
+ label: `${model.id}${suffix}`,
142
+ group,
143
+ };
144
+ const description = modelDescription(model);
145
+ if (description !== undefined)
146
+ out.description = description;
147
+ return out;
148
+ };
149
+ if (provider.channels === undefined) {
150
+ const group = `${provider.id} · ${status}`;
151
+ groups.push({ ready, items: provider.models.map((model) => item(model, group)) });
152
+ continue;
153
+ }
154
+ for (const channel of provider.channels) {
155
+ const group = `${provider.id} · ${channel.name} · ${status}`;
156
+ const models = provider.models.filter((m) => m.channels?.includes(channel.name));
157
+ if (models.length > 0)
158
+ groups.push({ ready, items: models.map((model) => item(model, group, channel.name)) });
159
+ }
130
160
  }
131
161
  return [...groups.filter((g) => g.ready), ...groups.filter((g) => !g.ready)].flatMap((g) => g.items);
132
162
  }
@@ -1,20 +1,20 @@
1
1
  /**
2
- * 状态栏(设计 §12.6):编辑器下方一行。[B7]
2
+ * 状态栏(设计 §12.6;终端界面视觉设计 v1 §3.10):编辑器下方一行,永远是最后一行。[B7]
3
3
  *
4
- * `provider/model · think:medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · rebill $0.11 · ctx 34%
5
- * · queue 1 · mode:default · codemode:only net! · preset:default · [宿主状态]`
4
+ * 两区:左区「权限模式 · shift+tab 切换」,右区用量,中间用空格撑开;空隙不足 4 列时退回单区(` · ` 连接)。
5
+ * `Accept edits · shift+tab 切换 sonnet-4-5 · medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · ctx 34%`
6
6
  *
7
- * - 用量来自 `session.getStats()`(`↑` = 输入含缓存读写,`↓` = 输出),只在 `refresh()` 时读取
8
- * (事件驱动),渲染只拼字符串;
9
- * - [W3-C2] `cache` 是**最近一次**请求的命中率(会话累计在 `/session`);端点三态:`unknown` →
10
- * `cache —`,`silent` → `cache 未报告`;保温计时中追加 `♨`;会话有重计费时显示 `rebill $x`
11
- * (无价模型显示重计费 token);会话层缓存未接线时回落到会话命中率 `cacheHitRate`;
12
- * - `ctx ?` 表示模型没有上下文窗口;≥ 70% 黄、≥ 90% 红;没有用量时不显示 token / cache / 费用;
13
- * - [W3-C2 A4] codemode 生效时显示 `codemode:on|only`,运行时 Node 的权限模型不隔离网络
14
- * (`detectSandboxCapability().strict` 为 false)时追加红色 `net!`;
15
- * - 宿主状态读 `HostAdapterHandle.status()`(宿主 `ui.setStatus`),每次渲染现取;
16
- * - 宽度不够时按优先级丢弃(宿主 → 预设 → rebill → 费用 → cache → token → think → 队列 →
17
- * codemode),模型、ctx、权限模式最后丢。
7
+ * - 右区顺序固定:模型 · 思考级别(只显示值,off 不显示)· `↑` 输入(含缓存读写)`↓` 输出 · cache · 费用 ·
8
+ * rebill · ctx · queue · codemode · 预设(非 default 才显示)· [宿主状态];分隔符固定为 ` · `,
9
+ * 模式永远在最左(tmux 宿主可按此解析)。
10
+ * - 用量来自 `session.getStats()`,只在 `refresh()` 时读取(事件驱动),渲染只拼字符串;
11
+ * - [W3-C2] `cache` 是**最近一次**请求的命中率;端点三态 `cache —` / `cache 未报告`;保温中追加 `♨`;
12
+ * 重计费 `rebill $x`(无价模型显示 token);codemode 生效时 `codemode on|only`,网络未隔离追加 `net!`;
13
+ * - 着色:整行 dim;模式名正文色(Bypass permissions warning、Plan accent);模型 accent;ctx 按阈值
14
+ * success / warning / error;rebill、queue warning;`net!` error。宽 ≥ 110 时 ctx 用余量表。
15
+ * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
16
+ * - 宽度不够时按优先级丢弃(数字大的先丢):切换提示 12 → 宿主 11 → 预设 10 → rebill 9 → 费用 8 →
17
+ * cache 7 → token 6 → 思考 5 → 队列 4 → codemode 3 → ctx 1 → 模型 0;模式永不丢。
18
18
  */
19
19
  import type { AgentSession, SessionCacheStats } from "../../agent/types.js";
20
20
  import { type Component, type Theme } from "../../tui.js";
@@ -32,7 +32,9 @@ export interface StatusBarSource {
32
32
  export declare function formatTokens(count: number): string;
33
33
  export declare function formatCost(cost: number): string;
34
34
  /** `cache 83%` / `cache —` / `cache 未报告`,保温计时中追加 ` ♨`。 */
35
- export declare function cacheText(cache: SessionCacheStats): string;
35
+ export declare function cacheText(cache: SessionCacheStats, warm?: string): string;
36
+ /** 按终端宽度缩写模型名(§3.10)。 */
37
+ export declare function abbreviateModel(ref: string, width: number): string;
36
38
  export declare class StatusBar implements Component {
37
39
  private readonly source;
38
40
  private readonly theme;
@@ -43,6 +45,7 @@ export declare class StatusBar implements Component {
43
45
  refresh(): void;
44
46
  setQueue(steering: number, followUp: number): void;
45
47
  private parts;
48
+ private ctxText;
46
49
  render(width: number): string[];
47
50
  invalidate(): void;
48
51
  }
@@ -1,22 +1,24 @@
1
1
  /**
2
- * 状态栏(设计 §12.6):编辑器下方一行。[B7]
2
+ * 状态栏(设计 §12.6;终端界面视觉设计 v1 §3.10):编辑器下方一行,永远是最后一行。[B7]
3
3
  *
4
- * `provider/model · think:medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · rebill $0.11 · ctx 34%
5
- * · queue 1 · mode:default · codemode:only net! · preset:default · [宿主状态]`
4
+ * 两区:左区「权限模式 · shift+tab 切换」,右区用量,中间用空格撑开;空隙不足 4 列时退回单区(` · ` 连接)。
5
+ * `Accept edits · shift+tab 切换 sonnet-4-5 · medium · ↑12.3k ↓1.2k · cache 80% ♨ · $0.12 · ctx 34%`
6
6
  *
7
- * - 用量来自 `session.getStats()`(`↑` = 输入含缓存读写,`↓` = 输出),只在 `refresh()` 时读取
8
- * (事件驱动),渲染只拼字符串;
9
- * - [W3-C2] `cache` 是**最近一次**请求的命中率(会话累计在 `/session`);端点三态:`unknown` →
10
- * `cache —`,`silent` → `cache 未报告`;保温计时中追加 `♨`;会话有重计费时显示 `rebill $x`
11
- * (无价模型显示重计费 token);会话层缓存未接线时回落到会话命中率 `cacheHitRate`;
12
- * - `ctx ?` 表示模型没有上下文窗口;≥ 70% 黄、≥ 90% 红;没有用量时不显示 token / cache / 费用;
13
- * - [W3-C2 A4] codemode 生效时显示 `codemode:on|only`,运行时 Node 的权限模型不隔离网络
14
- * (`detectSandboxCapability().strict` 为 false)时追加红色 `net!`;
15
- * - 宿主状态读 `HostAdapterHandle.status()`(宿主 `ui.setStatus`),每次渲染现取;
16
- * - 宽度不够时按优先级丢弃(宿主 → 预设 → rebill → 费用 → cache → token → think → 队列 →
17
- * codemode),模型、ctx、权限模式最后丢。
7
+ * - 右区顺序固定:模型 · 思考级别(只显示值,off 不显示)· `↑` 输入(含缓存读写)`↓` 输出 · cache · 费用 ·
8
+ * rebill · ctx · queue · codemode · 预设(非 default 才显示)· [宿主状态];分隔符固定为 ` · `,
9
+ * 模式永远在最左(tmux 宿主可按此解析)。
10
+ * - 用量来自 `session.getStats()`,只在 `refresh()` 时读取(事件驱动),渲染只拼字符串;
11
+ * - [W3-C2] `cache` 是**最近一次**请求的命中率;端点三态 `cache —` / `cache 未报告`;保温中追加 `♨`;
12
+ * 重计费 `rebill $x`(无价模型显示 token);codemode 生效时 `codemode on|only`,网络未隔离追加 `net!`;
13
+ * - 着色:整行 dim;模式名正文色(Bypass permissions warning、Plan accent);模型 accent;ctx 按阈值
14
+ * success / warning / error;rebill、queue warning;`net!` error。宽 ≥ 110 时 ctx 用余量表。
15
+ * - 模型名缩写:宽 < 100 去掉供应商前缀,< 60 再去掉 `@渠道`,< 48 去掉版本后缀(第一个 `-数字` 起)。
16
+ * - 宽度不够时按优先级丢弃(数字大的先丢):切换提示 12 → 宿主 11 → 预设 10 → rebill 9 → 费用 8 →
17
+ * cache 7 → token 6 → 思考 5 → 队列 4 → codemode 3 → ctx 1 → 模型 0;模式永不丢。
18
18
  */
19
- import { truncateToWidth, visibleWidth } from "../../tui.js";
19
+ import { permissionModeLabel } from "../../permissions/modes.js";
20
+ import { formatModelRef } from "../../ai/providers/channels.js";
21
+ import { Meter, levelColor, truncateToWidth, visibleWidth, } from "../../tui.js";
20
22
  export function formatTokens(count) {
21
23
  if (count < 1000)
22
24
  return String(count);
@@ -32,14 +34,29 @@ export function formatCost(cost) {
32
34
  return `$${cost.toFixed(3)}`;
33
35
  }
34
36
  /** `cache 83%` / `cache —` / `cache 未报告`,保温计时中追加 ` ♨`。 */
35
- export function cacheText(cache) {
37
+ export function cacheText(cache, warm = "♨") {
36
38
  const rate = cache.lastHitRate ?? cache.hitRate;
37
39
  const value = cache.reporting === "silent"
38
40
  ? "未报告"
39
41
  : cache.reporting === "reported" && rate !== undefined
40
42
  ? `${Math.round(rate * 100)}%`
41
43
  : "—";
42
- return `cache ${value}${cache.warming.state === "scheduled" ? " ♨" : ""}`;
44
+ return `cache ${value}${cache.warming.state === "scheduled" ? ` ${warm}` : ""}`;
45
+ }
46
+ /** 两区之间至少留的空格;不足时退回单区。 */
47
+ const MIN_GAP = 4;
48
+ /** ctx 改用余量表的最小宽度。 */
49
+ const METER_WIDTH = 110;
50
+ /** 按终端宽度缩写模型名(§3.10)。 */
51
+ export function abbreviateModel(ref, width) {
52
+ let out = ref;
53
+ if (width < 100)
54
+ out = out.slice(out.indexOf("/") + 1);
55
+ if (width < 60)
56
+ out = out.replace(/@.*$/, "");
57
+ if (width < 48)
58
+ out = out.replace(/-\d.*$/, "");
59
+ return out;
43
60
  }
44
61
  export class StatusBar {
45
62
  source;
@@ -57,81 +74,106 @@ export class StatusBar {
57
74
  setQueue(steering, followUp) {
58
75
  this.queue = steering + followUp;
59
76
  }
60
- parts() {
77
+ parts(width) {
61
78
  const theme = this.theme;
79
+ const g = theme.glyphs;
62
80
  const session = this.source.session();
63
81
  const state = session.state;
64
82
  const stats = this.stats ?? session.getStats();
65
83
  this.stats = stats;
66
84
  const parts = [];
67
- const model = state.model === undefined ? "?" : `${state.model.provider}/${state.model.id}`;
68
- parts.push({ text: theme.fg("accent", model), priority: 0 });
69
- parts.push({ text: `think:${state.thinkingLevel}`, priority: 5 });
85
+ /** 普通项整项 dim;`colored` 的项已自带颜色。 */
86
+ const tone = (text, colored) => colored ? text : theme.fg("dim", text);
87
+ const left = (text, priority, colored = false) => void parts.push({ text: tone(text, colored), priority, zone: "left" });
88
+ const right = (text, priority, colored = false) => void parts.push({ text: tone(text, colored), priority, zone: "right" });
89
+ const mode = permissionModeLabel(state.permissionMode);
90
+ left(state.permissionMode === "full-auto"
91
+ ? theme.fg("warning", mode)
92
+ : state.permissionMode === "plan"
93
+ ? theme.fg("accent", mode)
94
+ : theme.fg("text", mode), undefined, true);
95
+ if (width >= 40)
96
+ left("shift+tab 切换", 12);
97
+ const model = state.model === undefined ? "?" : abbreviateModel(formatModelRef(state.model), width);
98
+ right(theme.fg("accent", model), 0, true);
99
+ if (state.thinkingLevel !== "off")
100
+ right(state.thinkingLevel, 5);
70
101
  const t = stats.tokens;
71
102
  const prompt = t.input + t.cacheRead + t.cacheWrite;
103
+ const cache = stats.cache;
72
104
  if (prompt + t.output > 0) {
73
- parts.push({ text: `↑${formatTokens(prompt)} ↓${formatTokens(t.output)}`, priority: 6 });
74
- const cache = stats.cache;
105
+ right(`${g.arrowUp}${formatTokens(prompt)} ${g.arrowDown}${formatTokens(t.output)}`, 6);
75
106
  if (cache !== undefined)
76
- parts.push({ text: cacheText(cache), priority: 7 });
107
+ right(cacheText(cache, g.warm), 7);
77
108
  else if (stats.cacheHitRate !== undefined) {
78
- parts.push({ text: `cache ${Math.round(stats.cacheHitRate * 100)}%`, priority: 7 });
109
+ right(`cache ${Math.round(stats.cacheHitRate * 100)}%`, 7);
79
110
  }
80
111
  if (stats.cost === undefined)
81
- parts.push({ text: "$?", priority: 8 });
112
+ right("$?", 8);
82
113
  else if (stats.cost > 0)
83
- parts.push({ text: formatCost(stats.cost), priority: 8 });
114
+ right(formatCost(stats.cost), 8);
84
115
  if (cache !== undefined && cache.reBilledTokens > 0) {
85
116
  const amount = cache.reBilledUsd === undefined
86
117
  ? `${formatTokens(cache.reBilledTokens)} tok`
87
118
  : formatCost(cache.reBilledUsd);
88
- parts.push({ text: theme.fg("warning", `rebill ${amount}`), priority: 9 });
119
+ right(theme.fg("warning", `rebill ${amount}`), 9, true);
89
120
  }
90
121
  }
91
- const ctx = stats.contextPercent;
92
- const ctxText = ctx === undefined ? "ctx ?" : `ctx ${Math.round(ctx)}%`;
93
- parts.push({
94
- text: ctx !== undefined && ctx >= 90
95
- ? theme.fg("error", ctxText)
96
- : ctx !== undefined && ctx >= 70
97
- ? theme.fg("warning", ctxText)
98
- : ctxText,
99
- priority: 1,
100
- });
122
+ right(this.ctxText(stats.contextPercent, width), 1, true);
101
123
  if (this.queue > 0)
102
- parts.push({ text: theme.fg("warning", `queue ${this.queue}`), priority: 4 });
103
- const mode = `mode:${state.permissionMode}`;
104
- parts.push({
105
- text: state.permissionMode === "full-auto" ? theme.fg("warning", mode) : mode,
106
- priority: 2,
107
- });
124
+ right(theme.fg("warning", `queue ${this.queue}`), 4, true);
108
125
  const codemode = this.source.codemode?.();
109
126
  if (codemode !== undefined) {
110
127
  const strict = this.source.sandboxStrict?.() ?? true;
111
- parts.push({
112
- text: `codemode:${codemode}${strict ? "" : ` ${theme.fg("error", "net!")}`}`,
113
- priority: 3,
114
- });
128
+ const label = theme.fg("dim", `codemode ${codemode}`);
129
+ right(strict ? label : `${label} ${theme.fg("error", "net!")}`, 3, true);
115
130
  }
116
- parts.push({ text: `preset:${this.source.preset()}`, priority: 10 });
131
+ const preset = this.source.preset();
132
+ if (preset !== "default")
133
+ right(`preset ${preset}`, 10);
117
134
  const host = this.source.hostStatus?.();
118
135
  if (host !== undefined && host.size > 0) {
119
136
  const values = [...host.values()].filter((v) => v.trim() !== "");
120
137
  if (values.length > 0)
121
- parts.push({ text: `[${values.join(" · ")}]`, priority: 11 });
138
+ right(`[${values.join(" · ")}]`, 11);
122
139
  }
123
140
  return parts;
124
141
  }
142
+ ctxText(percent, width) {
143
+ const theme = this.theme;
144
+ if (percent === undefined)
145
+ return theme.fg("dim", "ctx ?");
146
+ const color = levelColor(percent / 100);
147
+ if (width >= METER_WIDTH) {
148
+ const meter = new Meter(percent / 100, { label: theme.fg("dim", "ctx"), theme });
149
+ return meter.render(80)[0] ?? "";
150
+ }
151
+ return theme.fg("dim", "ctx ") + theme.fg(color, `${Math.round(percent)}%`);
152
+ }
125
153
  render(width) {
126
- const parts = this.parts();
127
- const sep = this.theme.fg("dim", " · ");
154
+ const theme = this.theme;
155
+ const sep = theme.fg("dim", " · ");
128
156
  const join = (list) => list.map((p) => p.text).join(sep);
129
- let kept = parts;
130
- while (kept.length > 1 && visibleWidth(join(kept)) > width) {
131
- const drop = kept.reduce((worst, p) => (p.priority > worst.priority ? p : worst));
157
+ let kept = this.parts(width);
158
+ const zones = () => [
159
+ join(kept.filter((p) => p.zone === "left")),
160
+ join(kept.filter((p) => p.zone === "right")),
161
+ ];
162
+ const fits = () => {
163
+ const [l, r] = zones();
164
+ return visibleWidth(l) + (r === "" ? 0 : 3 + visibleWidth(r)) <= width;
165
+ };
166
+ while (!fits()) {
167
+ const droppable = kept.filter((p) => p.priority !== undefined);
168
+ if (droppable.length === 0)
169
+ break;
170
+ const drop = droppable.reduce((worst, p) => (p.priority > worst.priority ? p : worst));
132
171
  kept = kept.filter((p) => p !== drop);
133
172
  }
134
- return [truncateToWidth(join(kept), width)];
173
+ const [l, r] = zones();
174
+ const gap = width - visibleWidth(l) - visibleWidth(r);
175
+ const line = r === "" ? l : gap >= MIN_GAP ? l + " ".repeat(gap) + r : l + sep + r;
176
+ return [truncateToWidth(line, width)];
135
177
  }
136
178
  invalidate() {
137
179
  this.stats = undefined;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 工具调用的文字摘要(终端界面视觉设计 v1 §3.5):标题行的参数摘要、`⎿` 后的一行结果摘要、
3
+ * 统一 diff 解析与输出清洗。纯函数,ToolView 负责排版与着色之外的状态。
4
+ *
5
+ * 结果摘要(完成后):
6
+ * - read `读取 N 行`(图片 `图片 mime`、空文件 `空文件`);edit `N 处修改 · +a −b`;write `新建 / 覆盖 · N 行`;
7
+ * - bash `退出 c · 耗时 · N 行`(超时 `超时 · 耗时`、中断 `已中断 · 耗时`);
8
+ * - grep `N 处匹配 · M 个文件`;glob `N 个文件`;codemode `N 个内层调用 · 脚本输出 M 行`;
9
+ * - task `完成 · 耗时 · ↑in ↓out`;其它 `N 行输出`;失败(非 bash)取错误首行。
10
+ */
11
+ import type { ToolResult } from "../../tools/types.js";
12
+ import { type Theme } from "../../tui.js";
13
+ /** 工具输出 → 可安全显示的行。 */
14
+ export declare function cleanLines(text: string): string[];
15
+ export declare function flat(text: string, max?: number): string;
16
+ export declare function displayPath(path: string, cwd: string | undefined): string;
17
+ /** 标题行里的参数摘要。 */
18
+ export declare function toolSummary(name: string, args: unknown, cwd?: string): string;
19
+ /** 完成耗时:< 10s 一位小数(`2.1s`),< 60s 整秒,再长 `1m05s`。 */
20
+ export declare function formatDuration(ms: number): string;
21
+ export interface DiffRow {
22
+ kind: "hunk" | "add" | "del" | "ctx";
23
+ /** 原行(含 `+` / `-` / ` ` 前缀)。 */
24
+ text: string;
25
+ /** 行号:删除行取旧号,其余取新号;hunk 行没有。 */
26
+ num?: number;
27
+ }
28
+ /** 统一 diff → 行(去掉 `---` / `+++` 文件头),带行号。 */
29
+ export declare function parseDiff(diff: string): DiffRow[];
30
+ export declare function diffOf(result: ToolResult | undefined): string | undefined;
31
+ /** bash 完成后的正文:优先 `details.output`(不含退出码等附注)。 */
32
+ export declare function bashOutput(result: ToolResult): string | undefined;
33
+ export interface SummaryInput {
34
+ name: string;
35
+ args: unknown;
36
+ result: ToolResult;
37
+ isError: boolean;
38
+ /** 结果正文(已清洗)。 */
39
+ lines: readonly string[];
40
+ /** 完成耗时(ms);重放的历史调用没有,bash 取 `details.wall_time_seconds`。 */
41
+ elapsedMs: number | undefined;
42
+ /** 已显示的内层调用数(codemode 没有 details 时用)。 */
43
+ nestedCount?: number;
44
+ }
45
+ /** 完成后的一行结果摘要(已着色,不含 `⎿`)。 */
46
+ export declare function resultSummary(input: SummaryInput, theme: Theme): string;