micro-models-agent 2.11.0 → 2.12.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 (347) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/js/tabs.js +22 -3
  3. package/dist/cli/cache-line.js +1 -0
  4. package/dist/cli/command-suggest.js +5 -4
  5. package/dist/cli/commands.js +231 -122
  6. package/dist/cli/completer.js +132 -31
  7. package/dist/cli/index.js +1 -0
  8. package/dist/cli/json-payload.js +4 -4
  9. package/dist/cli/main.js +166 -61
  10. package/dist/cli/onboarding-commands.js +126 -0
  11. package/dist/cli/plugin-commands.js +2 -1
  12. package/dist/cli/relaunch.js +16 -13
  13. package/dist/cli/repl-commands-lsp.js +75 -0
  14. package/dist/cli/repl-commands-skills.js +105 -0
  15. package/dist/cli/repl-commands.js +589 -266
  16. package/dist/cli/repl.js +436 -177
  17. package/dist/cli/run-result.js +5 -6
  18. package/dist/cli/security-commands.js +37 -28
  19. package/dist/cli/setup-order.js +4 -55
  20. package/dist/cli/setup-prompt.js +3 -2
  21. package/dist/cli/setup.js +27 -107
  22. package/dist/cli/web-command.js +98 -0
  23. package/dist/config/budget.js +5 -5
  24. package/dist/config/config.js +103 -96
  25. package/dist/config/defaults.js +38 -8
  26. package/dist/config/domains.js +24 -27
  27. package/dist/config/env.js +46 -0
  28. package/dist/config/experts.js +2 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/paths.js +56 -0
  31. package/dist/config/redact.js +44 -0
  32. package/dist/config/registry-extra.js +334 -0
  33. package/dist/config/registry.js +584 -0
  34. package/dist/config/security.js +35 -36
  35. package/dist/config/timeouts.js +74 -0
  36. package/dist/config/utils.js +16 -15
  37. package/dist/core/agent/artifact-progress.js +59 -0
  38. package/dist/core/agent/artifact-resolve.js +79 -0
  39. package/dist/core/agent/audit-gate.js +7 -3
  40. package/dist/core/agent/compaction.js +2 -2
  41. package/dist/core/agent/constants.js +33 -4
  42. package/dist/core/agent/context-log.js +454 -0
  43. package/dist/core/agent/context-renderer.js +5 -0
  44. package/dist/core/agent/critic-gate.js +132 -0
  45. package/dist/core/agent/hallucination-gate.js +148 -38
  46. package/dist/core/agent/loop-state.js +15 -0
  47. package/dist/core/agent/prefix-monitor.js +1 -1
  48. package/dist/core/agent/reasoning-resolver.js +10 -1
  49. package/dist/core/agent/token-tracker.js +7 -4
  50. package/dist/core/agent/tool-batch.js +93 -18
  51. package/dist/core/agent/tool-images.js +18 -0
  52. package/dist/core/agent/tool-output.js +65 -11
  53. package/dist/core/agent/tool-pairing.js +60 -0
  54. package/dist/core/agent-moe.js +25 -24
  55. package/dist/core/agent.js +429 -108
  56. package/dist/core/bootstrap/agent-phase.js +66 -0
  57. package/dist/core/bootstrap/config-phase.js +86 -0
  58. package/dist/core/bootstrap/modules-phase.js +269 -0
  59. package/dist/core/bootstrap/provider-phase.js +183 -0
  60. package/dist/core/bootstrap/session-phase.js +204 -0
  61. package/dist/core/bootstrap/state.js +94 -0
  62. package/dist/core/bootstrap/workspace-phase.js +115 -0
  63. package/dist/core/bootstrap.js +92 -660
  64. package/dist/core/crash-handler.js +17 -7
  65. package/dist/core/environment.js +37 -12
  66. package/dist/core/host-plan.js +43 -0
  67. package/dist/core/host.js +23 -0
  68. package/dist/core/index.js +1 -0
  69. package/dist/core/prompt-builder.js +17 -1
  70. package/dist/core/prompt-overflow.js +42 -43
  71. package/dist/core/session-logger.js +122 -30
  72. package/dist/core/system-prompt.js +117 -0
  73. package/dist/core/version.js +6 -4
  74. package/dist/core/web-host.js +34 -0
  75. package/dist/core/workspace.js +19 -15
  76. package/dist/i18n/index.js +37 -3
  77. package/dist/index.js +7 -3
  78. package/dist/llm/image-utils.js +55 -64
  79. package/dist/llm/llm-errors.js +9 -0
  80. package/dist/llm/model-loader.js +10 -32
  81. package/dist/llm/models-dev.js +121 -0
  82. package/dist/llm/openai-compat.js +146 -73
  83. package/dist/llm/orchestrator.js +33 -11
  84. package/dist/llm/provider-budget.js +2 -2
  85. package/dist/llm/provider-context.js +233 -0
  86. package/dist/llm/provider.js +1 -1
  87. package/dist/llm/response.js +5 -0
  88. package/dist/llm/token-counter.js +27 -11
  89. package/dist/logger/app-logger.js +48 -22
  90. package/dist/logger/file-log.js +28 -19
  91. package/dist/logger/index.js +1 -0
  92. package/dist/main.js +273 -47
  93. package/dist/migration/backup.js +14 -3
  94. package/dist/migration/detect.js +12 -3
  95. package/dist/migration/index.js +1 -0
  96. package/dist/modules/artifacts/store.js +9 -8
  97. package/dist/modules/browser/actions.js +6 -0
  98. package/dist/modules/browser/bridge-client.js +14 -12
  99. package/dist/modules/browser/bridge-path.js +4 -4
  100. package/dist/modules/browser/console-buffer.js +37 -0
  101. package/dist/modules/browser/cookie-store.js +5 -0
  102. package/dist/modules/browser/driver-types.js +1 -0
  103. package/dist/modules/browser/driver.js +19 -11
  104. package/dist/modules/browser/index.js +3 -1
  105. package/dist/modules/browser/module.js +2 -1
  106. package/dist/modules/browser/session.js +10 -30
  107. package/dist/modules/browser/snapshot.js +19 -13
  108. package/dist/modules/browser/types.js +1 -0
  109. package/dist/modules/certification/cli.js +28 -20
  110. package/dist/modules/certification/fact-checker.js +9 -1
  111. package/dist/modules/certification/loader.js +9 -2
  112. package/dist/modules/certification/manifest.js +83 -21
  113. package/dist/modules/certification/runner.js +23 -13
  114. package/dist/modules/certification/scenarios.js +20 -20
  115. package/dist/modules/certification/syntax-scenarios.js +1 -2
  116. package/dist/modules/context/chunk-query.js +11 -9
  117. package/dist/modules/context/fact-extractor.js +37 -39
  118. package/dist/modules/context/index.js +1 -0
  119. package/dist/modules/context/manager.js +106 -80
  120. package/dist/modules/critic/client.js +176 -0
  121. package/dist/modules/critic/evidence.js +81 -0
  122. package/dist/modules/critic/provider.js +45 -0
  123. package/dist/modules/critic/tools.js +78 -0
  124. package/dist/modules/critic/types.js +1 -0
  125. package/dist/modules/execution/audit-runners.js +28 -34
  126. package/dist/modules/execution/auditor.js +53 -49
  127. package/dist/modules/execution/execution-plugin.js +142 -102
  128. package/dist/modules/execution/index.js +1 -1
  129. package/dist/modules/execution/input-from.js +37 -13
  130. package/dist/modules/execution/module.js +368 -191
  131. package/dist/modules/execution/moe-executor.js +53 -26
  132. package/dist/modules/execution/plan-actions.js +320 -202
  133. package/dist/modules/execution/plan-checks.js +35 -0
  134. package/dist/modules/execution/plan-coverage.js +36 -33
  135. package/dist/modules/execution/plan-steps.js +11 -3
  136. package/dist/modules/execution/plan-store.js +83 -17
  137. package/dist/modules/execution/plan-tool.js +89 -35
  138. package/dist/modules/execution/plan-validator.js +30 -10
  139. package/dist/modules/execution/planner.js +22 -13
  140. package/dist/modules/execution/stuck-detector.js +196 -93
  141. package/dist/modules/execution/tracker.js +22 -8
  142. package/dist/modules/execution/verifier.js +69 -48
  143. package/dist/modules/execution/windows-commands.js +12 -12
  144. package/dist/modules/hallucination/artifact-claim.js +93 -0
  145. package/dist/modules/hallucination/confidence.js +13 -11
  146. package/dist/modules/hallucination/consistency.js +10 -2
  147. package/dist/modules/hallucination/detector.js +10 -5
  148. package/dist/modules/hallucination/factual.js +43 -26
  149. package/dist/modules/hallucination/index.js +1 -0
  150. package/dist/modules/hallucination/js-identifiers.js +44 -45
  151. package/dist/modules/hallucination/llm-judge.js +17 -12
  152. package/dist/modules/hallucination/success-claim.js +59 -0
  153. package/dist/modules/indexer/cache.js +52 -4
  154. package/dist/modules/indexer/index.js +1 -0
  155. package/dist/modules/indexer/map-command.js +2 -4
  156. package/dist/modules/indexer/map-select.js +16 -16
  157. package/dist/modules/indexer/module.js +47 -12
  158. package/dist/modules/indexer/project-profile.js +8 -7
  159. package/dist/modules/indexer/symbols.js +15 -15
  160. package/dist/modules/indexer/walker.js +159 -29
  161. package/dist/modules/lsp/check-tool.js +7 -5
  162. package/dist/modules/lsp/client.js +39 -39
  163. package/dist/modules/lsp/command.js +17 -24
  164. package/dist/modules/lsp/config.js +12 -11
  165. package/dist/modules/lsp/index.js +1 -0
  166. package/dist/modules/lsp/module.js +48 -46
  167. package/dist/modules/lsp/probe.js +13 -14
  168. package/dist/modules/lsp/project-root.js +14 -17
  169. package/dist/modules/lsp/startup-check.js +18 -16
  170. package/dist/modules/mcp/client.js +59 -62
  171. package/dist/modules/mcp/index.js +1 -0
  172. package/dist/modules/mcp/module.js +66 -37
  173. package/dist/modules/mcp/registry.js +5 -0
  174. package/dist/modules/memory/index.js +1 -0
  175. package/dist/modules/memory/module.js +12 -8
  176. package/dist/modules/memory/search.js +4 -1
  177. package/dist/modules/memory/store.js +40 -2
  178. package/dist/modules/onboarding/init.js +47 -0
  179. package/dist/modules/onboarding/meet.js +30 -0
  180. package/dist/modules/onboarding/mode.js +30 -0
  181. package/dist/modules/onboarding/prompts.js +43 -0
  182. package/dist/modules/pipelines/engine.js +10 -0
  183. package/dist/modules/pipelines/index.js +1 -0
  184. package/dist/modules/pipelines/parser.js +4 -1
  185. package/dist/modules/pipelines/template.js +4 -0
  186. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  187. package/dist/modules/plugins/builtin/notify.js +2 -0
  188. package/dist/modules/plugins/index.js +1 -0
  189. package/dist/modules/plugins/loader.js +18 -3
  190. package/dist/modules/plugins/manager.js +37 -17
  191. package/dist/modules/pricing/index.js +10 -11
  192. package/dist/modules/pricing/prices.js +12 -12
  193. package/dist/modules/processes/index.js +1 -0
  194. package/dist/modules/processes/kill-tree.js +27 -31
  195. package/dist/modules/processes/registry.js +44 -25
  196. package/dist/modules/processes/runner.js +4 -4
  197. package/dist/modules/providers/capabilities.js +28 -0
  198. package/dist/modules/providers/create.js +3 -4
  199. package/dist/modules/providers/factory.js +7 -7
  200. package/dist/modules/providers/fallback.js +12 -12
  201. package/dist/modules/providers/health.js +4 -4
  202. package/dist/modules/providers/manager.js +50 -19
  203. package/dist/modules/providers/naming.js +15 -0
  204. package/dist/modules/providers/preflight.js +71 -0
  205. package/dist/modules/providers/presets.js +39 -8
  206. package/dist/modules/providers/registry.js +7 -2
  207. package/dist/modules/reasoning/policy.js +8 -6
  208. package/dist/modules/reasoning/probe.js +30 -25
  209. package/dist/modules/security/audit-log.js +19 -36
  210. package/dist/modules/security/audit-notifier.js +35 -75
  211. package/dist/modules/security/command-validator.js +99 -51
  212. package/dist/modules/security/content-scanner.js +8 -8
  213. package/dist/modules/security/data-sanitizer.js +9 -16
  214. package/dist/modules/security/encryption.js +32 -52
  215. package/dist/modules/security/index.js +2 -1
  216. package/dist/modules/security/network-validator.js +120 -43
  217. package/dist/modules/security/path-validator.js +61 -46
  218. package/dist/modules/security/protected-paths.js +73 -0
  219. package/dist/modules/security/rate-limiter.js +17 -49
  220. package/dist/modules/security/security-policies.js +25 -57
  221. package/dist/modules/security/session-encryption.js +32 -71
  222. package/dist/modules/security/session-isolation.js +15 -28
  223. package/dist/modules/session/auto-name.js +63 -0
  224. package/dist/modules/session/export.js +98 -0
  225. package/dist/modules/session/index.js +1 -0
  226. package/dist/modules/session/manager.js +79 -28
  227. package/dist/modules/session/module.js +4 -3
  228. package/dist/modules/session/store.js +57 -20
  229. package/dist/modules/setup/answers.js +35 -0
  230. package/dist/modules/setup/apply.js +79 -0
  231. package/dist/modules/setup/discovery.js +72 -0
  232. package/dist/modules/skills/index.js +1 -0
  233. package/dist/modules/skills/loader.js +9 -1
  234. package/dist/modules/skills/module.js +11 -0
  235. package/dist/modules/updater/changelog-reader.js +15 -16
  236. package/dist/modules/updater/checker.js +9 -4
  237. package/dist/modules/updater/dev-detect.js +7 -6
  238. package/dist/modules/updater/index.js +1 -0
  239. package/dist/modules/updater/module.js +17 -15
  240. package/dist/modules/user-profile/compressor.js +2 -0
  241. package/dist/modules/user-profile/index.js +1 -0
  242. package/dist/modules/user-profile/profile.js +10 -1
  243. package/dist/modules/webui/answer-api.js +29 -0
  244. package/dist/modules/webui/assets.js +50 -0
  245. package/dist/modules/webui/body.js +21 -0
  246. package/dist/modules/webui/commands-settings.js +209 -0
  247. package/dist/modules/webui/commands-shared.js +60 -0
  248. package/dist/modules/webui/commands.js +174 -0
  249. package/dist/modules/webui/config-api.js +134 -0
  250. package/dist/modules/webui/file-api.js +202 -0
  251. package/dist/modules/webui/forward.js +159 -0
  252. package/dist/modules/webui/history-api.js +112 -0
  253. package/dist/modules/webui/host-state.js +60 -0
  254. package/dist/modules/webui/i18n-api.js +9 -0
  255. package/dist/modules/webui/limits.js +6 -0
  256. package/dist/modules/webui/log-forward.js +20 -0
  257. package/dist/modules/webui/memory-commands.js +64 -0
  258. package/dist/modules/webui/model-cache.js +18 -0
  259. package/dist/modules/webui/module.js +148 -0
  260. package/dist/modules/webui/persist.js +24 -0
  261. package/dist/modules/webui/prompt-io.js +97 -0
  262. package/dist/modules/webui/providers-api.js +337 -0
  263. package/dist/modules/webui/providers-view.js +56 -0
  264. package/dist/modules/webui/reasoning-view.js +15 -0
  265. package/dist/modules/webui/run-agent.js +81 -0
  266. package/dist/modules/webui/server.js +284 -0
  267. package/dist/modules/webui/sessions-api.js +178 -0
  268. package/dist/modules/webui/settings-api.js +561 -0
  269. package/dist/modules/webui/setup-api.js +201 -0
  270. package/dist/modules/webui/state.js +74 -0
  271. package/dist/modules/webui/util.js +261 -0
  272. package/dist/output/bus.js +7 -2
  273. package/dist/output/channel.js +80 -28
  274. package/dist/output/format.js +2 -1
  275. package/dist/output/index.js +1 -0
  276. package/dist/output/json-sink.js +5 -3
  277. package/dist/output/machine.js +4 -3
  278. package/dist/output/session-sink.js +4 -4
  279. package/dist/tools/approve.js +4 -0
  280. package/dist/tools/attach-image.js +5 -4
  281. package/dist/tools/auto-fixer.js +26 -44
  282. package/dist/tools/auto-tags.js +83 -0
  283. package/dist/tools/bash.js +193 -116
  284. package/dist/tools/browser.js +76 -31
  285. package/dist/tools/chunk-query.js +14 -6
  286. package/dist/tools/create-dir.js +9 -5
  287. package/dist/tools/delete-file.js +9 -5
  288. package/dist/tools/download-file.js +32 -7
  289. package/dist/tools/edit-file.js +52 -14
  290. package/dist/tools/enable-tools.js +6 -6
  291. package/dist/tools/executor.js +132 -27
  292. package/dist/tools/file-info.js +3 -1
  293. package/dist/tools/filter-tools.js +3 -3
  294. package/dist/tools/glob-tool.js +4 -1
  295. package/dist/tools/grep-tool.js +13 -10
  296. package/dist/tools/hidden-tools-block.js +6 -6
  297. package/dist/tools/index.js +16 -8
  298. package/dist/tools/list-dir.js +4 -2
  299. package/dist/tools/load-skill.js +7 -0
  300. package/dist/tools/mcp-call.js +15 -4
  301. package/dist/tools/meet-done.js +14 -0
  302. package/dist/tools/move-file.js +14 -7
  303. package/dist/tools/path-utils.js +29 -29
  304. package/dist/tools/pipeline-run.js +29 -7
  305. package/dist/tools/preview.js +1 -1
  306. package/dist/tools/process-ids.js +31 -0
  307. package/dist/tools/process-kill.js +9 -16
  308. package/dist/tools/process-list.js +1 -0
  309. package/dist/tools/process-log.js +11 -16
  310. package/dist/tools/process-wait.js +63 -0
  311. package/dist/tools/prompt-io.js +4 -3
  312. package/dist/tools/question.js +172 -25
  313. package/dist/tools/read-file.js +55 -34
  314. package/dist/tools/recall.js +14 -8
  315. package/dist/tools/registry.js +6 -5
  316. package/dist/tools/remember.js +8 -4
  317. package/dist/tools/scope-request.js +11 -6
  318. package/dist/tools/search-history.js +5 -2
  319. package/dist/tools/session-info.js +2 -0
  320. package/dist/tools/set-thinking.js +8 -4
  321. package/dist/tools/subagent.js +59 -39
  322. package/dist/tools/syntax-validator.js +292 -90
  323. package/dist/tools/user-input.js +22 -18
  324. package/dist/tools/web-browse.js +9 -3
  325. package/dist/tools/web-fetch.js +12 -23
  326. package/dist/tools/web-search.js +7 -5
  327. package/dist/tools/write-file.js +45 -15
  328. package/dist/ui/action-log.js +13 -0
  329. package/dist/ui/box.js +4 -3
  330. package/dist/ui/colors.js +2 -0
  331. package/dist/ui/diff.js +6 -5
  332. package/dist/ui/index.js +1 -0
  333. package/dist/ui/line-editor.js +230 -122
  334. package/dist/ui/line-math.js +10 -2
  335. package/dist/ui/md-formatter.js +57 -37
  336. package/dist/ui/output.js +8 -3
  337. package/dist/ui/plan-view.js +21 -10
  338. package/dist/ui/renderer.js +393 -67
  339. package/dist/ui/spinner.js +16 -5
  340. package/dist/ui/table.js +15 -17
  341. package/dist/ui/theme.js +143 -0
  342. package/dist/utils/child-run.js +20 -0
  343. package/dist/utils/error.js +1 -1
  344. package/dist/utils/index.js +2 -0
  345. package/dist/utils/retry.js +2 -2
  346. package/dist/utils/truncate.js +1 -1
  347. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
