@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,114 @@
1
+ /**
2
+ * 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
3
+ *
4
+ * - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
5
+ * - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
6
+ * - 不解码像素,只读文件头取尺寸。
7
+ */
8
+ import { readFile, stat } from "node:fs/promises";
9
+ import { extname } from "node:path";
10
+ import { AmaError } from "../errors.js";
11
+ export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
12
+ export const IMAGE_EXTENSIONS = {
13
+ ".png": "image/png",
14
+ ".jpg": "image/jpeg",
15
+ ".jpeg": "image/jpeg",
16
+ ".gif": "image/gif",
17
+ ".webp": "image/webp",
18
+ };
19
+ /** 按扩展名(不分大小写);不是图片返回 undefined。 */
20
+ export function imageMimeFromPath(path) {
21
+ return IMAGE_EXTENSIONS[extname(path).toLowerCase()];
22
+ }
23
+ /** 按文件头识别;认不出返回 undefined。 */
24
+ export function sniffImageMime(buf) {
25
+ if (buf.length >= 8 && buf.readUInt32BE(0) === 0x89504e47 && buf.readUInt32BE(4) === 0x0d0a1a0a)
26
+ return "image/png";
27
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
28
+ return "image/jpeg";
29
+ if (buf.length >= 6 && /^GIF8[79]a$/.test(buf.toString("ascii", 0, 6)))
30
+ return "image/gif";
31
+ if (buf.length >= 12 &&
32
+ buf.toString("ascii", 0, 4) === "RIFF" &&
33
+ buf.toString("ascii", 8, 12) === "WEBP")
34
+ return "image/webp";
35
+ return undefined;
36
+ }
37
+ /** 只读文件头取尺寸;不认识返回 undefined。 */
38
+ export function imageSize(buf, mimeType) {
39
+ try {
40
+ if (mimeType === "image/png" && buf.length >= 24) {
41
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
42
+ }
43
+ if (mimeType === "image/gif" && buf.length >= 10) {
44
+ return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
45
+ }
46
+ if (mimeType === "image/jpeg")
47
+ return jpegSize(buf);
48
+ if (mimeType === "image/webp")
49
+ return webpSize(buf);
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ return undefined;
55
+ }
56
+ function jpegSize(buf) {
57
+ let i = 2;
58
+ while (i + 9 < buf.length) {
59
+ if (buf[i] !== 0xff) {
60
+ i++;
61
+ continue;
62
+ }
63
+ const marker = buf[i + 1] ?? 0;
64
+ const isSof = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
65
+ if (isSof)
66
+ return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
67
+ i += 2 + buf.readUInt16BE(i + 2);
68
+ }
69
+ return undefined;
70
+ }
71
+ function webpSize(buf) {
72
+ if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF")
73
+ return undefined;
74
+ const chunk = buf.toString("ascii", 12, 16);
75
+ if (chunk === "VP8 ") {
76
+ return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
77
+ }
78
+ if (chunk === "VP8L") {
79
+ const b = buf.readUInt32LE(21);
80
+ return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
81
+ }
82
+ if (chunk === "VP8X") {
83
+ return { width: buf.readUIntLE(24, 3) + 1, height: buf.readUIntLE(27, 3) + 1 };
84
+ }
85
+ return undefined;
86
+ }
87
+ /**
88
+ * 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
89
+ */
90
+ export async function loadImageFile(path) {
91
+ let info;
92
+ try {
93
+ info = await stat(path);
94
+ }
95
+ catch {
96
+ throw new AmaError("invalid_arguments", `图片不存在:${path}`);
97
+ }
98
+ if (!info.isFile())
99
+ throw new AmaError("invalid_arguments", `不是文件:${path}`);
100
+ if (info.size > MAX_IMAGE_BYTES)
101
+ throw new AmaError("invalid_arguments", `图片超过 ${MAX_IMAGE_BYTES / 1024 / 1024} MB 上限:${path}(${(info.size / 1024 / 1024).toFixed(1)} MB)`);
102
+ const buf = await readFile(path);
103
+ const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(path);
104
+ if (mimeType === undefined)
105
+ throw new AmaError("invalid_arguments", `不是支持的图片(PNG / JPEG / GIF / WebP):${path}`);
106
+ const size = imageSize(buf, mimeType);
107
+ return {
108
+ block: { type: "image", data: buf.toString("base64"), mimeType },
109
+ path,
110
+ mimeType,
111
+ bytes: buf.length,
112
+ ...(size ? { size } : {}),
113
+ };
114
+ }
@@ -5,27 +5,52 @@
5
5
  * | ------------- | ----------------------------------------- |
6
6
  * | `default` | read、edit、write、bash、grep、glob |
7
7
  * | `minimal` | read、edit、write、bash |
8
- * | `codemode` | 只有 codemode(其余工具在脚本里调用) |
8
+ * | `codemode-only` | 只有 codemode(其余工具在脚本里调用) |
9
9
  * | `coordinator` | read + 宿主注册的工具 |
10
10
  *
11
11
  * - 预设只管**内置**工具;宿主 / SDK 注册的工具(`canvas_*` 等)总在活动集里,除非用户
12
12
  * `--tools a,b,c` 整组替换(之后只用列出的名字)。
13
13
  * - `tools.default`:`+name` / `-name` 在预设上增减;不带前缀的名字整组替换预设的内置工具,
14
14
  * 之后再应用带前缀的项。未注册的名字记 warning 并忽略。
15
- * - codemode 开关跟随预设:`codemode` 预设 → `only`,其余 → `off`;`codemode.mode` 显式配置覆盖。
16
- * - codemode 工具由 B10 经组装根的 `toolFactories` 注册(`codemode.mode` 为 off 时不注册);不可用
17
- * (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode 预设**回退到 default 并
18
- * warning**(不报错:零配置用户不应因此起不来),`on` 同样忽略。
15
+ * - `codemode` 是 `codemode-only` 的旧名(0.3.0),配置合并与命令行解析时折成规范名。
16
+ * - codemode 开关跟随预设(`presetCodemodeMode`):`default` → `on`(只在沙箱 strict,即 Node ≥ 25 隔离
17
+ * 网络时;Node 22 / 24 → `off`),`codemode-only` → `only`,`minimal` / `coordinator` → `off`。
18
+ * `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
19
+ * - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
20
+ * `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
21
+ * - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
22
+ * (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
23
+ * warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
19
24
  * - `only` 模式活动集独占:宿主 / SDK 工具也不直接暴露,只能在脚本里调用(`exclusive`)。
20
25
  */
21
- import type { AmaConfig, CodemodeMode, ToolsPreset } from "../config/types.js";
26
+ import { type AmaConfig, type CodemodeMode, type ToolsPreset } from "../config/types.js";
22
27
  import { ToolRegistry } from "./registry.js";
23
28
  import type { ToolDefinition, ToolSource } from "./types.js";
24
29
  export declare const CODEMODE_TOOL = "codemode";
25
- /** 各预设下模型直接看到的内置工具(codemode 预设见 `resolvePreset`)。 */
30
+ /** 各预设下模型直接看到的内置工具(codemode-only 预设见 `resolvePreset`)。 */
26
31
  export declare const PRESET_TOOLS: Readonly<Record<ToolsPreset, readonly string[]>>;
32
+ /**
33
+ * 预设对应的 codemode 模式(没有显式 `codemode.mode` 时)。`default` 只在沙箱 strict(Node ≥ 25,
34
+ * 权限模型同时隔离网络)时开 `on`:Node 22 / 24 下 codemode 属 execute 类,每次都要审批,`-p` 下
35
+ * 直接被拒,所以缺省关闭(启动时提示一次,见 cli/codemode-notice.ts)。
36
+ */
37
+ export declare function presetCodemodeMode(preset: ToolsPreset, strict: boolean): CodemodeMode;
38
+ export interface CodemodeModeResolution {
39
+ mode: CodemodeMode;
40
+ /** `config`:显式 `codemode.mode`;`preset`:跟随预设。 */
41
+ source: "config" | "preset";
42
+ preset: ToolsPreset;
43
+ strict: boolean;
44
+ }
45
+ /** 生效的 codemode 模式及其来源;`strict` 缺省按运行时探测(测试与组装根可注入)。 */
46
+ export declare function resolveCodemodeMode(config: Pick<AmaConfig, "tools" | "codemode">, strict?: boolean): CodemodeModeResolution;
27
47
  /** 生效的 codemode 模式:显式配置优先,否则跟随预设。 */
28
- export declare function effectiveCodemodeMode(config: Pick<AmaConfig, "tools" | "codemode">): CodemodeMode;
48
+ export declare function effectiveCodemodeMode(config: Pick<AmaConfig, "tools" | "codemode">, strict?: boolean): CodemodeMode;
49
+ /**
50
+ * codemode 脚本里可调用工具的范围:`coordinator` 预设只能调活动集里的工具(`"active"`),其它预设
51
+ * 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
52
+ */
53
+ export declare function codemodeCallableFilter(config: Pick<AmaConfig, "tools">): "active" | undefined;
29
54
  /**
30
55
  * `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
31
56
  * 返回去重后的名字(保持首次出现顺序)。
@@ -43,6 +68,8 @@ export declare function resolvePreset(input: {
43
68
  config: Pick<AmaConfig, "tools" | "codemode">;
44
69
  /** 该名字是否已注册(且未禁用)。 */
45
70
  available(name: string): boolean;
71
+ /** 沙箱是否 strict;缺省按运行时探测。 */
72
+ strict?: boolean;
46
73
  }): PresetResolution;
47
74
  type RegisterListener = (tool: ToolDefinition, source: ToolSource) => void;
48
75
  /**
@@ -5,34 +5,68 @@
5
5
  * | ------------- | ----------------------------------------- |
6
6
  * | `default` | read、edit、write、bash、grep、glob |
7
7
  * | `minimal` | read、edit、write、bash |
8
- * | `codemode` | 只有 codemode(其余工具在脚本里调用) |
8
+ * | `codemode-only` | 只有 codemode(其余工具在脚本里调用) |
9
9
  * | `coordinator` | read + 宿主注册的工具 |
10
10
  *
11
11
  * - 预设只管**内置**工具;宿主 / SDK 注册的工具(`canvas_*` 等)总在活动集里,除非用户
12
12
  * `--tools a,b,c` 整组替换(之后只用列出的名字)。
13
13
  * - `tools.default`:`+name` / `-name` 在预设上增减;不带前缀的名字整组替换预设的内置工具,
14
14
  * 之后再应用带前缀的项。未注册的名字记 warning 并忽略。
15
- * - codemode 开关跟随预设:`codemode` 预设 → `only`,其余 → `off`;`codemode.mode` 显式配置覆盖。
16
- * - codemode 工具由 B10 经组装根的 `toolFactories` 注册(`codemode.mode` 为 off 时不注册);不可用
17
- * (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode 预设**回退到 default 并
18
- * warning**(不报错:零配置用户不应因此起不来),`on` 同样忽略。
15
+ * - `codemode` 是 `codemode-only` 的旧名(0.3.0),配置合并与命令行解析时折成规范名。
16
+ * - codemode 开关跟随预设(`presetCodemodeMode`):`default` → `on`(只在沙箱 strict,即 Node ≥ 25 隔离
17
+ * 网络时;Node 22 / 24 → `off`),`codemode-only` → `only`,`minimal` / `coordinator` → `off`。
18
+ * `codemode.mode` 显式配置覆盖(缺省配置不写这个键,以后调整映射能惠及老用户)。
19
+ * - `coordinator` 预设即使显式 `on`,脚本里能调用的工具也只限活动集(read + 宿主工具),见
20
+ * `codemodeCallableFilter`:协调者「不写文件、不跑 bash」的约定不能经 codemode 绕过。
21
+ * - codemode 工具由 B10 经组装根的 `toolFactories` 注册(生效模式为 off 时不注册);不可用
22
+ * (例如 `codemode.requireStrict` 而运行时 Node 不隔离网络)时 codemode-only 预设**回退到 default 并
23
+ * warning**(不报错:零配置用户不应因此起不来),显式 `on` 同样忽略;跟随预设得到的 `on` 静默回退。
19
24
  * - `only` 模式活动集独占:宿主 / SDK 工具也不直接暴露,只能在脚本里调用(`exclusive`)。
20
25
  */
26
+ import { detectSandboxCapability } from "../codemode/capability.js";
27
+ import { canonicalPreset, } from "../config/types.js";
21
28
  import { ToolRegistry } from "./registry.js";
22
29
  export const CODEMODE_TOOL = "codemode";
23
- /** 各预设下模型直接看到的内置工具(codemode 预设见 `resolvePreset`)。 */
30
+ /** 各预设下模型直接看到的内置工具(codemode-only 预设见 `resolvePreset`)。 */
24
31
  export const PRESET_TOOLS = Object.freeze({
25
32
  default: ["bash", "edit", "glob", "grep", "read", "write"],
26
33
  minimal: ["bash", "edit", "read", "write"],
27
- codemode: [CODEMODE_TOOL],
34
+ "codemode-only": [CODEMODE_TOOL],
28
35
  coordinator: ["read"],
29
36
  });
30
- /** 生效的 codemode 模式:显式配置优先,否则跟随预设。 */
31
- export function effectiveCodemodeMode(config) {
37
+ /**
38
+ * 预设对应的 codemode 模式(没有显式 `codemode.mode` 时)。`default` 只在沙箱 strict(Node ≥ 25,
39
+ * 权限模型同时隔离网络)时开 `on`:Node 22 / 24 下 codemode 属 execute 类,每次都要审批,`-p` 下
40
+ * 直接被拒,所以缺省关闭(启动时提示一次,见 cli/codemode-notice.ts)。
41
+ */
42
+ export function presetCodemodeMode(preset, strict) {
43
+ switch (preset) {
44
+ case "codemode-only":
45
+ return "only";
46
+ case "default":
47
+ return strict ? "on" : "off";
48
+ default:
49
+ return "off";
50
+ }
51
+ }
52
+ /** 生效的 codemode 模式及其来源;`strict` 缺省按运行时探测(测试与组装根可注入)。 */
53
+ export function resolveCodemodeMode(config, strict = detectSandboxCapability().strict) {
54
+ const preset = canonicalPreset(config.tools?.preset) ?? "default";
32
55
  const explicit = config.codemode?.mode;
33
56
  if (explicit !== undefined)
34
- return explicit;
35
- return config.tools?.preset === "codemode" ? "only" : "off";
57
+ return { mode: explicit, source: "config", preset, strict };
58
+ return { mode: presetCodemodeMode(preset, strict), source: "preset", preset, strict };
59
+ }
60
+ /** 生效的 codemode 模式:显式配置优先,否则跟随预设。 */
61
+ export function effectiveCodemodeMode(config, strict) {
62
+ return resolveCodemodeMode(config, strict).mode;
63
+ }
64
+ /**
65
+ * codemode 脚本里可调用工具的范围:`coordinator` 预设只能调活动集里的工具(`"active"`),其它预设
66
+ * 不限(`undefined`;`on` 模式下脚本还能调没直接暴露的 ls / task / todo 等)。
67
+ */
68
+ export function codemodeCallableFilter(config) {
69
+ return canonicalPreset(config.tools?.preset) === "coordinator" ? "active" : undefined;
36
70
  }
37
71
  /**
38
72
  * `tools.default` 微调:不带前缀的名字整组替换 `base`,再依次应用 `+name` / `-name`。
@@ -55,13 +89,18 @@ export function applyToolAdjustments(base, adjustments) {
55
89
  }
56
90
  export function resolvePreset(input) {
57
91
  const warnings = [];
58
- let preset = input.config.tools?.preset ?? "default";
59
- let codemode = effectiveCodemodeMode(input.config);
92
+ const resolved = resolveCodemodeMode(input.config, input.strict);
93
+ let preset = resolved.preset;
94
+ let codemode = resolved.mode;
95
+ // 跟随预设得到的 on(default 预设)在 codemode 不可用时静默回退:零配置不该因此出 warning。
96
+ if (codemode === "on" && resolved.source === "preset" && !input.available(CODEMODE_TOOL)) {
97
+ codemode = "off";
98
+ }
60
99
  if (codemode !== "off" && !input.available(CODEMODE_TOOL)) {
61
- warnings.push(preset === "codemode"
62
- ? "工具预设 codemode 需要 codemode 工具(不可用),已回退到 default"
100
+ warnings.push(preset === "codemode-only"
101
+ ? "工具预设 codemode-only 需要 codemode 工具(不可用),已回退到 default"
63
102
  : `codemode.mode ${codemode} 需要 codemode 工具(不可用),已忽略`);
64
- if (preset === "codemode")
103
+ if (preset === "codemode-only")
65
104
  preset = "default";
66
105
  codemode = "off";
67
106
  }
@@ -69,7 +108,7 @@ export function resolvePreset(input) {
69
108
  if (codemode === "only")
70
109
  base = [CODEMODE_TOOL];
71
110
  else {
72
- base = [...PRESET_TOOLS[preset === "codemode" ? "default" : preset]];
111
+ base = [...PRESET_TOOLS[preset === "codemode-only" ? "default" : preset]];
73
112
  if (codemode === "on")
74
113
  base.push(CODEMODE_TOOL);
75
114
  }
@@ -2,10 +2,12 @@
2
2
  * `read` 工具(设计 §5.2)。[B3]
3
3
  *
4
4
  * 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB 先到者,末尾提示
5
- * `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(模型不支持
6
- * 图片时只给路径与尺寸);成功后 `ctx.markRead(abs)`。
5
+ * `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
6
+ * 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片或超过 5 MB 时只给路径与尺寸);成功后
7
+ * `ctx.markRead(abs)`。
7
8
  */
8
9
  import type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
10
+ export { imageSize, type ImageSize } from "./image-file.js";
9
11
  export interface ReadInput {
10
12
  path: string;
11
13
  offset?: number;
@@ -16,12 +18,6 @@ export interface ReadToolOptions {
16
18
  supportsImages?(ctx: ToolContext): boolean;
17
19
  }
18
20
  export declare function isBinary(buf: Buffer): boolean;
19
- export interface ImageSize {
20
- width: number;
21
- height: number;
22
- }
23
- /** 只读文件头取尺寸;不认识返回 undefined。 */
24
- export declare function imageSize(buf: Buffer, mimeType: string): ImageSize | undefined;
25
21
  /** `cat -n` 形状。 */
26
22
  export declare function numberLines(lines: readonly string[], firstLine: number): string;
27
23
  export declare function executeRead(input: ReadInput, ctx: ToolContext, options?: ReadToolOptions): Promise<ToolResult>;
@@ -2,21 +2,16 @@
2
2
  * `read` 工具(设计 §5.2)。[B3]
3
3
  *
4
4
  * 文本按 `cat -n` 形状返回(行号右对齐 6 位 + Tab);头截断 2000 行 / 50 KB 先到者,末尾提示
5
- * `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(模型不支持
6
- * 图片时只给路径与尺寸);成功后 `ctx.markRead(abs)`。
5
+ * `offset` 续读;NUL 嗅探判二进制并拒绝;png / jpg / gif / webp 作为 ImageBlock 返回(MIME 按文件头,
6
+ * 与 `--image` / `@图片` 共用 image-file.ts;模型不支持图片或超过 5 MB 时只给路径与尺寸);成功后
7
+ * `ctx.markRead(abs)`。
7
8
  */
8
9
  import { readFile, stat } from "node:fs/promises";
9
- import { extname } from "node:path";
10
10
  import { displayPath, resolvePath } from "./paths.js";
11
11
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
12
12
  import { normalizeToLF, splitBom } from "./edit-fuzzy.js";
13
- const IMAGE_MIME = {
14
- ".png": "image/png",
15
- ".jpg": "image/jpeg",
16
- ".jpeg": "image/jpeg",
17
- ".gif": "image/gif",
18
- ".webp": "image/webp",
19
- };
13
+ import { MAX_IMAGE_BYTES, imageMimeFromPath, imageSize, sniffImageMime } from "./image-file.js";
14
+ export { imageSize } from "./image-file.js";
20
15
  const SNIFF_BYTES = 8000;
21
16
  export function isBinary(buf) {
22
17
  const end = Math.min(buf.length, SNIFF_BYTES);
@@ -25,56 +20,6 @@ export function isBinary(buf) {
25
20
  return true;
26
21
  return false;
27
22
  }
28
- /** 只读文件头取尺寸;不认识返回 undefined。 */
29
- export function imageSize(buf, mimeType) {
30
- try {
31
- if (mimeType === "image/png" && buf.length >= 24) {
32
- return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
33
- }
34
- if (mimeType === "image/gif" && buf.length >= 10) {
35
- return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
36
- }
37
- if (mimeType === "image/jpeg")
38
- return jpegSize(buf);
39
- if (mimeType === "image/webp")
40
- return webpSize(buf);
41
- }
42
- catch {
43
- return undefined;
44
- }
45
- return undefined;
46
- }
47
- function jpegSize(buf) {
48
- let i = 2;
49
- while (i + 9 < buf.length) {
50
- if (buf[i] !== 0xff) {
51
- i++;
52
- continue;
53
- }
54
- const marker = buf[i + 1] ?? 0;
55
- const isSof = marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker);
56
- if (isSof)
57
- return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
58
- i += 2 + buf.readUInt16BE(i + 2);
59
- }
60
- return undefined;
61
- }
62
- function webpSize(buf) {
63
- if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF")
64
- return undefined;
65
- const chunk = buf.toString("ascii", 12, 16);
66
- if (chunk === "VP8 ") {
67
- return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
68
- }
69
- if (chunk === "VP8L") {
70
- const b = buf.readUInt32LE(21);
71
- return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
72
- }
73
- if (chunk === "VP8X") {
74
- return { width: buf.readUIntLE(24, 3) + 1, height: buf.readUIntLE(27, 3) + 1 };
75
- }
76
- return undefined;
77
- }
78
23
  function error(message) {
79
24
  return { content: message, isError: true };
80
25
  }
@@ -82,8 +27,9 @@ function error(message) {
82
27
  export function numberLines(lines, firstLine) {
83
28
  return lines.map((line, i) => `${String(firstLine + i).padStart(6)}\t${line}`).join("\n");
84
29
  }
85
- async function readImage(abs, shown, mimeType, ctx, options) {
30
+ async function readImage(abs, shown, ctx, options) {
86
31
  const buf = await readFile(abs);
32
+ const mimeType = sniffImageMime(buf) ?? imageMimeFromPath(abs) ?? "application/octet-stream";
87
33
  const size = imageSize(buf, mimeType);
88
34
  const dims = size ? `${size.width}x${size.height}, ` : "";
89
35
  const caption = `Image: ${shown} (${dims}${mimeType}, ${formatSize(buf.length)})`;
@@ -92,6 +38,12 @@ async function readImage(abs, shown, mimeType, ctx, options) {
92
38
  if (options.supportsImages && !options.supportsImages(ctx)) {
93
39
  return { content: `${caption}\n[The current model does not accept image input.]`, details };
94
40
  }
41
+ if (buf.length > MAX_IMAGE_BYTES) {
42
+ return {
43
+ content: `${caption}\n[Image exceeds the ${formatSize(MAX_IMAGE_BYTES)} attachment limit; not attached.]`,
44
+ details,
45
+ };
46
+ }
95
47
  const blocks = [
96
48
  { type: "text", text: caption },
97
49
  { type: "image", data: buf.toString("base64"), mimeType },
@@ -110,9 +62,8 @@ export async function executeRead(input, ctx, options = {}) {
110
62
  }
111
63
  if (info.isDirectory())
112
64
  return error(`${shown} is a directory; use the ls tool instead`);
113
- const mimeType = IMAGE_MIME[extname(abs).toLowerCase()];
114
- if (mimeType !== undefined)
115
- return readImage(abs, shown, mimeType, ctx, options);
65
+ if (imageMimeFromPath(abs) !== undefined)
66
+ return readImage(abs, shown, ctx, options);
116
67
  const buf = await readFile(abs);
117
68
  if (isBinary(buf))
118
69
  return error(`${shown} appears to be a binary file; refusing to read it`);
@@ -3,9 +3,13 @@
3
3
  *
4
4
  * 补全与偏差:
5
5
  * - 设计的 `Theme` 写作 `bg(...)` 与 `bold/dim/italic/underline(s)`,此处展开为完整签名。
6
- * - `SemanticColor` 取 §12.7 的 11 个语义色名。
6
+ * - `SemanticColor` 取 §12.7 的 11 个语义色名;终端界面视觉设计 v1 加 `muted`(正文与 dim 之间的次要文字)、
7
+ * `link`(链接文字)、`selection`(选中行底色,只在 ≥ 256 色时作 bg)共 14 个。
7
8
  * - 补全 `CURSOR_MARKER` 常量与 `isFocusable()` 判别函数(B4 的 tui.ts 与 B7 都要用)。
9
+ * - 终端界面视觉设计 v1:`Theme.glyphs`(字形表与 ASCII 回退,见 glyphs.ts)。
8
10
  */
11
+ import type { Glyphs } from "./glyphs.js";
12
+ export type { Glyphs, BoxGlyphs } from "./glyphs.js";
9
13
  /** APC 序列;获焦组件在光标处输出它,TUI 据此摆硬件光标并在输出前剔除。 */
10
14
  export declare const CURSOR_MARKER = "\u001B_ama:c\u0007";
11
15
  export interface Component {
@@ -20,7 +24,7 @@ export interface Focusable {
20
24
  focused: boolean;
21
25
  }
22
26
  export declare function isFocusable(component: Component): component is Component & Focusable;
23
- export type SemanticColor = "text" | "dim" | "accent" | "success" | "warning" | "error" | "user" | "assistant" | "tool" | "border" | "code";
27
+ export type SemanticColor = "text" | "dim" | "accent" | "success" | "warning" | "error" | "user" | "assistant" | "tool" | "border" | "code" | "muted" | "link" | "selection";
24
28
  /** 0 = 无色(NO_COLOR);16_777_216 = truecolor。 */
25
29
  export type ColorDepth = 0 | 16 | 256 | 16_777_216;
26
30
  export interface ThemeCapabilities {
@@ -35,4 +39,6 @@ export interface Theme {
35
39
  italic(text: string): string;
36
40
  underline(text: string): string;
37
41
  readonly caps: ThemeCapabilities;
42
+ /** 字形(Unicode 或 ASCII 回退);组件一律从这里取。 */
43
+ readonly glyphs: Glyphs;
38
44
  }
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * 补全与偏差:
5
5
  * - 设计的 `Theme` 写作 `bg(...)` 与 `bold/dim/italic/underline(s)`,此处展开为完整签名。
6
- * - `SemanticColor` 取 §12.7 的 11 个语义色名。
6
+ * - `SemanticColor` 取 §12.7 的 11 个语义色名;终端界面视觉设计 v1 加 `muted`(正文与 dim 之间的次要文字)、
7
+ * `link`(链接文字)、`selection`(选中行底色,只在 ≥ 256 色时作 bg)共 14 个。
7
8
  * - 补全 `CURSOR_MARKER` 常量与 `isFocusable()` 判别函数(B4 的 tui.ts 与 B7 都要用)。
9
+ * - 终端界面视觉设计 v1:`Theme.glyphs`(字形表与 ASCII 回退,见 glyphs.ts)。
8
10
  */
9
11
  /** APC 序列;获焦组件在光标处输出它,TUI 据此摆硬件光标并在输出前剔除。 */
10
12
  export const CURSOR_MARKER = "\x1b_ama:c\x07";
@@ -2,15 +2,19 @@
2
2
  * 边框 / 内边距容器(设计 §12.1)。[B4]
3
3
  *
4
4
  * 子组件按内宽渲染后加圆角边框(可选标题,嵌在上边框)与内边距;宽度不足 4 列时退化为只加内边距。
5
+ * 边框字形取 `theme.glyphs.box`(ASCII 下为 `+ - |`),颜色缺省 `border`,可用 `borderColor` 改
6
+ * (审批对话框按严重度用 error / warning)。
5
7
  * 焦点与键输入透传给子组件,所以可以直接把 `Box(SelectList)` 交给 `showOverlay`。
6
8
  */
7
- import { type Component, type Theme } from "../component.js";
9
+ import { type Component, type SemanticColor, type Theme } from "../component.js";
8
10
  export interface BoxOptions {
9
11
  border?: boolean;
10
12
  paddingX?: number;
11
13
  paddingY?: number;
12
14
  title?: string;
13
15
  theme?: Theme;
16
+ /** 边框颜色,缺省 `border`。 */
17
+ borderColor?: SemanticColor;
14
18
  }
15
19
  export declare class Box implements Component {
16
20
  readonly child: Component;
@@ -21,6 +25,7 @@ export declare class Box implements Component {
21
25
  set focused(value: boolean);
22
26
  handleInput(data: string): void;
23
27
  render(width: number): string[];
28
+ private get glyphs();
24
29
  private topBorder;
25
30
  invalidate(): void;
26
31
  }
@@ -2,10 +2,13 @@
2
2
  * 边框 / 内边距容器(设计 §12.1)。[B4]
3
3
  *
4
4
  * 子组件按内宽渲染后加圆角边框(可选标题,嵌在上边框)与内边距;宽度不足 4 列时退化为只加内边距。
5
+ * 边框字形取 `theme.glyphs.box`(ASCII 下为 `+ - |`),颜色缺省 `border`,可用 `borderColor` 改
6
+ * (审批对话框按严重度用 error / warning)。
5
7
  * 焦点与键输入透传给子组件,所以可以直接把 `Box(SelectList)` 交给 `showOverlay`。
6
8
  */
7
9
  import { isFocusable } from "../component.js";
8
10
  import { padToWidth, truncateToWidth, visibleWidth } from "../ansi.js";
11
+ import { UNICODE_GLYPHS } from "../glyphs.js";
9
12
  export class Box {
10
13
  child;
11
14
  options;
@@ -31,7 +34,9 @@ export class Box {
31
34
  const frame = border ? 2 : 0;
32
35
  const inner = Math.max(1, width - frame - padX * 2);
33
36
  const body = this.child.render(inner);
34
- const color = (s) => this.options.theme ? this.options.theme.fg("border", s) : s;
37
+ const borderColor = this.options.borderColor ?? "border";
38
+ const color = (s) => this.options.theme ? this.options.theme.fg(borderColor, s) : s;
39
+ const g = this.glyphs;
35
40
  const pad = " ".repeat(padX);
36
41
  const contentWidth = Math.max(0, width - frame);
37
42
  const rows = [];
@@ -47,17 +52,22 @@ export class Box {
47
52
  return rows;
48
53
  const lines = [this.topBorder(width, color)];
49
54
  for (const row of rows)
50
- lines.push(color("│") + row + color("│"));
51
- lines.push(color("╰" + "─".repeat(width - 2) + "╯"));
55
+ lines.push(color(g.vertical) + row + color(g.vertical));
56
+ lines.push(color(g.bottomLeft + g.horizontal.repeat(width - 2) + g.bottomRight));
52
57
  return lines;
53
58
  }
59
+ get glyphs() {
60
+ return (this.options.theme?.glyphs ?? UNICODE_GLYPHS).box;
61
+ }
54
62
  topBorder(width, color) {
63
+ const g = this.glyphs;
55
64
  const title = this.options.title;
56
- if (title === undefined || title === "")
57
- return color("╭" + "─".repeat(width - 2) + "╮");
65
+ if (title === undefined || title === "") {
66
+ return color(g.topLeft + g.horizontal.repeat(width - 2) + g.topRight);
67
+ }
58
68
  const label = truncateToWidth(` ${title} `, width - 4);
59
69
  const rest = Math.max(0, width - 3 - visibleWidth(label));
60
- return color("╭─") + label + color("─".repeat(rest) + "╮");
70
+ return color(g.topLeft + g.horizontal) + label + color(g.horizontal.repeat(rest) + g.topRight);
61
71
  }
62
72
  invalidate() {
63
73
  this.child.invalidate();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 左竖条卡片(终端界面视觉设计 v1 §3.6、§3.13):压缩摘要、`/session` 等面板。
3
+ *
4
+ * 每行前加 `theme.glyphs.card`(`▎`,ASCII `|`)与一个空格,竖条用 `border` 色;可选标题行(正文色粗体)
5
+ * 与跟在标题后的副标题(`muted`,两个空格隔开)。不加上下边框——Box 只留给代码块、启动头与覆盖层。
6
+ * 子组件按 `width - 2` 渲染;空行只画竖条。
7
+ */
8
+ import type { Component, SemanticColor, Theme } from "../component.js";
9
+ export interface CardOptions {
10
+ theme?: Theme;
11
+ title?: string;
12
+ /** 标题后的次要说明(muted)。 */
13
+ subtitle?: string;
14
+ /** 竖条颜色,缺省 `border`。 */
15
+ barColor?: SemanticColor;
16
+ }
17
+ export declare class Card implements Component {
18
+ readonly child: Component | undefined;
19
+ private readonly options;
20
+ constructor(child: Component | undefined, options?: CardOptions);
21
+ render(width: number): string[];
22
+ invalidate(): void;
23
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 左竖条卡片(终端界面视觉设计 v1 §3.6、§3.13):压缩摘要、`/session` 等面板。
3
+ *
4
+ * 每行前加 `theme.glyphs.card`(`▎`,ASCII `|`)与一个空格,竖条用 `border` 色;可选标题行(正文色粗体)
5
+ * 与跟在标题后的副标题(`muted`,两个空格隔开)。不加上下边框——Box 只留给代码块、启动头与覆盖层。
6
+ * 子组件按 `width - 2` 渲染;空行只画竖条。
7
+ */
8
+ import { truncateToWidth } from "../ansi.js";
9
+ import { UNICODE_GLYPHS } from "../glyphs.js";
10
+ export class Card {
11
+ child;
12
+ options;
13
+ constructor(child, options = {}) {
14
+ this.child = child;
15
+ this.options = options;
16
+ }
17
+ render(width) {
18
+ const { theme, title, subtitle } = this.options;
19
+ const glyph = (theme?.glyphs ?? UNICODE_GLYPHS).card;
20
+ const bar = theme ? theme.fg(this.options.barColor ?? "border", glyph) : glyph;
21
+ const inner = Math.max(1, width - 2);
22
+ const lines = [];
23
+ if (title !== undefined && title !== "") {
24
+ let head = theme ? theme.bold(title) : title;
25
+ if (subtitle !== undefined && subtitle !== "") {
26
+ head += " " + (theme ? theme.fg("muted", subtitle) : subtitle);
27
+ }
28
+ lines.push(head);
29
+ }
30
+ if (this.child)
31
+ lines.push(...this.child.render(inner));
32
+ return lines.map((line) => line === "" ? bar : truncateToWidth(`${bar} ${truncateToWidth(line, inner)}`, width));
33
+ }
34
+ invalidate() {
35
+ this.child?.invalidate();
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 编辑器的历史文件(设计 §12.4):JSONL,每行一个 JSON 字符串,保留最近 `limit` 条。[B4]
3
+ * 读写失败都不影响编辑(损坏的行跳过;写失败静默)。
4
+ */
5
+ export declare function loadHistoryFile(path: string, limit?: number): string[];
6
+ export declare function appendHistoryFile(path: string, entry: string, limit: number): void;