@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,45 @@
1
+ /**
2
+ * 编辑器的历史文件(设计 §12.4):JSONL,每行一个 JSON 字符串,保留最近 `limit` 条。[B4]
3
+ * 读写失败都不影响编辑(损坏的行跳过;写失败静默)。
4
+ */
5
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+ export function loadHistoryFile(path, limit = 500) {
8
+ let text;
9
+ try {
10
+ text = readFileSync(path, "utf8");
11
+ }
12
+ catch {
13
+ return [];
14
+ }
15
+ const entries = [];
16
+ for (const line of text.split("\n")) {
17
+ if (line.trim() === "")
18
+ continue;
19
+ try {
20
+ const value = JSON.parse(line);
21
+ if (typeof value === "string" && value !== "")
22
+ entries.push(value);
23
+ }
24
+ catch {
25
+ // 损坏的行跳过
26
+ }
27
+ }
28
+ return entries.slice(-limit);
29
+ }
30
+ export function appendHistoryFile(path, entry, limit) {
31
+ try {
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
34
+ const all = loadHistoryFile(path, Number.MAX_SAFE_INTEGER);
35
+ if (all.length > limit * 2) {
36
+ writeFileSync(path, all
37
+ .slice(-limit)
38
+ .map((e) => JSON.stringify(e) + "\n")
39
+ .join(""));
40
+ }
41
+ }
42
+ catch {
43
+ // 历史写失败不影响编辑
44
+ }
45
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[paste #N +M lines]`,
2
+ * 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[粘贴 #N · M 行]`,
3
3
  * 在编辑器里是不可分割段;提交时展开为原文。[B4]
4
4
  *
5
5
  * - 换行规范化:`\r\n` / 单独的 `\r` → `\n`(括号粘贴里终端常把换行发成 `\r`)。
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[paste #N +M lines]`,
2
+ * 大粘贴折叠(设计 §12.4):超过 10 行或超过 1 000 字符的粘贴折叠为 `[粘贴 #N · M 行]`,
3
3
  * 在编辑器里是不可分割段;提交时展开为原文。[B4]
4
4
  *
5
5
  * - 换行规范化:`\r\n` / 单独的 `\r` → `\n`(括号粘贴里终端常把换行发成 `\r`)。
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export const PASTE_LINE_THRESHOLD = 10;
10
10
  export const PASTE_CHAR_THRESHOLD = 1000;
11
- const MARKER_RE = /\[paste #(\d+) \+(\d+) lines\]/g;
11
+ const MARKER_RE = /\[粘贴 #(\d+) · (\d+) 行\]/g;
12
12
  export function normalizePastedText(text) {
13
13
  return text.replace(/\r\n?/g, "\n");
14
14
  }
@@ -20,7 +20,7 @@ export function shouldCollapse(text) {
20
20
  return countLines(text) > PASTE_LINE_THRESHOLD || text.length > PASTE_CHAR_THRESHOLD;
21
21
  }
22
22
  export function formatMarker(id, lines) {
23
- return `[paste #${id} +${lines} lines]`;
23
+ return `[粘贴 #${id} · ${lines} 行]`;
24
24
  }
25
25
  export class PasteStore {
26
26
  next = 1;
@@ -48,7 +48,7 @@ export class PasteStore {
48
48
  }
49
49
  /** 一行内已登记标记的区间(编辑器的不可分割段)。 */
50
50
  ranges(line) {
51
- if (this.entries.size === 0 || !line.includes("[paste #"))
51
+ if (this.entries.size === 0 || !line.includes("[粘贴 #"))
52
52
  return [];
53
53
  const out = [];
54
54
  for (const m of line.matchAll(MARKER_RE)) {
@@ -1,17 +1,21 @@
1
1
  /**
2
2
  * 多行编辑器组件(设计 §12.3、§12.4)。[B4]
3
3
  *
4
- * - 渲染:上下边框之间按列宽折行(不切开字素与折叠标记);超过 `maxVisibleLines` 时视窗跟随光标,
5
- * 边框上提示上 / 下方隐藏的行数;获焦时在光标处输出 CURSOR_MARKER 并反显光标格。
4
+ * - 渲染:上下规则线之间按列宽折行(不切开字素与折叠标记);首行前是提示符 `› `(`user` 粗体,
5
+ * `disableSubmit` 时 dim),续行缩进 2 列对齐;超过 `maxVisibleLines` 时视窗跟随光标,规则线上提示
6
+ * 上 / 下方隐藏的行数(dim);获焦时在光标处输出 CURSOR_MARKER 并反显光标格。空文本显示占位
7
+ * (获焦时也显示,光标反显占位首字)。
6
8
  * - 键位:`tui.editor.*`(见 keybindings.ts)。Enter 提交(`disableSubmit` 时忽略);Shift+Enter / Ctrl+J 换行;
7
9
  * Up/Down 在首 / 末视觉行时浏览历史(单行文本或空文本或已在浏览中),否则按视觉行移动。
8
10
  * - 括号粘贴(`ESC[200~…ESC[201~`,由 StdinBuffer 合成一次输入):> 10 行或 > 1 000 字符折叠为
9
- * `[paste #N +M lines]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
10
- * - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点。
11
+ * `[粘贴 #N · M 行]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
12
+ * - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点;
13
+ * 弹层缩进 2 列,尾行 `(i/n) Tab 接受 · Esc 关闭`。
11
14
  * - 历史:会话内 + 可选历史文件(JSONL,每行一个 JSON 字符串,保留最近 500 条)。
12
15
  */
13
16
  import { type Component, type Focusable, type Theme } from "../component.js";
14
17
  import { type Keybindings } from "../keybindings.js";
18
+ export { loadHistoryFile } from "./editor-history.js";
15
19
  export interface AutocompleteItem {
16
20
  /** 接受后替换进编辑器的文本。 */
17
21
  value: string;
@@ -52,7 +56,6 @@ export interface EditorOptions {
52
56
  onSubmit?(text: string): void;
53
57
  onChange?(text: string): void;
54
58
  }
55
- export declare function loadHistoryFile(path: string, limit?: number): string[];
56
59
  export declare class Editor implements Component, Focusable {
57
60
  private readonly options;
58
61
  focused: boolean;
@@ -103,7 +106,10 @@ export declare class Editor implements Component, Focusable {
103
106
  private applyCompletion;
104
107
  private acceptCompletion;
105
108
  private closeCompletion;
109
+ /** 文本列宽:去掉提示符与行尾光标格。 */
106
110
  private contentWidth;
111
+ private prompt;
112
+ private placeholderLine;
107
113
  /** 逻辑行 → 视觉行(按列宽折行,不切开字素与折叠标记)。 */
108
114
  private layout;
109
115
  private cursorRowIndex;
@@ -1,65 +1,32 @@
1
1
  /**
2
2
  * 多行编辑器组件(设计 §12.3、§12.4)。[B4]
3
3
  *
4
- * - 渲染:上下边框之间按列宽折行(不切开字素与折叠标记);超过 `maxVisibleLines` 时视窗跟随光标,
5
- * 边框上提示上 / 下方隐藏的行数;获焦时在光标处输出 CURSOR_MARKER 并反显光标格。
4
+ * - 渲染:上下规则线之间按列宽折行(不切开字素与折叠标记);首行前是提示符 `› `(`user` 粗体,
5
+ * `disableSubmit` 时 dim),续行缩进 2 列对齐;超过 `maxVisibleLines` 时视窗跟随光标,规则线上提示
6
+ * 上 / 下方隐藏的行数(dim);获焦时在光标处输出 CURSOR_MARKER 并反显光标格。空文本显示占位
7
+ * (获焦时也显示,光标反显占位首字)。
6
8
  * - 键位:`tui.editor.*`(见 keybindings.ts)。Enter 提交(`disableSubmit` 时忽略);Shift+Enter / Ctrl+J 换行;
7
9
  * Up/Down 在首 / 末视觉行时浏览历史(单行文本或空文本或已在浏览中),否则按视觉行移动。
8
10
  * - 括号粘贴(`ESC[200~…ESC[201~`,由 StdinBuffer 合成一次输入):> 10 行或 > 1 000 字符折叠为
9
- * `[paste #N +M lines]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
10
- * - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点。
11
+ * `[粘贴 #N · M 行]`,提交时展开;粘贴期间不触发补全;粘贴后紧随的 `\r` 是普通 Enter(提交)。
12
+ * - 补全:提供者由外部注入(`/` 命令、`@` 文件等由 B7 实现);同步或异步返回候选与替换起点;
13
+ * 弹层缩进 2 列,尾行 `(i/n) Tab 接受 · Esc 关闭`。
11
14
  * - 历史:会话内 + 可选历史文件(JSONL,每行一个 JSON 字符串,保留最近 500 条)。
12
15
  */
13
- import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
- import { dirname } from "node:path";
15
16
  import { CURSOR_MARKER } from "../component.js";
16
17
  import { truncateToWidth, visibleWidth } from "../ansi.js";
17
18
  import { defaultKeybindings } from "../keybindings.js";
18
19
  import { isPasteData, isPrintableText, unwrapPaste } from "../keys.js";
19
20
  import { plainTheme } from "../theme.js";
20
21
  import { EditorBuffer } from "./editor-buffer.js";
22
+ import { appendHistoryFile, loadHistoryFile } from "./editor-history.js";
21
23
  import { PasteStore } from "./editor-paste.js";
22
24
  import { SelectList } from "./select-list.js";
25
+ export { loadHistoryFile } from "./editor-history.js";
23
26
  const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
24
- export function loadHistoryFile(path, limit = 500) {
25
- let text;
26
- try {
27
- text = readFileSync(path, "utf8");
28
- }
29
- catch {
30
- return [];
31
- }
32
- const entries = [];
33
- for (const line of text.split("\n")) {
34
- if (line.trim() === "")
35
- continue;
36
- try {
37
- const value = JSON.parse(line);
38
- if (typeof value === "string" && value !== "")
39
- entries.push(value);
40
- }
41
- catch {
42
- // 损坏的行跳过
43
- }
44
- }
45
- return entries.slice(-limit);
46
- }
47
- function appendHistoryFile(path, entry, limit) {
48
- try {
49
- mkdirSync(dirname(path), { recursive: true });
50
- appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
51
- const all = loadHistoryFile(path, Number.MAX_SAFE_INTEGER);
52
- if (all.length > limit * 2) {
53
- writeFileSync(path, all
54
- .slice(-limit)
55
- .map((e) => JSON.stringify(e) + "\n")
56
- .join(""));
57
- }
58
- }
59
- catch {
60
- // 历史写失败不影响编辑
61
- }
62
- }
27
+ /** 提示符 `› ` / 续行缩进的宽度。 */
28
+ const PROMPT_WIDTH = 2;
29
+ const COMPLETION_FOOTER = "Tab 接受 · Esc 关闭";
63
30
  export class Editor {
64
31
  options;
65
32
  focused = false;
@@ -380,7 +347,13 @@ export class Editor {
380
347
  ? { value: item.value, label: item.label }
381
348
  : { value: item.value, label: item.label, description: item.description });
382
349
  this.completion = {
383
- list: new SelectList(items, { maxVisible: 8, theme: this.theme, keybindings: this.keys }),
350
+ list: new SelectList(items, {
351
+ maxVisible: 8,
352
+ theme: this.theme,
353
+ keybindings: this.keys,
354
+ footer: COMPLETION_FOOTER,
355
+ showCount: true,
356
+ }),
384
357
  from: result.from,
385
358
  };
386
359
  }
@@ -401,8 +374,22 @@ export class Editor {
401
374
  this.completionToken++;
402
375
  }
403
376
  // ---- 渲染 -----------------------------------------------------------------
377
+ /** 文本列宽:去掉提示符与行尾光标格。 */
404
378
  contentWidth(width) {
405
- return Math.max(1, width - 1);
379
+ return Math.max(1, width - PROMPT_WIDTH - 1);
380
+ }
381
+ prompt() {
382
+ const glyph = this.theme.glyphs.prompt;
383
+ return this.theme.fg(this.disableSubmit ? "dim" : "user", this.theme.bold(glyph)) + " ";
384
+ }
385
+ placeholderLine(width) {
386
+ const text = this.options.placeholder ?? "";
387
+ if (!this.focused)
388
+ return truncateToWidth(this.prompt() + this.theme.fg("dim", text), width);
389
+ const first = nextGrapheme(text, 0);
390
+ const rest = text.slice(first.length);
391
+ const cursor = CURSOR_MARKER + `\x1b[7m${this.theme.fg("dim", first)}\x1b[27m`;
392
+ return truncateToWidth(this.prompt() + cursor + this.theme.fg("dim", rest), width);
406
393
  }
407
394
  /** 逻辑行 → 视觉行(按列宽折行,不切开字素与折叠标记)。 */
408
395
  layout(width) {
@@ -494,27 +481,34 @@ export class Editor {
494
481
  this.scrollTop = cursorRow - maxVisible + 1;
495
482
  this.scrollTop = Math.max(0, Math.min(this.scrollTop, rows.length - maxVisible));
496
483
  const end = Math.min(rows.length, this.scrollTop + maxVisible);
497
- const lines = [this.border(width, this.scrollTop > 0 ? `↑ ${this.scrollTop}` : "")];
498
- if (this.buffer.isEmpty() && this.options.placeholder && !this.focused) {
499
- lines.push(truncateToWidth(theme.fg("dim", this.options.placeholder), width));
484
+ const g = theme.glyphs;
485
+ const lines = [this.border(width, this.scrollTop > 0 ? `${g.arrowUp} ${this.scrollTop}` : "")];
486
+ if (this.buffer.isEmpty() && this.options.placeholder) {
487
+ lines.push(this.placeholderLine(width));
500
488
  }
501
489
  else {
490
+ const indent = " ".repeat(PROMPT_WIDTH);
502
491
  for (let i = this.scrollTop; i < end; i++) {
503
- lines.push(this.renderRow(rows[i], this.focused && i === cursorRow));
492
+ const prefix = i === 0 ? this.prompt() : indent;
493
+ lines.push(prefix + this.renderRow(rows[i], this.focused && i === cursorRow));
504
494
  }
505
495
  }
506
- lines.push(this.border(width, end < rows.length ? `↓ ${rows.length - end}` : ""));
496
+ const below = rows.length - end;
497
+ lines.push(this.border(width, below > 0 ? `${g.arrowDown} ${below}` : ""));
507
498
  if (this.completion) {
508
- for (const line of this.completion.list.render(width))
509
- lines.push(line);
499
+ for (const line of this.completion.list.render(Math.max(1, width - 2))) {
500
+ lines.push(" " + line);
501
+ }
510
502
  }
511
503
  return lines;
512
504
  }
513
505
  border(width, label) {
514
- if (label === "" || width < label.length + 6)
515
- return this.theme.fg("border", "─".repeat(width));
516
- const tail = "─".repeat(width - label.length - 5);
517
- return (this.theme.fg("border", "─── ") +
506
+ const rule = this.theme.glyphs.rule;
507
+ const labelWidth = visibleWidth(label);
508
+ if (label === "" || width < labelWidth + 6)
509
+ return this.theme.fg("border", rule.repeat(width));
510
+ const tail = rule.repeat(width - labelWidth - 5);
511
+ return (this.theme.fg("border", rule.repeat(3) + " ") +
518
512
  this.theme.fg("dim", label) +
519
513
  this.theme.fg("border", " " + tail));
520
514
  }
@@ -4,6 +4,7 @@
4
4
  * - 键列宽 = 最长键的可见宽度,但不超过总宽的 `maxKeyRatio`(缺省 0.4,至少 4 列),超出的键截断;
5
5
  * - 值在剩余宽度内截断(不换行,保留 ANSI 样式);值里的换行折成 ` ⏎ `;
6
6
  * - 宽度连键列都放不下时退化为只显示截断后的键;空行(`key` 为空串)原样输出空行,可作分组间隔。
7
+ * - `wrap: true`:值按剩余宽度折行(值里的换行也换行),续行对齐值列。
7
8
  */
8
9
  import type { Component, SemanticColor, Theme } from "../component.js";
9
10
  export interface KeyValueRow {
@@ -17,6 +18,8 @@ export interface KeyValueOptions {
17
18
  /** 两列之间的空白,缺省 2。 */
18
19
  gap?: number;
19
20
  maxKeyRatio?: number;
21
+ /** 值折行(续行对齐值列),缺省 false(截断)。 */
22
+ wrap?: boolean;
20
23
  }
21
24
  export declare class KeyValue implements Component {
22
25
  private rows;
@@ -4,8 +4,9 @@
4
4
  * - 键列宽 = 最长键的可见宽度,但不超过总宽的 `maxKeyRatio`(缺省 0.4,至少 4 列),超出的键截断;
5
5
  * - 值在剩余宽度内截断(不换行,保留 ANSI 样式);值里的换行折成 ` ⏎ `;
6
6
  * - 宽度连键列都放不下时退化为只显示截断后的键;空行(`key` 为空串)原样输出空行,可作分组间隔。
7
+ * - `wrap: true`:值按剩余宽度折行(值里的换行也换行),续行对齐值列。
7
8
  */
8
- import { padToWidth, truncateToWidth, visibleWidth } from "../ansi.js";
9
+ import { padToWidth, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../ansi.js";
9
10
  /** 键列至少保留的宽度(键更短时取键宽);再窄就只显示键。 */
10
11
  const MIN_KEY_WIDTH = 4;
11
12
  export class KeyValue {
@@ -29,15 +30,24 @@ export class KeyValue {
29
30
  const keyWidth = Math.min(longest, Math.max(share, MIN_KEY_WIDTH));
30
31
  const valueWidth = width - keyWidth - gap;
31
32
  const { theme, keyColor = "dim" } = this.options;
32
- const lines = this.rows.map(({ key, value }) => {
33
+ const wrap = this.options.wrap === true;
34
+ const lines = this.rows.flatMap(({ key, value }) => {
33
35
  if (key === "" && value === "")
34
- return "";
36
+ return [""];
35
37
  const shownKey = truncateToWidth(key, Math.max(keyWidth, 1));
36
38
  const styledKey = theme ? theme.fg(keyColor, shownKey) : shownKey;
37
39
  if (valueWidth < 1)
38
- return truncateToWidth(styledKey, width);
39
- const flat = value.replace(/\r?\n/g, " ⏎ ");
40
- return padToWidth(styledKey, keyWidth) + " ".repeat(gap) + truncateToWidth(flat, valueWidth);
40
+ return [truncateToWidth(styledKey, width)];
41
+ const head = padToWidth(styledKey, keyWidth) + " ".repeat(gap);
42
+ if (!wrap) {
43
+ const flat = value.replace(/\r?\n/g, " ⏎ ");
44
+ return [head + truncateToWidth(flat, valueWidth)];
45
+ }
46
+ const pieces = value
47
+ .split(/\r?\n/)
48
+ .flatMap((line) => (line === "" ? [""] : wrapTextWithAnsi(line, valueWidth)));
49
+ const indent = " ".repeat(keyWidth + gap);
50
+ return pieces.map((piece, i) => (i === 0 ? head : indent) + piece);
41
51
  });
42
52
  this.cache = { width, lines };
43
53
  return lines;
@@ -1,35 +1,65 @@
1
1
  /**
2
- * 旋转指示器(设计 §12.1、§12.6:运行中显示 Loader 与已用时)。[B4]
2
+ * 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
3
3
  *
4
- * 帧由定时器推进并调用 `requestRender`;`stop()` 后不再触发渲染。时钟可注入便于测试。
4
+ * 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
5
+ * - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
6
+ * `setMessage(text)` 是只换动词的旧接口。
7
+ * - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
8
+ * `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
9
+ * - `animation: false`:spinner 固定为 `glyphs.spinnerStatic`,定时器每秒一次,已用时变了才请求重绘。
10
+ * - `stop()` 后不再触发渲染。时钟可注入便于测试。
5
11
  */
6
12
  import type { Component, Theme } from "../component.js";
7
- export declare const LOADER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
+ /** Unicode 帧表(兼容旧导出;实际帧取 `theme.glyphs.spinner`)。 */
14
+ export declare const LOADER_FRAMES: readonly string[];
8
15
  export interface LoaderOptions {
16
+ /** 初始动词(旧名 message)。 */
9
17
  message?: string;
10
18
  theme?: Theme;
11
- /** 帧间隔(ms),缺省 80。 */
19
+ /** 帧间隔(ms),缺省 Unicode 80、ASCII 250;`animation: false` 时为 1000。 */
12
20
  intervalMs?: number;
13
21
  /** 显示已用时(`12s`、`1m05s`),缺省 true。 */
14
22
  showElapsed?: boolean;
23
+ /** false:spinner 静止,缺省 true。 */
24
+ animation?: boolean;
15
25
  now?: () => number;
16
26
  }
27
+ export interface LoaderVerbOptions {
28
+ /** 本动词是否显示已用时(缺省跟随 `showElapsed`)。 */
29
+ elapsed?: boolean;
30
+ }
17
31
  export declare function formatElapsed(ms: number): string;
18
32
  export declare class Loader implements Component {
19
33
  private readonly requestRender;
20
34
  private readonly options;
21
- private frame;
35
+ private frameIndex;
22
36
  private timer;
23
37
  private startedAt;
24
- private message;
38
+ private verb;
39
+ private extras;
40
+ private verbElapsed;
41
+ private lastElapsed;
42
+ private readonly listeners;
25
43
  private readonly now;
26
44
  constructor(requestRender: () => void, options?: LoaderOptions);
27
45
  get running(): boolean;
46
+ private get frames();
47
+ private get animated();
48
+ /** 当前帧字形(静止时为 `spinnerStatic`)。 */
49
+ get frame(): string;
50
+ /** 订阅换帧(返回取消函数)。 */
51
+ onFrame(listener: (frame: string) => void): () => void;
52
+ /** 换动词与附加项(附加项 dim,排在已用时之后)。 */
53
+ setVerb(verb: string, extras?: readonly string[], options?: LoaderVerbOptions): void;
54
+ /** 旧接口:只换动词。 */
28
55
  setMessage(message: string): void;
56
+ /** 已用时从现在重新算(换到另一段工作时)。 */
57
+ resetElapsed(): void;
29
58
  start(): void;
30
59
  stop(): void;
31
- /** 推进一帧(定时器调用;测试可直接调用)。 */
60
+ /** 推进一帧(定时器调用;测试可直接调用)。静止时只在已用时变化时重绘。 */
32
61
  tick(): void;
62
+ private elapsedText;
33
63
  render(width: number): string[];
34
64
  invalidate(): void;
35
65
  }
@@ -1,10 +1,18 @@
1
1
  /**
2
- * 旋转指示器(设计 §12.1、§12.6:运行中显示 Loader 与已用时)。[B4]
2
+ * 运行指示(设计 §12.1、§12.6;终端界面视觉设计 v1 §3.7)。[B4]
3
3
  *
4
- * 帧由定时器推进并调用 `requestRender`;`stop()` 后不再触发渲染。时钟可注入便于测试。
4
+ * 一行 `⠋ 动词 · 已用时 · 附加项…`:spinner `accent`、动词正文色、其余 `dim`,以 ` · ` 连接。
5
+ * - `setVerb(verb, extras, { elapsed })` 换动词(思考中 / 回复中 / 运行 bash / 等待确认……);
6
+ * `setMessage(text)` 是只换动词的旧接口。
7
+ * - 帧取 `theme.glyphs.spinner`(Unicode 10 帧 80 ms、ASCII 4 帧 250 ms);`frame` 暴露当前帧字形,
8
+ * `onFrame` 让别的组件(运行中的工具摘要行)与 Loader 同帧换字,保证一帧只多改一行。
9
+ * - `animation: false`:spinner 固定为 `glyphs.spinnerStatic`,定时器每秒一次,已用时变了才请求重绘。
10
+ * - `stop()` 后不再触发渲染。时钟可注入便于测试。
5
11
  */
6
12
  import { truncateToWidth } from "../ansi.js";
7
- export const LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
+ import { UNICODE_GLYPHS } from "../glyphs.js";
14
+ /** Unicode 帧表(兼容旧导出;实际帧取 `theme.glyphs.spinner`)。 */
15
+ export const LOADER_FRAMES = UNICODE_GLYPHS.spinner;
8
16
  export function formatElapsed(ms) {
9
17
  const total = Math.max(0, Math.floor(ms / 1000));
10
18
  if (total < 60)
@@ -16,31 +24,74 @@ export function formatElapsed(ms) {
16
24
  export class Loader {
17
25
  requestRender;
18
26
  options;
19
- frame = 0;
27
+ frameIndex = 0;
20
28
  timer = null;
21
29
  startedAt;
22
- message;
30
+ verb;
31
+ extras = [];
32
+ verbElapsed;
33
+ lastElapsed = "";
34
+ listeners = new Set();
23
35
  now;
24
36
  constructor(requestRender, options = {}) {
25
37
  this.requestRender = requestRender;
26
38
  this.options = options;
27
39
  this.now = options.now ?? Date.now;
28
- this.message = options.message ?? "";
40
+ this.verb = options.message ?? "";
29
41
  this.startedAt = this.now();
30
42
  }
31
43
  get running() {
32
44
  return this.timer !== null;
33
45
  }
34
- setMessage(message) {
35
- this.message = message;
46
+ get frames() {
47
+ return (this.options.theme?.glyphs ?? UNICODE_GLYPHS).spinner;
48
+ }
49
+ get animated() {
50
+ return this.options.animation !== false;
51
+ }
52
+ /** 当前帧字形(静止时为 `spinnerStatic`)。 */
53
+ get frame() {
54
+ const glyphs = this.options.theme?.glyphs ?? UNICODE_GLYPHS;
55
+ return this.animated
56
+ ? this.frames[this.frameIndex % this.frames.length]
57
+ : glyphs.spinnerStatic;
58
+ }
59
+ /** 订阅换帧(返回取消函数)。 */
60
+ onFrame(listener) {
61
+ this.listeners.add(listener);
62
+ return () => this.listeners.delete(listener);
63
+ }
64
+ /** 换动词与附加项(附加项 dim,排在已用时之后)。 */
65
+ setVerb(verb, extras = [], options = {}) {
66
+ const same = verb === this.verb &&
67
+ options.elapsed === this.verbElapsed &&
68
+ extras.length === this.extras.length &&
69
+ extras.every((e, i) => e === this.extras[i]);
70
+ if (same)
71
+ return;
72
+ this.verb = verb;
73
+ this.extras = [...extras];
74
+ this.verbElapsed = options.elapsed;
36
75
  this.requestRender();
37
76
  }
77
+ /** 旧接口:只换动词。 */
78
+ setMessage(message) {
79
+ this.setVerb(message);
80
+ }
81
+ /** 已用时从现在重新算(换到另一段工作时)。 */
82
+ resetElapsed() {
83
+ this.startedAt = this.now();
84
+ }
38
85
  start() {
39
86
  if (this.timer !== null)
40
87
  return;
41
88
  this.startedAt = this.now();
42
- this.frame = 0;
43
- this.timer = setInterval(() => this.tick(), this.options.intervalMs ?? 80);
89
+ this.frameIndex = 0;
90
+ const glyphs = this.options.theme?.glyphs ?? UNICODE_GLYPHS;
91
+ const interval = this.animated
92
+ ? (this.options.intervalMs ?? (glyphs.ascii ? 250 : 80))
93
+ : Math.max(this.options.intervalMs ?? 1000, 1000);
94
+ this.timer = setInterval(() => this.tick(), interval);
44
95
  this.timer.unref?.();
45
96
  this.requestRender();
46
97
  }
@@ -50,22 +101,34 @@ export class Loader {
50
101
  clearInterval(this.timer);
51
102
  this.timer = null;
52
103
  }
53
- /** 推进一帧(定时器调用;测试可直接调用)。 */
104
+ /** 推进一帧(定时器调用;测试可直接调用)。静止时只在已用时变化时重绘。 */
54
105
  tick() {
55
- this.frame = (this.frame + 1) % LOADER_FRAMES.length;
106
+ if (this.animated)
107
+ this.frameIndex = (this.frameIndex + 1) % this.frames.length;
108
+ else if (this.elapsedText() === this.lastElapsed)
109
+ return;
110
+ const frame = this.frame;
111
+ for (const listener of this.listeners)
112
+ listener(frame);
56
113
  this.requestRender();
57
114
  }
115
+ elapsedText() {
116
+ return formatElapsed(this.now() - this.startedAt);
117
+ }
58
118
  render(width) {
59
119
  const theme = this.options.theme;
60
- const spinner = LOADER_FRAMES[this.frame];
61
- const parts = [theme ? theme.fg("accent", spinner) : spinner];
62
- if (this.message !== "")
63
- parts.push(this.message);
64
- if (this.options.showElapsed !== false) {
65
- const elapsed = `(${formatElapsed(this.now() - this.startedAt)})`;
66
- parts.push(theme ? theme.fg("dim", elapsed) : elapsed);
67
- }
68
- return [truncateToWidth(parts.join(" "), width)];
120
+ const dim = (s) => (theme ? theme.fg("dim", s) : s);
121
+ const parts = [];
122
+ if (this.verb !== "")
123
+ parts.push(this.verb);
124
+ const elapsed = this.elapsedText();
125
+ this.lastElapsed = elapsed;
126
+ if (this.verbElapsed ?? this.options.showElapsed !== false)
127
+ parts.push(dim(elapsed));
128
+ for (const extra of this.extras)
129
+ parts.push(dim(extra));
130
+ const spinner = theme ? theme.fg("accent", this.frame) : this.frame;
131
+ return [truncateToWidth(`${spinner} ${parts.join(dim(" · "))}`, width)];
69
132
  }
70
133
  invalidate() { }
71
134
  }
@@ -4,7 +4,11 @@
4
4
  * 支持:ATX 标题、段落(软换行合并为空格,行尾两个空格或 `\` 为硬换行)、无序 / 有序列表(缩进嵌套)、
5
5
  * 围栏代码块(边框 + 语言标签;流式时未闭合的围栏按代码块渲染)、引用、分隔线、
6
6
  * 行内 `code` / **粗体** / *斜体* / ~~删除线~~ / [链接](url) / <自动链接> / 反斜杠转义。
7
- * 表格降级为等宽对齐文本(不换行、超宽截断);不做语法高亮(§12.9)。
7
+ * 表格降级为等宽对齐文本(列间两个空格、首行粗体其下一条规则线、不换行、超宽截断);不做语法高亮(§12.9)。
8
+ *
9
+ * 配色(终端界面视觉设计 v1 §3.3):h1 / h2 `accent` 粗体,h3+ 粗体;行内代码 `code`;链接文字 `link`
10
+ * 加下划线、URL 以 ` (url)` `dim` 追加;列表符号 `muted`;代码块全宽、边框 `border`、语言标签 `dim`、
11
+ * 正文 `text`;引用 `▎` + `muted` 斜体。字形(圆点、框线、竖条)取 `theme.glyphs`。
8
12
  *
9
13
  * 缓存:按块缓存渲染结果(键 = 宽度 + 块源文本),流式追加时只有末块重新渲染;解析也是增量的
10
14
  * (`Markdown.append` 只从尾部可能变化的块重新解析)。