+ // Рендерер чата: события агента в терминальные строки по STYLE.md.
1
2
  import { pc } from "./colors";
2
3
  import { FormattingStream } from "./md-formatter";
3
4
  import { Spinner } from "./spinner";
5
+ import { actionMarkFor, failureMark, glyph, successMark, formatDuration, toolAccent, } from "./theme";
6
+ import { recordLastAction } from "./action-log";
4
7
  import { getTerminalWidth } from "./table";
5
8
  import { t } from "../i18n/index";
6
9
  import { formatCost } from "../modules/pricing/prices";
@@ -9,7 +12,7 @@ import { isAbsolute, relative, sep } from "path";
9
12
  function formatUsd(cost) {
10
13
  return formatCost(cost);
11
14
  }
12
- /** Tools whose `path` argument is shown in the header. */
15
+ /** Тулы, чей аргумент `path` показывается в шапке. */
13
16
  const PATH_TOOLS = new Set([
14
17
  "read_file",
15
18
  "write_file",
@@ -24,41 +27,53 @@ function toDisplayPath(baseDir, p) {
24
27
  if (!baseDir || !isAbsolute(p))
25
28
  return p;
26
29
  const rel = relative(baseDir, p);
27
- // Outside baseDir (../ prefix) or another drive (absolute result) → keep as-is.
30
+ // Вне baseDir (префикс ../) или другой диск (абсолютный результат) — как есть.
28
31
  if (!rel || rel.startsWith("..") || isAbsolute(rel))
29
32
  return p;
30
33
  return rel.split(sep).join("/");
31
34
  }
32
35
  const GUTTER = " ";
33
36
  const MAX_OUTPUT_LINES = 50;
34
- /** Tools that must NOT get a busy spinner — they block on a user prompt. */
37
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
38
+ function stripAnsi(text) {
39
+ return text.replace(ANSI_RE, "");
40
+ }
41
+ /** Тулы без busy-спиннера: висят на промпте пользователя. */
35
42
  const SPINNERLESS_TOOLS = new Set(["question", "approve"]);
36
- /** opencode-like leading marker per tool category. */
37
- export function toolMarker(tool) {
38
- switch (tool) {
39
- case "write_file":
40
- case "edit_file":
41
- case "create_dir":
42
- case "move_file":
43
- case "delete_file":
44
- return "←";
45
- case "read_file":
46
- case "list_dir":
47
- case "file_info":
48
- return "→";
49
- case "glob":
50
- case "grep":
51
- return "✱";
52
- case "bash":
53
- return "$";
54
- default:
55
- return "⚙";
56
- }
43
+ /**
44
+ * Read-only тулы разведки. В quiet прогон таких сворачивается в одну строку
45
+ * `│ N actions ✓`; мутирующие/исполняющие всегда видимы.
46
+ */
47
+ const RECON_TOOLS = new Set([
48
+ "read_file",
49
+ "list_dir",
50
+ "glob",
51
+ "grep",
52
+ "file_info",
53
+ "project_map",
54
+ "chunk_query",
55
+ "web_search",
56
+ "web_fetch",
57
+ "web_browse",
58
+ "search_history",
59
+ "recall",
60
+ "session_info",
61
+ "process_list",
62
+ "process_log",
63
+ ]);
64
+ /**
65
+ * Ведущий маркер каждого действия. В стиле один маркер (`◇`) на все тулы:
66
+ * категории передаются локализованным ярлыком, а не глифом.
67
+ * Функция ради вызывающих с явным `icon`.
68
+ */
69
+ export function toolMarker(_tool) {
70
+ return glyph.action;
57
71
  }
72
+ /** Rich-терминал: TTY и не CI. */
58
73
  export function isRichTerminal() {
59
74
  return Boolean(process.stdout.isTTY) && !process.env.CI;
60
75
  }
61
- /** Short human-friendly summary of tool arguments. */
76
+ /** Короткая человекочитаемая сводка аргументов тула. */
62
77
  export function summarizeArgs(args) {
63
78
  const preferred = ["path", "file", "query", "url", "command", "name"];
64
79
  for (const key of preferred) {
@@ -72,15 +87,134 @@ export function summarizeArgs(args) {
72
87
  return "";
73
88
  return serialized.length > 60 ? `${serialized.slice(0, 57)}…` : serialized;
74
89
  }
90
+ /** Скалярные ключи, показываемые как есть, в порядке приоритета, для обычных тулов. */
91
+ const GENERIC_ARG_KEYS = [
92
+ "path",
93
+ "file",
94
+ "pattern",
95
+ "query",
96
+ "url",
97
+ "command",
98
+ "name",
99
+ "task",
100
+ "level",
101
+ "id",
102
+ "source",
103
+ "step",
104
+ "key",
105
+ "value",
106
+ "text",
107
+ ];
108
+ function str(value) {
109
+ if (typeof value === "string")
110
+ return value;
111
+ if (typeof value === "number")
112
+ return String(value);
113
+ return "";
114
+ }
115
+ function truncateArg(text, max = 60) {
116
+ return text.length > max ? `${text.slice(0, max - 3)}…` : text;
117
+ }
118
+ /**
119
+ * Человекочитаемая сводка аргументов. Структурные/экшен-тулы несут сырой JSON,
120
+ * который потёк бы в чат, — им короткая локализованная фраза вместо него.
121
+ */
122
+ export function humanArgs(tool, args) {
123
+ if (tool === "plan") {
124
+ const action = typeof args.action === "string" ? args.action : "";
125
+ if (action === "update") {
126
+ const step = args.step ?? args.stepId;
127
+ const status = args.status;
128
+ if (step !== undefined || status !== undefined) {
129
+ return t("ui.plan_update", {
130
+ step: String(step ?? "?"),
131
+ status: String(status ?? "?"),
132
+ });
133
+ }
134
+ }
135
+ if (action === "create" && typeof args.title === "string")
136
+ return args.title;
137
+ if (action)
138
+ return action;
139
+ }
140
+ if (tool === "plan_step") {
141
+ const step = args.step;
142
+ const status = args.status;
143
+ if (step !== undefined || status !== undefined) {
144
+ return t("ui.plan_update", {
145
+ step: String(step ?? "?"),
146
+ status: String(status ?? "?"),
147
+ });
148
+ }
149
+ }
150
+ if (tool === "todo") {
151
+ const action = typeof args.action === "string" ? args.action : "";
152
+ const items = Array.isArray(args.items) ? args.items.length : undefined;
153
+ if (action && items !== undefined)
154
+ return t("ui.todo_action", { action, count: String(items) });
155
+ if (action)
156
+ return action;
157
+ }
158
+ // Обычные экшен-тулы (browser, project_map, lsp_check, …) несут `action` плюс
159
+ // пару коротких скаляров — рендерим фразу, а не сырой JSON.
160
+ if (typeof args.action === "string" && args.action) {
161
+ const extras = ["direction", "selector", "key", "name", "path", "url", "query", "text", "id"]
162
+ .map((k) => (typeof args[k] === "string" && args[k] ? args[k] : undefined))
163
+ .filter((v) => Boolean(v));
164
+ return truncateArg([args.action, ...extras].join(" "));
165
+ }
166
+ // Композиты, где один ключ уронил бы половину смысла.
167
+ if (tool === "move_file") {
168
+ const from = str(args.from);
169
+ const to = str(args.to);
170
+ if (from && to)
171
+ return truncateArg(`${from} → ${to}`);
172
+ }
173
+ if (tool === "mcp_call") {
174
+ const server = str(args.server);
175
+ const name = str(args.tool);
176
+ if (server && name)
177
+ return `${server}/${name}`;
178
+ }
179
+ if (tool === "remember") {
180
+ const category = str(args.category);
181
+ const key = str(args.key);
182
+ const entry = str(args.entry);
183
+ const body = key && args.value !== undefined ? `${key} = ${str(args.value)}` : key || entry;
184
+ const phrase = [category, body].filter(Boolean).join(" · ");
185
+ if (phrase)
186
+ return truncateArg(phrase);
187
+ }
188
+ if (tool === "enable_tools") {
189
+ const tags = Array.isArray(args.tags) ? args.tags.map(String).filter(Boolean) : [];
190
+ if (tags.length)
191
+ return tags.join(", ");
192
+ }
193
+ for (const key of GENERIC_ARG_KEYS) {
194
+ const value = str(args[key]);
195
+ if (value)
196
+ return truncateArg(value);
197
+ }
198
+ return summarizeArgs(args);
199
+ }
200
+ /**
201
+ * Локализованный человеческий ярлык тула. Системные имена (`read_file`, `bash`, …)
202
+ * НЕ показываются юзеру ни в одной локали — см. `docs/design/ui/STYLE.md`.
203
+ * Сырой идентификатор — лишь ключ поиска; без friendly-перевода откатываемся на
204
+ * общий локализованный ярлык, но не на сам идентификатор.
205
+ */
75
206
  function friendlyTool(tool) {
76
207
  const key = `tool.friendly.${tool}`;
77
208
  const value = t(key);
78
- return value !== key ? value : tool;
209
+ if (value !== key)
210
+ return value;
211
+ const fallback = t("ui.action_fallback");
212
+ return fallback !== "ui.action_fallback" ? fallback : "Action";
79
213
  }
80
214
  /**
81
- * Streams agent events to the terminal. Rich mode (TTY) adds a spinner;
82
- * plain mode falls back to simple prefixed lines.
83
- * Colors come from `colors.ts`, disabled outside a TTY, in CI, or on NO_COLOR.
215
+ * Стримит события агента в терминал. Rich-режим (TTY) добавляет спиннер;
216
+ * plain — простые строки с префиксом.
217
+ * Цвета из `colors.ts`, вне TTY / в CI / при NO_COLOR выключены.
84
218
  */
85
219
  export class Renderer {
86
220
  rich;
@@ -89,17 +223,30 @@ export class Renderer {
89
223
  channel;
90
224
  width;
91
225
  baseDir;
226
+ verbosity;
92
227
  card = null;
93
228
  thoughtStarted = false;
94
229
  thoughtStartMs = 0;
95
230
  thoughtHeaderPrinted = false;
231
+ /** Чередует короткие человеческие ярлыки, чтобы повторы thinking не звучали роботно. */
232
+ thinkingCount = 0;
233
+ thinkingLabel = "";
96
234
  textStarted = false;
235
+ reconBuffer = [];
236
+ /**
237
+ * Отложенная на шаг строка действия (quiet): даёт идентичному повтору
238
+ * свернуться в строку `×N`. Всегда сбрасывается до печати чего-то новее.
239
+ */
240
+ pendingFooter = null;
241
+ repeatCount = 0;
242
+ repeatMs = 0;
97
243
  outputLineCount = 0;
98
244
  outputSuppressed = false;
99
245
  constructor(opts = {}) {
100
246
  this.rich = opts.rich ?? isRichTerminal();
101
247
  this.width = opts.width ?? getTerminalWidth();
102
248
  this.baseDir = opts.baseDir;
249
+ this.verbosity = opts.verbosity ?? "normal";
103
250
  this.channel = opts.channel ?? getDefaultChannel();
104
251
  this.spinner = new Spinner({
105
252
  enabled: this.rich && (opts.spinner ?? true),
@@ -108,42 +255,56 @@ export class Renderer {
108
255
  });
109
256
  this.fmt = new FormattingStream((line) => this.channel.writeRaw(`${line}\n`), this.width, (text) => this.channel.writeRaw(text), { reservedCols: 2, streamPartials: this.rich });
110
257
  }
111
- /** Show a thinking spinner while waiting for LLM response. */
258
+ /** Спиннер обдумывания на время ожидания ответа LLM. */
112
259
  showLoader() {
113
260
  this.spinner.start(t("ui.thinking"));
114
261
  }
115
- /** Final answer streamed from the model (markdown-formatted). */
262
+ /** Финальный ответ модели стримом (markdown-формат). */
116
263
  text(chunk) {
117
264
  this.endCard();
265
+ this.flushRecon();
266
+ this.closeThought();
118
267
  this.spinner.stop();
119
268
  if (!this.textStarted) {
120
269
  this.textStarted = true;
121
- this.channel.writeRaw(`\n${pc.dim("-")} `);
270
+ // Ответ уже введён разделителем/ярлыком REPL; ведущий маркер конфликтовал
271
+ // бы с кодовым блоком, открывающим ответ.
272
+ this.channel.writeRaw("\n");
122
273
  }
123
274
  this.fmt.write(chunk);
124
275
  }
125
- /** Arbitrary status/meta text (tool output, reasoning, MoE status). */
276
+ /** Произвольный статус/мета-текст (вывод тула, reasoning, статус MoE). */
126
277
  meta(chunk) {
127
278
  this.spinner.stop();
128
279
  if (this.thoughtStarted) {
129
- if (!this.thoughtHeaderPrinted) {
130
- this.channel.writeRaw(`\n${pc.dim("→")} Thought: `);
131
- this.thoughtHeaderPrinted = true;
132
- }
280
+ this.writeThoughtHeader();
133
281
  this.channel.writeRaw(pc.dim(chunk));
134
282
  }
135
283
  else if (this.card) {
136
284
  this.writeInlineBody(chunk);
137
285
  }
138
286
  else {
287
+ this.flushRecon();
139
288
  this.channel.writeRaw(chunk);
140
289
  }
141
290
  }
142
- /** Stream tool body lines with the gutter prefix, skipping blank lines. */
291
+ /** Стримит строки тела тула с гуттером, пустые скипает. */
143
292
  writeInlineBody(chunk) {
144
293
  for (const line of chunk.split("\n")) {
145
294
  if (line.trim() === "")
146
295
  continue;
296
+ const plain = stripAnsi(line).trim();
297
+ if (this.card) {
298
+ if (this.card.body.length < MAX_OUTPUT_LINES)
299
+ this.card.body.push(plain);
300
+ if (!this.card.firstBody)
301
+ this.card.firstBody = plain;
302
+ }
303
+ if (this.verbosity === "quiet") {
304
+ // Первую строку держим, чтобы у провала осталось короткое объяснение, но
305
+ // чат не заливаем: детали — через /verbose и Ctrl+O.
306
+ continue;
307
+ }
147
308
  this.outputLineCount++;
148
309
  if (this.outputLineCount > MAX_OUTPUT_LINES) {
149
310
  this.outputSuppressed = true;
@@ -152,107 +313,272 @@ export class Renderer {
152
313
  this.channel.writeRaw(`${GUTTER}${line}\n`);
153
314
  }
154
315
  }
155
- /** Dimmed reasoning stream (already colored by the agent). */
316
+ /** Dim-стрим рассуждений (красится здесь, а не агентом). */
156
317
  reasoning(chunk) {
157
318
  this.spinner.stop();
158
- if (this.thoughtStarted && !this.thoughtHeaderPrinted) {
159
- this.channel.writeRaw(`\n${pc.dim("→")} Thought: `);
160
- this.thoughtHeaderPrinted = true;
161
- }
319
+ this.flushRecon();
320
+ this.writeThoughtHeader();
162
321
  this.channel.writeRaw(pc.dim(chunk));
163
322
  }
164
- /** Model is loading / generating — record start time. */
323
+ /**
324
+ * Открывает блок мысли: `✦ <акцентный ярлык> · ` — ярлык держит акцент
325
+ * рассуждений, разделитель и тело dim, чтобы глагол статуса не сливался
326
+ * с собственными словами модели.
327
+ */
328
+ writeThoughtHeader() {
329
+ if (!this.thoughtStarted || this.thoughtHeaderPrinted)
330
+ return;
331
+ this.channel.writeRaw(`\n${pc.magenta(glyph.thinking)} ${pc.magenta(this.thinkingLabel)} ${pc.dim("·")} `);
332
+ this.thoughtHeaderPrinted = true;
333
+ }
334
+ /** Модель грузится/генерирует — фиксируем старт. */
165
335
  thinkingStart() {
336
+ this.flushRecon();
166
337
  this.thoughtStarted = true;
167
338
  this.thoughtStartMs = Date.now();
168
339
  this.thoughtHeaderPrinted = false;
340
+ this.thinkingLabel = this.nextThinkingLabel();
169
341
  this.spinner.start(t("ui.thinking"));
170
342
  }
171
- /** LLM call finished; print duration and close the thought block. */
343
+ /**
344
+ * Чередует несколько коротких человеческих ярлыков по фазам thinking. Первая
345
+ * фаза всегда канонична `ui.thinking_label` (стабильно в тестах/скринах).
346
+ */
347
+ nextThinkingLabel() {
348
+ const index = this.thinkingCount++;
349
+ const key = index === 0 ? "ui.thinking_label" : `ui.thinking_label_${(index % 3) + 1}`;
350
+ const value = t(key);
351
+ return value !== key ? value : t("ui.thinking_label");
352
+ }
353
+ /** Вызов LLM завершён; печатаем длительность и закрываем блок мысли. */
172
354
  thinkingEnd() {
173
355
  this.spinner.stop();
356
+ this.closeThought();
357
+ }
358
+ /**
359
+ * Закрывает открытый блок мысли с печатью длительности. Безопасно звать
360
+ * многократно: и финальный ответ, и следующий тул требуют закрытого блока,
361
+ * чтобы длительность не приземлилась внутрь текста ответа.
362
+ */
363
+ closeThought() {
174
364
  if (this.thoughtStarted && this.thoughtHeaderPrinted) {
175
365
  const duration = Date.now() - this.thoughtStartMs;
176
- this.channel.writeRaw(pc.dim(` ${duration}ms\n`));
366
+ this.channel.writeRaw(pc.dim(` ${formatDuration(duration)}\n`));
177
367
  }
178
368
  this.thoughtStarted = false;
179
369
  this.thoughtHeaderPrinted = false;
180
370
  }
181
- toolStart(tool, args, stepContext, icon) {
371
+ /** Старт тула: шапка действия + busy-спиннер (кроме интерактивных). */
372
+ toolStart(tool, args, stepContext, _icon) {
182
373
  this.spinner.stop();
183
374
  this.fmt.flushPartialNow();
184
375
  this.endCard();
376
+ this.closeThought();
185
377
  this.outputLineCount = 0;
186
378
  this.outputSuppressed = false;
187
379
  const displayArgs = PATH_TOOLS.has(tool) && typeof args.path === "string"
188
380
  ? { ...args, path: toDisplayPath(this.baseDir, args.path) }
189
381
  : args;
190
- const summary = summarizeArgs(displayArgs);
191
- const marker = icon || toolMarker(tool);
382
+ const summary = humanArgs(tool, displayArgs);
192
383
  const label = friendlyTool(tool);
193
- this.card = { tool, args, start: Date.now() };
194
- if (stepContext) {
195
- this.channel.writeRaw(`\n${pc.dim("↓")} ${pc.cyan(stepContext)}\n`);
384
+ // Один глиф в цвете категории — пер-тул emoji-иконки не используются.
385
+ const mark = actionMarkFor(tool);
386
+ const accent = toolAccent(tool);
387
+ const deferred = this.verbosity === "quiet";
388
+ this.card = {
389
+ tool,
390
+ args,
391
+ start: Date.now(),
392
+ label,
393
+ summary,
394
+ mark,
395
+ deferred,
396
+ firstBody: "",
397
+ body: [],
398
+ };
399
+ if (stepContext && !deferred) {
400
+ this.channel.writeRaw(`\n${pc.dim(glyph.detail)} ${pc.cyan(stepContext)}\n`);
196
401
  }
197
- this.channel.writeRaw(`${pc.dim(marker)} ${label}${summary ? ` ${pc.dim(summary)}` : ""}\n`);
198
- // Busy indicator while the tool actually runs — read/write/edit/bash can
199
- // take seconds; without this the chat looks frozen until the footer.
402
+ if (!deferred) {
403
+ this.channel.writeRaw(`${mark} ${accent(label)}${summary ? ` ${pc.dim("·")} ${pc.dim(summary)}` : ""}\n`);
404
+ }
405
+ // Busy-индикатор на время реального исполнения: read/write/edit/bash могут
406
+ // идти секунды; без него чат выглядит зависшим до футера.
200
407
  if (!SPINNERLESS_TOOLS.has(tool)) {
201
408
  this.spinner.start(`${label}${summary ? ` ${summary}` : ""}`);
202
409
  }
203
410
  }
204
- /** Live plan checklist — prints the plan block as-is (already colored). */
411
+ /** Смена детализации рендера (использует `/verbose`). */
412
+ setVerbosity(verbosity) {
413
+ this.verbosity = verbosity;
414
+ }
415
+ /** Выпуск буферного прогона read-only действий (quiet). */
416
+ flushRecon() {
417
+ // Отложенная строка действия всегда старше любого буферного recon-прогона:
418
+ // печатаем её первой, чтобы чат остался хронологичным.
419
+ this.flushPendingFooter();
420
+ if (this.reconBuffer.length === 0)
421
+ return;
422
+ const items = this.reconBuffer;
423
+ this.reconBuffer = [];
424
+ if (items.length === 1) {
425
+ const it = items[0];
426
+ const accent = toolAccent(it.tool);
427
+ const head = `${it.mark} ${accent(it.label)}${it.summary ? ` ${pc.dim("·")} ${pc.dim(it.summary)}` : ""}`;
428
+ this.channel.writeRaw(`${head} ${successMark()} ${pc.dim(formatDuration(it.duration))}\n`);
429
+ return;
430
+ }
431
+ const total = items.reduce((a, b) => a + b.duration, 0);
432
+ const label = t("ui.actions_summary", { count: String(items.length) });
433
+ this.channel.writeRaw(`${pc.dim(glyph.group)} ${label} ${successMark()} ${pc.dim(`· ${formatDuration(total)}`)} ${pc.dim("· /verbose")}\n`);
434
+ }
435
+ /** Печать отложенной строки, повторы сворачивает в одну строку `×N`. */
436
+ flushPendingFooter() {
437
+ const p = this.pendingFooter;
438
+ if (!p)
439
+ return;
440
+ this.pendingFooter = null;
441
+ const times = this.repeatCount + 1;
442
+ const total = p.duration + this.repeatMs;
443
+ this.repeatCount = 0;
444
+ this.repeatMs = 0;
445
+ const head = `${p.leadMark} ${toolAccent(p.tool)(p.label)}${p.summary ? ` ${pc.dim("·")} ${pc.dim(p.summary)}` : ""}`;
446
+ const timesPart = times > 1 ? ` ${pc.dim(`×${times}`)}` : "";
447
+ this.channel.writeRaw(`${head} ${successMark()} ${pc.dim(formatDuration(total))}${timesPart}\n`);
448
+ }
449
+ /** Живой чеклист плана: печатает блок как есть (уже раскрашен). */
205
450
  planBlock(lines) {
206
451
  this.spinner.stop();
207
452
  this.fmt.flushPartialNow();
208
453
  this.endCard();
454
+ this.flushRecon();
209
455
  for (const line of lines) {
210
456
  this.channel.writeRaw(`${line}\n`);
211
457
  }
212
458
  }
459
+ /** Конец тула: футер с длительностью (ошибка → красный маркер + первая строка тела). */
213
460
  toolEnd(_tool, duration, error, ctxDelta, costUsd) {
214
461
  this.spinner.stop();
215
462
  this.fmt.flushPartialNow();
216
463
  if (!this.card)
217
464
  return;
218
- const marker = error ? pc.red("✗") : pc.green("✓");
219
- let footer = `${GUTTER}${marker} ${pc.dim(`${duration}ms`)}`;
465
+ const { label, summary, deferred } = this.card;
466
+ const mark = error ? failureMark() : successMark();
467
+ let tail = `${mark} ${pc.dim(formatDuration(duration))}`;
220
468
  if (ctxDelta !== undefined && ctxDelta !== 0) {
221
469
  const deltaStr = ctxDelta > 0 ? pc.green(`+${ctxDelta}`) : pc.yellow(`${ctxDelta} ↓`);
222
- footer += ` ${pc.dim("ctx")} ${deltaStr}`;
470
+ tail += ` ${pc.dim("ctx")} ${deltaStr}`;
223
471
  }
224
472
  if (costUsd !== undefined && costUsd > 0) {
225
- footer += ` ${pc.dim("cost")} ${pc.yellow(formatUsd(costUsd))}`;
473
+ tail += ` ${pc.dim("cost")} ${pc.yellow(formatUsd(costUsd))}`;
474
+ }
475
+ if (deferred) {
476
+ this.toolEndDeferred(tail, duration, error);
226
477
  }
227
- this.channel.writeRaw(`${footer}\n`);
228
- if (this.outputSuppressed) {
229
- this.channel.writeRaw(`${GUTTER}${pc.dim(`... (${this.outputLineCount - MAX_OUTPUT_LINES} more lines)`)}\n`);
478
+ else {
479
+ const footer = `${GUTTER}${tail}`;
480
+ this.channel.writeRaw(`${footer}\n`);
481
+ if (this.outputSuppressed) {
482
+ this.channel.writeRaw(`${GUTTER}${pc.dim(`... (${this.outputLineCount - MAX_OUTPUT_LINES} more lines)`)}\n`);
483
+ }
230
484
  }
485
+ this.recordLast(label, summary, error, duration);
231
486
  this.card = null;
232
487
  }
233
- /** Raw pre-formatted text (diff blocks, warnings). */
488
+ /**
489
+ * Quiet-футер: сворачивает отложенную шапку в одну строку действия.
490
+ * Успешные read-only тулы буферятся в recon-сводку; подряд идущие одинаковые
491
+ * успешные сворачиваются в строку `×N`; всё прочее буферится на шаг, чтобы
492
+ * повтор успел свернуться. Ошибки печатаются сразу.
493
+ */
494
+ toolEndDeferred(tail, duration, error) {
495
+ const card = this.card;
496
+ if (!card)
497
+ return;
498
+ const { label, summary, mark: leadMark, firstBody } = card;
499
+ // Прогон успешных read-only тулов буферится и суммируется.
500
+ if (!error && RECON_TOOLS.has(card.tool)) {
501
+ this.flushPendingFooter();
502
+ this.reconBuffer.push({ tool: card.tool, mark: leadMark, label, summary, duration });
503
+ return;
504
+ }
505
+ // Подряд идущие одинаковые успешные действия (poll-циклы, повторные
506
+ // статус-чеки) — в одну строку `×N` вместо N строк.
507
+ const repeatKey = `${card.tool}::${label}::${summary}`;
508
+ if (!error && this.pendingFooter?.key === repeatKey) {
509
+ this.repeatCount += 1;
510
+ this.repeatMs += duration;
511
+ return;
512
+ }
513
+ this.flushPendingFooter();
514
+ this.flushRecon();
515
+ if (!error) {
516
+ // Буфер на шаг: строка печатается на следующем действии (или любом сбросе),
517
+ // чтобы идентичный повтор успел свернуться.
518
+ this.pendingFooter = {
519
+ key: repeatKey,
520
+ leadMark,
521
+ tool: card.tool,
522
+ label,
523
+ summary,
524
+ duration,
525
+ };
526
+ return;
527
+ }
528
+ const head = `${leadMark} ${toolAccent(card.tool)(label)}${summary ? ` ${pc.dim("·")} ${pc.dim(summary)}` : ""}`;
529
+ this.channel.writeRaw(`${head} ${tail}\n`);
530
+ if (error && firstBody) {
531
+ const soft = firstBody.startsWith("⚠");
532
+ const color = soft ? pc.yellow : pc.red;
533
+ this.channel.writeRaw(`${GUTTER}${pc.dim(glyph.detail)} ${color(firstBody)}\n`);
534
+ }
535
+ }
536
+ /** Запоминает действие для допечатки по `Ctrl+O` (живёт дольше прогона). */
537
+ recordLast(label, summary, error, duration) {
538
+ const firstBody = this.card?.firstBody ?? "";
539
+ const status = !error
540
+ ? "ok"
541
+ : firstBody.startsWith("⚠")
542
+ ? "warn"
543
+ : "error";
544
+ recordLastAction({
545
+ tool: this.card?.tool ?? "",
546
+ label,
547
+ args: summary,
548
+ status,
549
+ durationMs: duration,
550
+ body: this.card?.body ?? [],
551
+ });
552
+ }
553
+ /** Сырой предформатированный текст (диффы, предупреждения). */
234
554
  raw(text) {
235
555
  this.spinner.stop();
236
556
  this.fmt.flushPartialNow();
237
557
  this.endCard();
558
+ this.flushRecon();
238
559
  this.channel.writeRaw(text);
239
560
  }
561
+ /** Ошибка в stderr с красным маркером. */
240
562
  error(text) {
241
563
  this.spinner.stop();
242
564
  this.fmt.flushPartialNow();
243
565
  this.endCard();
244
- this.channel.writeRaw(`${pc.red(text)}\n`, "stderr");
566
+ this.flushRecon();
567
+ this.channel.writeRaw(`${failureMark()} ${pc.red(text)}\n`, "stderr");
245
568
  }
569
+ /** Сброс всех буферов в конце хода. */
246
570
  flush() {
247
571
  this.endCard();
572
+ this.flushRecon();
248
573
  this.spinner.stop();
249
574
  this.fmt.flushPartialNow();
250
575
  this.fmt.flush();
251
576
  this.textStarted = false;
252
577
  }
253
- /** Footer with model, provider, and total duration. */
578
+ /** Футер с моделью, провайдером и общей длительностью. */
254
579
  footer(model, provider, durationMs) {
255
- this.channel.writeRaw(`\n${pc.dim("▣")} ${model} · ${provider} · ${pc.dim(`${durationMs}ms`)}\n`);
580
+ this.flushRecon();
581
+ this.channel.writeRaw(`\n${pc.dim(glyph.metric)} ${model} · ${provider} · ${pc.dim(formatDuration(durationMs))}\n`);
256
582
  }
257
583
  endCard() {
258
584
  if (!this.card)