micro-models-agent 2.11.0 → 2.13.1

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 (356) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/app.js +22 -2
  3. package/dist/assets/index.html +39 -9
  4. package/dist/assets/js/dom.js +15 -0
  5. package/dist/assets/js/feed.js +129 -10
  6. package/dist/assets/js/sidebar.js +512 -0
  7. package/dist/assets/js/tabs.js +30 -5
  8. package/dist/assets/setup.js +267 -8
  9. package/dist/assets/styles.css +168 -3
  10. package/dist/cli/cache-line.js +1 -0
  11. package/dist/cli/command-suggest.js +5 -4
  12. package/dist/cli/commands.js +231 -122
  13. package/dist/cli/completer.js +132 -31
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-payload.js +4 -4
  16. package/dist/cli/main.js +166 -61
  17. package/dist/cli/onboarding-commands.js +126 -0
  18. package/dist/cli/plugin-commands.js +2 -1
  19. package/dist/cli/relaunch.js +16 -13
  20. package/dist/cli/repl-commands-lsp.js +75 -0
  21. package/dist/cli/repl-commands-skills.js +105 -0
  22. package/dist/cli/repl-commands.js +589 -266
  23. package/dist/cli/repl.js +436 -177
  24. package/dist/cli/run-result.js +5 -6
  25. package/dist/cli/security-commands.js +37 -28
  26. package/dist/cli/setup-order.js +4 -55
  27. package/dist/cli/setup-prompt.js +3 -2
  28. package/dist/cli/setup.js +27 -107
  29. package/dist/cli/web-command.js +98 -0
  30. package/dist/config/budget.js +5 -5
  31. package/dist/config/config.js +103 -96
  32. package/dist/config/defaults.js +38 -8
  33. package/dist/config/domains.js +24 -27
  34. package/dist/config/env.js +46 -0
  35. package/dist/config/experts.js +2 -0
  36. package/dist/config/index.js +1 -0
  37. package/dist/config/paths.js +56 -0
  38. package/dist/config/redact.js +44 -0
  39. package/dist/config/registry-extra.js +334 -0
  40. package/dist/config/registry.js +584 -0
  41. package/dist/config/security.js +35 -36
  42. package/dist/config/timeouts.js +74 -0
  43. package/dist/config/utils.js +16 -15
  44. package/dist/core/agent/artifact-progress.js +59 -0
  45. package/dist/core/agent/artifact-resolve.js +79 -0
  46. package/dist/core/agent/audit-gate.js +7 -3
  47. package/dist/core/agent/compaction.js +2 -2
  48. package/dist/core/agent/constants.js +33 -4
  49. package/dist/core/agent/context-log.js +454 -0
  50. package/dist/core/agent/context-renderer.js +5 -0
  51. package/dist/core/agent/critic-gate.js +132 -0
  52. package/dist/core/agent/hallucination-gate.js +148 -38
  53. package/dist/core/agent/loop-state.js +15 -0
  54. package/dist/core/agent/prefix-monitor.js +1 -1
  55. package/dist/core/agent/reasoning-resolver.js +10 -1
  56. package/dist/core/agent/token-tracker.js +7 -4
  57. package/dist/core/agent/tool-batch.js +93 -18
  58. package/dist/core/agent/tool-images.js +18 -0
  59. package/dist/core/agent/tool-output.js +65 -11
  60. package/dist/core/agent/tool-pairing.js +60 -0
  61. package/dist/core/agent-moe.js +25 -24
  62. package/dist/core/agent.js +429 -108
  63. package/dist/core/bootstrap/agent-phase.js +66 -0
  64. package/dist/core/bootstrap/config-phase.js +86 -0
  65. package/dist/core/bootstrap/modules-phase.js +269 -0
  66. package/dist/core/bootstrap/provider-phase.js +183 -0
  67. package/dist/core/bootstrap/session-phase.js +204 -0
  68. package/dist/core/bootstrap/state.js +94 -0
  69. package/dist/core/bootstrap/workspace-phase.js +115 -0
  70. package/dist/core/bootstrap.js +92 -660
  71. package/dist/core/crash-handler.js +17 -7
  72. package/dist/core/environment.js +37 -12
  73. package/dist/core/host-plan.js +43 -0
  74. package/dist/core/host.js +23 -0
  75. package/dist/core/index.js +1 -0
  76. package/dist/core/prompt-builder.js +17 -1
  77. package/dist/core/prompt-overflow.js +42 -43
  78. package/dist/core/session-logger.js +122 -30
  79. package/dist/core/system-prompt.js +117 -0
  80. package/dist/core/version.js +6 -4
  81. package/dist/core/web-host.js +34 -0
  82. package/dist/core/workspace.js +19 -15
  83. package/dist/i18n/en.json +89 -3
  84. package/dist/i18n/index.js +37 -3
  85. package/dist/i18n/ru.json +89 -3
  86. package/dist/index.js +7 -3
  87. package/dist/llm/image-utils.js +55 -64
  88. package/dist/llm/llm-errors.js +9 -0
  89. package/dist/llm/model-loader.js +10 -32
  90. package/dist/llm/models-dev.js +121 -0
  91. package/dist/llm/openai-compat.js +146 -73
  92. package/dist/llm/orchestrator.js +33 -11
  93. package/dist/llm/provider-budget.js +2 -2
  94. package/dist/llm/provider-context.js +233 -0
  95. package/dist/llm/provider.js +1 -1
  96. package/dist/llm/response.js +5 -0
  97. package/dist/llm/token-counter.js +27 -11
  98. package/dist/logger/app-logger.js +48 -22
  99. package/dist/logger/file-log.js +28 -19
  100. package/dist/logger/index.js +1 -0
  101. package/dist/main.js +888 -139
  102. package/dist/migration/backup.js +14 -3
  103. package/dist/migration/detect.js +12 -3
  104. package/dist/migration/index.js +1 -0
  105. package/dist/modules/artifacts/store.js +9 -8
  106. package/dist/modules/browser/actions.js +6 -0
  107. package/dist/modules/browser/bridge-client.js +14 -12
  108. package/dist/modules/browser/bridge-path.js +4 -4
  109. package/dist/modules/browser/console-buffer.js +37 -0
  110. package/dist/modules/browser/cookie-store.js +5 -0
  111. package/dist/modules/browser/driver-types.js +1 -0
  112. package/dist/modules/browser/driver.js +19 -11
  113. package/dist/modules/browser/index.js +3 -1
  114. package/dist/modules/browser/module.js +2 -1
  115. package/dist/modules/browser/session.js +10 -30
  116. package/dist/modules/browser/snapshot.js +19 -13
  117. package/dist/modules/browser/types.js +1 -0
  118. package/dist/modules/certification/cli.js +28 -20
  119. package/dist/modules/certification/fact-checker.js +9 -1
  120. package/dist/modules/certification/loader.js +9 -2
  121. package/dist/modules/certification/manifest.js +83 -21
  122. package/dist/modules/certification/runner.js +23 -13
  123. package/dist/modules/certification/scenarios.js +20 -20
  124. package/dist/modules/certification/syntax-scenarios.js +1 -2
  125. package/dist/modules/context/chunk-query.js +11 -9
  126. package/dist/modules/context/fact-extractor.js +37 -39
  127. package/dist/modules/context/index.js +1 -0
  128. package/dist/modules/context/manager.js +106 -80
  129. package/dist/modules/critic/client.js +176 -0
  130. package/dist/modules/critic/evidence.js +81 -0
  131. package/dist/modules/critic/provider.js +45 -0
  132. package/dist/modules/critic/tools.js +78 -0
  133. package/dist/modules/critic/types.js +1 -0
  134. package/dist/modules/execution/audit-runners.js +28 -34
  135. package/dist/modules/execution/auditor.js +53 -49
  136. package/dist/modules/execution/execution-plugin.js +142 -102
  137. package/dist/modules/execution/index.js +1 -1
  138. package/dist/modules/execution/input-from.js +37 -13
  139. package/dist/modules/execution/module.js +368 -191
  140. package/dist/modules/execution/moe-executor.js +53 -26
  141. package/dist/modules/execution/plan-actions.js +320 -202
  142. package/dist/modules/execution/plan-checks.js +35 -0
  143. package/dist/modules/execution/plan-coverage.js +36 -33
  144. package/dist/modules/execution/plan-steps.js +11 -3
  145. package/dist/modules/execution/plan-store.js +83 -17
  146. package/dist/modules/execution/plan-tool.js +89 -35
  147. package/dist/modules/execution/plan-validator.js +30 -10
  148. package/dist/modules/execution/planner.js +22 -13
  149. package/dist/modules/execution/stuck-detector.js +196 -93
  150. package/dist/modules/execution/tracker.js +22 -8
  151. package/dist/modules/execution/verifier.js +69 -48
  152. package/dist/modules/execution/windows-commands.js +12 -12
  153. package/dist/modules/hallucination/artifact-claim.js +93 -0
  154. package/dist/modules/hallucination/confidence.js +13 -11
  155. package/dist/modules/hallucination/consistency.js +10 -2
  156. package/dist/modules/hallucination/detector.js +10 -5
  157. package/dist/modules/hallucination/factual.js +43 -26
  158. package/dist/modules/hallucination/index.js +1 -0
  159. package/dist/modules/hallucination/js-identifiers.js +44 -45
  160. package/dist/modules/hallucination/llm-judge.js +17 -12
  161. package/dist/modules/hallucination/success-claim.js +59 -0
  162. package/dist/modules/indexer/cache.js +52 -4
  163. package/dist/modules/indexer/index.js +1 -0
  164. package/dist/modules/indexer/map-command.js +2 -4
  165. package/dist/modules/indexer/map-select.js +16 -16
  166. package/dist/modules/indexer/module.js +47 -12
  167. package/dist/modules/indexer/project-profile.js +8 -7
  168. package/dist/modules/indexer/symbols.js +15 -15
  169. package/dist/modules/indexer/walker.js +159 -29
  170. package/dist/modules/lsp/check-tool.js +7 -5
  171. package/dist/modules/lsp/client.js +39 -39
  172. package/dist/modules/lsp/command.js +17 -24
  173. package/dist/modules/lsp/config.js +12 -11
  174. package/dist/modules/lsp/index.js +1 -0
  175. package/dist/modules/lsp/module.js +48 -46
  176. package/dist/modules/lsp/probe.js +13 -14
  177. package/dist/modules/lsp/project-root.js +14 -17
  178. package/dist/modules/lsp/startup-check.js +18 -16
  179. package/dist/modules/mcp/client.js +59 -62
  180. package/dist/modules/mcp/index.js +1 -0
  181. package/dist/modules/mcp/module.js +66 -37
  182. package/dist/modules/mcp/registry.js +5 -0
  183. package/dist/modules/memory/index.js +1 -0
  184. package/dist/modules/memory/module.js +12 -8
  185. package/dist/modules/memory/search.js +4 -1
  186. package/dist/modules/memory/store.js +40 -2
  187. package/dist/modules/onboarding/init.js +47 -0
  188. package/dist/modules/onboarding/meet.js +30 -0
  189. package/dist/modules/onboarding/mode.js +30 -0
  190. package/dist/modules/onboarding/prompts.js +43 -0
  191. package/dist/modules/pipelines/engine.js +10 -0
  192. package/dist/modules/pipelines/index.js +1 -0
  193. package/dist/modules/pipelines/parser.js +4 -1
  194. package/dist/modules/pipelines/template.js +4 -0
  195. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  196. package/dist/modules/plugins/builtin/notify.js +2 -0
  197. package/dist/modules/plugins/index.js +1 -0
  198. package/dist/modules/plugins/loader.js +18 -3
  199. package/dist/modules/plugins/manager.js +37 -17
  200. package/dist/modules/pricing/index.js +10 -11
  201. package/dist/modules/pricing/prices.js +12 -12
  202. package/dist/modules/processes/index.js +1 -0
  203. package/dist/modules/processes/kill-tree.js +27 -31
  204. package/dist/modules/processes/registry.js +44 -25
  205. package/dist/modules/processes/runner.js +4 -4
  206. package/dist/modules/providers/capabilities.js +28 -0
  207. package/dist/modules/providers/create.js +3 -4
  208. package/dist/modules/providers/factory.js +7 -7
  209. package/dist/modules/providers/fallback.js +12 -12
  210. package/dist/modules/providers/health.js +4 -4
  211. package/dist/modules/providers/manager.js +50 -19
  212. package/dist/modules/providers/naming.js +15 -0
  213. package/dist/modules/providers/preflight.js +71 -0
  214. package/dist/modules/providers/presets.js +39 -8
  215. package/dist/modules/providers/registry.js +7 -2
  216. package/dist/modules/reasoning/policy.js +8 -6
  217. package/dist/modules/reasoning/probe.js +30 -25
  218. package/dist/modules/security/audit-log.js +19 -36
  219. package/dist/modules/security/audit-notifier.js +35 -75
  220. package/dist/modules/security/command-validator.js +99 -51
  221. package/dist/modules/security/content-scanner.js +8 -8
  222. package/dist/modules/security/data-sanitizer.js +9 -16
  223. package/dist/modules/security/encryption.js +32 -52
  224. package/dist/modules/security/index.js +2 -1
  225. package/dist/modules/security/network-validator.js +120 -43
  226. package/dist/modules/security/path-validator.js +61 -46
  227. package/dist/modules/security/protected-paths.js +73 -0
  228. package/dist/modules/security/rate-limiter.js +17 -49
  229. package/dist/modules/security/security-policies.js +25 -57
  230. package/dist/modules/security/session-encryption.js +32 -71
  231. package/dist/modules/security/session-isolation.js +15 -28
  232. package/dist/modules/session/auto-name.js +63 -0
  233. package/dist/modules/session/export.js +98 -0
  234. package/dist/modules/session/index.js +1 -0
  235. package/dist/modules/session/manager.js +79 -28
  236. package/dist/modules/session/module.js +4 -3
  237. package/dist/modules/session/store.js +57 -20
  238. package/dist/modules/setup/answers.js +35 -0
  239. package/dist/modules/setup/apply.js +79 -0
  240. package/dist/modules/setup/discovery.js +72 -0
  241. package/dist/modules/skills/index.js +1 -0
  242. package/dist/modules/skills/loader.js +9 -1
  243. package/dist/modules/skills/module.js +11 -0
  244. package/dist/modules/updater/changelog-reader.js +15 -16
  245. package/dist/modules/updater/checker.js +9 -4
  246. package/dist/modules/updater/dev-detect.js +7 -6
  247. package/dist/modules/updater/index.js +1 -0
  248. package/dist/modules/updater/module.js +17 -15
  249. package/dist/modules/user-profile/compressor.js +2 -0
  250. package/dist/modules/user-profile/index.js +1 -0
  251. package/dist/modules/user-profile/profile.js +10 -1
  252. package/dist/modules/webui/answer-api.js +29 -0
  253. package/dist/modules/webui/assets.js +50 -0
  254. package/dist/modules/webui/body.js +21 -0
  255. package/dist/modules/webui/commands-settings.js +209 -0
  256. package/dist/modules/webui/commands-shared.js +60 -0
  257. package/dist/modules/webui/commands.js +174 -0
  258. package/dist/modules/webui/config-api.js +134 -0
  259. package/dist/modules/webui/file-api.js +202 -0
  260. package/dist/modules/webui/forward.js +159 -0
  261. package/dist/modules/webui/history-api.js +112 -0
  262. package/dist/modules/webui/host-state.js +60 -0
  263. package/dist/modules/webui/i18n-api.js +9 -0
  264. package/dist/modules/webui/limits.js +6 -0
  265. package/dist/modules/webui/log-forward.js +20 -0
  266. package/dist/modules/webui/memory-commands.js +64 -0
  267. package/dist/modules/webui/model-cache.js +18 -0
  268. package/dist/modules/webui/module.js +148 -0
  269. package/dist/modules/webui/persist.js +24 -0
  270. package/dist/modules/webui/prompt-io.js +97 -0
  271. package/dist/modules/webui/providers-api.js +337 -0
  272. package/dist/modules/webui/providers-view.js +56 -0
  273. package/dist/modules/webui/reasoning-view.js +15 -0
  274. package/dist/modules/webui/run-agent.js +81 -0
  275. package/dist/modules/webui/server.js +284 -0
  276. package/dist/modules/webui/sessions-api.js +178 -0
  277. package/dist/modules/webui/settings-api.js +561 -0
  278. package/dist/modules/webui/setup-api.js +201 -0
  279. package/dist/modules/webui/state.js +74 -0
  280. package/dist/modules/webui/util.js +261 -0
  281. package/dist/output/bus.js +7 -2
  282. package/dist/output/channel.js +80 -28
  283. package/dist/output/format.js +2 -1
  284. package/dist/output/index.js +1 -0
  285. package/dist/output/json-sink.js +5 -3
  286. package/dist/output/machine.js +4 -3
  287. package/dist/output/session-sink.js +4 -4
  288. package/dist/tools/approve.js +4 -0
  289. package/dist/tools/attach-image.js +5 -4
  290. package/dist/tools/auto-fixer.js +26 -44
  291. package/dist/tools/auto-tags.js +83 -0
  292. package/dist/tools/bash.js +193 -116
  293. package/dist/tools/browser.js +76 -31
  294. package/dist/tools/chunk-query.js +14 -6
  295. package/dist/tools/create-dir.js +9 -5
  296. package/dist/tools/delete-file.js +9 -5
  297. package/dist/tools/download-file.js +32 -7
  298. package/dist/tools/edit-file.js +52 -14
  299. package/dist/tools/enable-tools.js +6 -6
  300. package/dist/tools/executor.js +132 -27
  301. package/dist/tools/file-info.js +3 -1
  302. package/dist/tools/filter-tools.js +3 -3
  303. package/dist/tools/glob-tool.js +4 -1
  304. package/dist/tools/grep-tool.js +13 -10
  305. package/dist/tools/hidden-tools-block.js +6 -6
  306. package/dist/tools/index.js +16 -8
  307. package/dist/tools/list-dir.js +4 -2
  308. package/dist/tools/load-skill.js +7 -0
  309. package/dist/tools/mcp-call.js +15 -4
  310. package/dist/tools/meet-done.js +14 -0
  311. package/dist/tools/move-file.js +14 -7
  312. package/dist/tools/path-utils.js +29 -29
  313. package/dist/tools/pipeline-run.js +29 -7
  314. package/dist/tools/preview.js +1 -1
  315. package/dist/tools/process-ids.js +31 -0
  316. package/dist/tools/process-kill.js +9 -16
  317. package/dist/tools/process-list.js +1 -0
  318. package/dist/tools/process-log.js +11 -16
  319. package/dist/tools/process-wait.js +63 -0
  320. package/dist/tools/prompt-io.js +4 -3
  321. package/dist/tools/question.js +172 -25
  322. package/dist/tools/read-file.js +55 -34
  323. package/dist/tools/recall.js +14 -8
  324. package/dist/tools/registry.js +6 -5
  325. package/dist/tools/remember.js +8 -4
  326. package/dist/tools/scope-request.js +11 -6
  327. package/dist/tools/search-history.js +5 -2
  328. package/dist/tools/session-info.js +2 -0
  329. package/dist/tools/set-thinking.js +8 -4
  330. package/dist/tools/subagent.js +59 -39
  331. package/dist/tools/syntax-validator.js +292 -90
  332. package/dist/tools/user-input.js +22 -18
  333. package/dist/tools/web-browse.js +9 -3
  334. package/dist/tools/web-fetch.js +12 -23
  335. package/dist/tools/web-search.js +7 -5
  336. package/dist/tools/write-file.js +45 -15
  337. package/dist/ui/action-log.js +13 -0
  338. package/dist/ui/box.js +4 -3
  339. package/dist/ui/colors.js +2 -0
  340. package/dist/ui/diff.js +6 -5
  341. package/dist/ui/index.js +1 -0
  342. package/dist/ui/line-editor.js +230 -122
  343. package/dist/ui/line-math.js +10 -2
  344. package/dist/ui/md-formatter.js +57 -37
  345. package/dist/ui/output.js +8 -3
  346. package/dist/ui/plan-view.js +21 -10
  347. package/dist/ui/renderer.js +393 -67
  348. package/dist/ui/spinner.js +16 -5
  349. package/dist/ui/table.js +15 -17
  350. package/dist/ui/theme.js +143 -0
  351. package/dist/utils/child-run.js +20 -0
  352. package/dist/utils/error.js +1 -1
  353. package/dist/utils/index.js +2 -0
  354. package/dist/utils/retry.js +2 -2
  355. package/dist/utils/truncate.js +1 -1
  356. package/package.json +1 -1
@@ -1,10 +1,14 @@
1
+ // Строчный редактор: raw-ввод, история, Tab-комплит, DSR-якорь кадра (см. DSR ниже).
1
2
  import * as readline from "readline";
2
3
  import stringWidth from "string-width";
4
+ import { pc } from "./colors";
5
+ import { readEnv } from "../config/env";
3
6
  import { buildLayout, layoutIndexFor, visualColAt, charOffsetFor, charLen, stripAnsi, } from "./line-math";
7
+ /** Многострочный редактор ввода с историей и Tab-комплитом (raw-режим TTY). */
4
8
  export class LineEditor {
5
9
  input;
6
10
  output;
7
- /** Host hook: return true to swallow the key before the editor handles it. */
11
+ /** Хук хоста: true — поглотить клавишу до обработки редактором. */
8
12
  onKeyInput;
9
13
  onInterrupt;
10
14
  promptStr;
@@ -14,55 +18,59 @@ export class LineEditor {
14
18
  lines = [""];
15
19
  row = 0;
16
20
  col = 0;
21
+ /** Перенос не-TTY чанкованного ввода (частичная строка между чтениями). */
22
+ pendingLine = "";
17
23
  historyIndex = -1;
18
24
  stash = null;
19
25
  isDone = false;
20
26
  isPaste = false;
21
- /** Pasted characters accumulated between `paste-start` and `paste-end`.
22
- * Inserted as a single batch on `paste-end` — one render instead of one
23
- * per keypress, so a paste appears as a paste, not as fast typing. */
27
+ /** Вставленное копятся между `paste-start` и `paste-end`.
28
+ * Вставляется одним батчем на `paste-end`: один рендер вместо рендера на
29
+ * клавишу, чтобы вставка выглядела вставкой, а не быстрой печатью. */
24
30
  pasteBuffer = "";
25
- /** Whether the last paste keypress was a bare CR — used to collapse a
26
- * CRLF pair into a single newline (Windows clipboard text). */
31
+ /** Была ли последняя вставленная клавиша голым CR: схлопывает CRLF-пару
32
+ * в один перевод строки (текст из буфера Windows). */
27
33
  pasteLastWasCR = false;
28
34
  lastWasCR = false;
29
35
  questionCb = null;
36
+ /** Активная Tab-сессия комплита. Пока стоит, ↑/↓/Tab крутят кандидатов, а
37
+ * Esc возвращает `original`; любая другая клавиша сессию роняет. */
38
+ completionSession = null;
30
39
  rawMode = false;
31
40
  listeners = {};
32
- /** stdin handlers registered in the constructor — removed in close() so
33
- * re-creating the editor never accumulates stale listeners. */
41
+ /** stdin-хендлеры из конструктора: снимаются в close(), чтобы пересоздание
42
+ * редактора не копило протухшие слушатели. */
34
43
  dsrDataHandler;
35
44
  keypressHandler;
36
45
  dataHandler;
37
46
  endHandler;
38
- /** Visual row (within the drawn frame) where the cursor was placed by the
39
- * last render — fallback for moving back to the frame's first row before
40
- * redrawing when no absolute anchor is known yet. Relative moves break
41
- * once the terminal scrolls (the frame shifts up on screen while the
42
- * relative count does not), which duplicated multi-line input. */
47
+ /** Визуальный ряд (внутри рисуемого кадра), куда последний рендер поставил
48
+ * курсор. Фолбэк для возврата на первую строку кадра перед перерисовкой,
49
+ * пока нет абсолютного якоря. Относительные ходы ломаются при скролле
50
+ * терминала (кадр уезжает вверх, счётчик — нет), что дублировало
51
+ * многострочный ввод. */
43
52
  prevCursorRow = 0;
44
- /** Absolute 1-based screen row of the frame's first visual row, learned
45
- * from a single DSR cursor-position report (`ESC[6n`) sent once per
46
- * prompt presentation. After the initial query the anchor is updated
47
- * deterministically via scroll math — no further queries are sent,
48
- * avoiding ConPTY desync. Falls back to relative moves when the reply
49
- * never arrives (timeout / broken terminal). */
53
+ /** Абсолютный 1-based экранный ряд первой визуальной строки кадра. Выучен
54
+ * из одного DSR-ответа о позиции курсора (`ESC[6n`), шлётся раз на показ
55
+ * промпта. Дальше якорь ведётся детерминированной скролл-математикой, новых
56
+ * запросов нет (защита от рассинхрона ConPTY). Без ответа (таймаут/битый
57
+ * терминал) — фолбэк на относительные ходы. */
50
58
  frameTopAbs = null;
51
- /** True while waiting for a DSR reply (exactly one outstanding query). */
59
+ /** Ждём ли DSR-ответ (ровно один outstanding-запрос). */
52
60
  dsrPending = false;
53
- /** Buffer for partial DSR escape sequences split across data chunks. */
61
+ /** Буфер частичных DSR-escape, разрезанных между чанками. */
54
62
  dsrTail = "";
55
- /** Visual row of the cursor at the exact moment the outstanding DSR query
56
- * was sent. The reply is matched against this, not `prevCursorRow`, which
57
- * later renders overwrite while the query is still pending. */
63
+ /** Визуальный ряд курсора ровно в момент отправки outstanding DSR-запроса.
64
+ * Ответ матчится против него, а не `prevCursorRow`: промежуточные рендеры
65
+ * перезаписывают тот, пока запрос ещё летит. */
58
66
  dsrQueryRow = 0;
59
- /** When false, keystrokes are ignored (except Ctrl+C) while the agent
60
- * streams — the host owns the terminal during that phase. */
67
+ /** Выкл — клавиши игнорятся (кроме Ctrl+C), пока агент стримит: терминалом
68
+ * в этой фазе владеет хост. */
61
69
  inputEnabled = true;
62
- /** While true the prompt frame is not drawn; the agent owns the screen. */
70
+ /** Вкл — кадр промпта не рисуется; экраном владеет агент. */
63
71
  frameSuspended = false;
64
- /** Clears `dsrPending` when no DSR reply arrives, falling back to
65
- * relative cursor moves instead of wedging on a broken terminal. */
72
+ /** Сбрасывает `dsrPending`, если DSR-ответ не пришёл: фолбэк на относительные
73
+ * ходы вместо клина на битом терминале. */
66
74
  dsrTimer = null;
67
75
  constructor(opts) {
68
76
  this.input = opts.input;
@@ -80,17 +88,17 @@ export class LineEditor {
80
88
  if (this.rawMode)
81
89
  this.input.setRawMode(true);
82
90
  this.enableTerminalProtocols();
83
- // Raw data listener for DSR cursor-position replies. Runs alongside
84
- // readline's keypress emitter (both receive every chunk); replies may
85
- // arrive merged with regular keystrokes, hence the global regex.
91
+ // Сырой слушатель DSR-ответов о позиции курсора. Живёт рядом с keypress-эмитером
92
+ // readline (оба получают каждый чанк); ответы могут приходить склеенными с
93
+ // обычными клавишами — отсюда глобальный regex.
86
94
  this.dsrDataHandler = (buf) => this.consumeDsrReplies(buf.toString("utf-8"));
87
95
  this.input.on("data", this.dsrDataHandler);
88
96
  readline.emitKeypressEvents(this.input);
89
97
  this.keypressHandler = (str, key) => this.onKey(str, key);
90
98
  this.input.on("keypress", this.keypressHandler);
91
- // The editor owns stdin now: ensure it is flowing even if a previous
92
- // consumer paused it (e.g. the setup wizard).
93
- this.input.resume?.();
99
+ // Редактор теперь владеет stdin: взводим flowing, даже если прошлый
100
+ // потребитель его стопил (напр. визард настройки).
101
+ this.input.resume();
94
102
  }
95
103
  else {
96
104
  this.dataHandler = (buf) => this.onData(buf);
@@ -189,6 +197,7 @@ export class LineEditor {
189
197
  this.col = 0;
190
198
  this.historyIndex = -1;
191
199
  this.stash = null;
200
+ this.completionSession = null;
192
201
  this.invalidateAnchor();
193
202
  if (this.input.isTTY)
194
203
  this.render();
@@ -199,6 +208,7 @@ export class LineEditor {
199
208
  this.lines = [""];
200
209
  this.row = 0;
201
210
  this.col = 0;
211
+ this.completionSession = null;
202
212
  this.invalidateAnchor();
203
213
  if (this.input.isTTY)
204
214
  this.render();
@@ -208,15 +218,14 @@ export class LineEditor {
208
218
  return;
209
219
  this.isDone = true;
210
220
  this.disableTerminalProtocols();
211
- const inp = this.input;
212
221
  if (this.dsrDataHandler)
213
- inp.removeListener("data", this.dsrDataHandler);
222
+ this.input.removeListener("data", this.dsrDataHandler);
214
223
  if (this.keypressHandler)
215
- inp.removeListener("keypress", this.keypressHandler);
224
+ this.input.removeListener("keypress", this.keypressHandler);
216
225
  if (this.dataHandler)
217
- inp.removeListener("data", this.dataHandler);
226
+ this.input.removeListener("data", this.dataHandler);
218
227
  if (this.endHandler)
219
- inp.removeListener("end", this.endHandler);
228
+ this.input.removeListener("end", this.endHandler);
220
229
  if (this.rawMode && this.input.setRawMode) {
221
230
  this.input.setRawMode(false);
222
231
  }
@@ -226,34 +235,30 @@ export class LineEditor {
226
235
  }
227
236
  this.emitClose();
228
237
  }
229
- // ---- input handling ----
238
+ // ── Ввод ──
230
239
  onData(buf) {
231
240
  if (this.isDone)
232
241
  return;
233
242
  let text = buf.toString("utf-8");
234
243
  text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
235
244
  const parts = text.split("\n");
236
- for (let i = 0; i < parts.length; i++) {
237
- if (i < parts.length - 1) {
238
- this.emitLine(parts[i]);
239
- this.lines = [""];
240
- this.row = 0;
241
- this.col = 0;
242
- }
243
- else if (parts[i]) {
244
- this.emitLine(parts[i]);
245
- this.lines = [""];
246
- this.row = 0;
247
- this.col = 0;
248
- }
245
+ // Незавершённый хвост — в следующий чанк: немедленный эмит разрезал бы
246
+ // `"hello wor" + "ld\n"` на две неверные строки.
247
+ this.pendingLine += parts[0];
248
+ for (let i = 1; i < parts.length; i++) {
249
+ this.emitLine(this.pendingLine);
250
+ this.lines = [""];
251
+ this.row = 0;
252
+ this.col = 0;
253
+ this.pendingLine = parts[i];
249
254
  }
250
255
  }
251
256
  onKey(str, key) {
252
257
  if (this.isDone)
253
258
  return;
254
259
  if (!this.inputEnabled) {
255
- // While streaming, only interrupt keys are honored: Ctrl+C aborts the
256
- // run, Escape is forwarded so the host can implement double-Esc abort.
260
+ // На стриме уважаются только прерывающие клавиши: Ctrl+C абортит прогон,
261
+ // Esc форвардится, чтобы хост реализовал двойной-Esc аборт.
257
262
  const k = key;
258
263
  if (k?.ctrl && k.name === "c") {
259
264
  this.ctrlC();
@@ -265,7 +270,31 @@ export class LineEditor {
265
270
  }
266
271
  return;
267
272
  }
268
- if (this.onKeyInput && this.onKeyInput(str, key))
273
+ // Сессия комплита приоритетнее хост-хуков: Esc обязан вернуть исходный
274
+ // partial, а не дёрнуть хостовый Esc-хендлинг; стрелки/Tab — крутить.
275
+ // Любая другая клавиша роняет сессию и обрабатывается обычно (с хост-хуком).
276
+ if (this.completionSession) {
277
+ const ck = key;
278
+ const plain = !ck.ctrl && !ck.meta;
279
+ if (ck?.name === "down" && plain) {
280
+ this.cycleCompletion(1);
281
+ return;
282
+ }
283
+ if (ck?.name === "up" && plain) {
284
+ this.cycleCompletion(-1);
285
+ return;
286
+ }
287
+ if (ck?.name === "tab") {
288
+ this.cycleCompletion(1);
289
+ return;
290
+ }
291
+ if (ck?.name === "escape") {
292
+ this.cancelCompletion();
293
+ return;
294
+ }
295
+ this.completionSession = null;
296
+ }
297
+ if (this.onKeyInput?.(str, key))
269
298
  return;
270
299
  const k = key;
271
300
  const name = k?.name ?? "undefined";
@@ -277,10 +306,9 @@ export class LineEditor {
277
306
  }
278
307
  return;
279
308
  }
280
- // Never treat a cursor-position report (`ESC[row;colR`) as typed input:
281
- // on some Windows/ConPTY + Bun combinations the trailing "R" leaks into
282
- // the buffer and corrupts the answer (observed as every prompt answering
283
- // "R"). Swallow the whole sequence.
309
+ // Отчёт о позиции курсора (`ESC[row;colR`) — никогда не ввод: на части связок
310
+ // Windows/ConPTY + Bun хвостовой "R" протекает в буфер и портит ответ
311
+ // (каждый промпт отвечал "R"). Глотаем последовательность целиком.
284
312
  if (/^\x1b\[\d+;\d+R$/.test(seq))
285
313
  return;
286
314
  if (name === "paste-start" || seq === "\x1b[200~") {
@@ -344,8 +372,8 @@ export class LineEditor {
344
372
  this.lastWasCR = false;
345
373
  return;
346
374
  }
347
- // In question mode, LF should also submit (script/pipes send
348
- // \n without preceding \r, unlike real terminals).
375
+ // В question-режиме LF тоже сабмитит (скрипты/пайпы шлют \n без \r спереди,
376
+ // в отличие от настоящих терминалов).
349
377
  if (this.questionCb) {
350
378
  this.submit();
351
379
  return;
@@ -450,6 +478,7 @@ export class LineEditor {
450
478
  }
451
479
  }
452
480
  submit() {
481
+ this.completionSession = null;
453
482
  if (this.questionCb) {
454
483
  const cb = this.questionCb;
455
484
  this.questionCb = null;
@@ -505,15 +534,16 @@ export class LineEditor {
505
534
  this.lines = [""];
506
535
  this.row = 0;
507
536
  this.col = 0;
537
+ this.completionSession = null;
508
538
  this.invalidateAnchor();
509
539
  this.render();
510
540
  if (this.onInterrupt) {
511
541
  this.onInterrupt();
512
542
  return;
513
543
  }
514
- // Default: emit only when someone listens. With zero handlers
515
- // process.emit("SIGINT") takes the DEFAULT action (process termination);
516
- // the REPL always installs a listener, so production is unaffected.
544
+ // Дефолт: эмитим, только если слушают. С нулём хендлеров process.emit("SIGINT")
545
+ // выполняет действие ПО УМОЛЧАНИЮ (смерть процесса); у REPL слушатель всегда
546
+ // стоит, так что прод не страдает.
517
547
  if (process.listenerCount("SIGINT") > 0) {
518
548
  process.emit("SIGINT");
519
549
  }
@@ -526,7 +556,7 @@ export class LineEditor {
526
556
  this.deleteForward();
527
557
  }
528
558
  }
529
- // ---- editing primitives ----
559
+ // ── Примитивы правки ──
530
560
  currentChars() {
531
561
  return Array.from(this.lines[this.row]);
532
562
  }
@@ -534,6 +564,7 @@ export class LineEditor {
534
564
  this.lines[this.row] = chars.join("");
535
565
  }
536
566
  insertText(text) {
567
+ this.completionSession = null;
537
568
  const sanitized = text.replace(/\r\n/g, "\n").replace(/\r/g, "");
538
569
  const chars = this.currentChars();
539
570
  const t = Array.from(sanitized);
@@ -542,9 +573,9 @@ export class LineEditor {
542
573
  this.col += t.length;
543
574
  this.render();
544
575
  }
545
- /** Bulk-insert pasted text at the cursor. Splits on newlines so the buffer
546
- * keeps one string per line (instead of embedding literal `\n` chars),
547
- * and renders once — a paste must look like a paste, not like typing. */
576
+ /** Пакетная вставка текста под курсор. Режет по переводам строк, чтобы буфер
577
+ * держал строку на элемент (а не литеральные `\n`), и рендерит один раз:
578
+ * вставка должна выглядеть вставкой, а не печатью. */
548
579
  insertPaste(text) {
549
580
  const segments = text.split("\n");
550
581
  if (segments.length === 1) {
@@ -759,6 +790,7 @@ export class LineEditor {
759
790
  }
760
791
  }
761
792
  applyHistoryEntry() {
793
+ this.completionSession = null;
762
794
  const entry = this.history[this.history.length - 1 - this.historyIndex];
763
795
  this.lines = entry.split("\n");
764
796
  if (this.lines.length === 0)
@@ -768,6 +800,53 @@ export class LineEditor {
768
800
  this.render();
769
801
  }
770
802
  // ---- completion / misc ----
803
+ /** Replace the UTF-16 range `[from, from + removeText.length)` on the
804
+ * current buffer line with `insertText`, placing the cursor right after. */
805
+ replaceCompletionToken(from, removeText, insertText) {
806
+ const line = this.lines[this.row];
807
+ const head = line.slice(0, from);
808
+ const tail = line.slice(from + removeText.length);
809
+ this.lines[this.row] = head + insertText + tail;
810
+ this.col = charLen(head) + charLen(insertText);
811
+ }
812
+ /** Position of `partial` under the cursor (fallback: last in the line), or
813
+ * -1 when not found. */
814
+ partialStart(line, partial) {
815
+ const chars = Array.from(line);
816
+ const beforeCursor = chars.slice(0, this.col).join("");
817
+ let from = beforeCursor.lastIndexOf(partial);
818
+ if (from === -1)
819
+ from = line.lastIndexOf(partial);
820
+ return from;
821
+ }
822
+ applyCompletionCandidate(index) {
823
+ const s = this.completionSession;
824
+ if (!s)
825
+ return;
826
+ const chosen = s.items[index].value;
827
+ this.replaceCompletionToken(s.from, s.inserted, chosen);
828
+ s.index = index;
829
+ s.inserted = chosen;
830
+ this.render();
831
+ }
832
+ cycleCompletion(delta) {
833
+ const s = this.completionSession;
834
+ if (!s)
835
+ return;
836
+ const n = s.items.length;
837
+ if (n === 0)
838
+ return;
839
+ const next = (s.index + delta + n) % n;
840
+ this.applyCompletionCandidate(next);
841
+ }
842
+ cancelCompletion() {
843
+ const s = this.completionSession;
844
+ if (!s)
845
+ return;
846
+ this.replaceCompletionToken(s.from, s.inserted, s.original);
847
+ this.completionSession = null;
848
+ this.render();
849
+ }
771
850
  tabComplete() {
772
851
  if (!this.completer)
773
852
  return;
@@ -778,32 +857,60 @@ export class LineEditor {
778
857
  return;
779
858
  }
780
859
  if (matches.length === 1) {
781
- // Complete the partial under the CURSOR, not the last occurrence in
782
- // the whole line — with the cursor mid-line, lastIndexOf() inserted
783
- // at the line's end instead.
784
- const chars = Array.from(line);
785
- const beforeCursor = chars.slice(0, this.col).join("");
786
- let from = beforeCursor.lastIndexOf(partial);
787
- if (from === -1)
788
- from = line.lastIndexOf(partial);
860
+ // Дополняем partial ПОД КУРСОРОМ, а не последнее вхождение в строке: при
861
+ // курсоре в середине lastIndexOf() вставлял в конец строки.
862
+ const from = this.partialStart(line, partial);
789
863
  if (from === -1) {
790
864
  this.output.write("\x07");
791
865
  return;
792
866
  }
793
- const head = line.slice(0, from);
794
- const tail = line.slice(from + partial.length);
795
- this.lines[this.row] = head + matches[0] + tail;
796
- this.col = charLen(head) + charLen(matches[0]);
867
+ this.replaceCompletionToken(from, partial, matches[0].value);
797
868
  this.render();
798
869
  return;
799
870
  }
800
- const rows = matches.map((m) => " " + m);
801
- this.output.write("\r\n" + rows.join("\r\n") + "\r\n");
802
- // The printed list moved the cursor (rows.length + 1) visual rows down
803
- // from its frame position. Account for those rows so the next render
804
- // redraws the frame in place — otherwise its relative up-move starts
805
- // above the frame and paints garbage over the just-printed list.
806
- this.prevCursorRow += rows.length + 1;
871
+ const from = this.partialStart(line, partial);
872
+ if (from === -1) {
873
+ this.output.write("\x07");
874
+ return;
875
+ }
876
+ const width = this.columns();
877
+ const limit = width - 1;
878
+ const rows = matches.map((m) => {
879
+ const plain = m.description ? ` ${m.value} ${m.description}` : ` ${m.value}`;
880
+ if (limit <= 0)
881
+ return "";
882
+ if (stringWidth(plain) <= limit) {
883
+ return m.description ? ` ${m.value} ${pc.dim(m.description)}` : ` ${m.value}`;
884
+ }
885
+ // Режем по видимым колонкам (одних кодпоинтов мало для CJK/emoji).
886
+ let acc = "";
887
+ let w = 0;
888
+ for (const ch of plain) {
889
+ const cw = stringWidth(ch);
890
+ if (w + cw > limit - 1)
891
+ break;
892
+ acc += ch;
893
+ w += cw;
894
+ }
895
+ return `${acc}…`;
896
+ });
897
+ this.output.write(`\r\n${rows.join("\r\n")}\r\n`);
898
+ // Напечатанный список увёл курсор ниже старого кадра. Сбрасываем абсолютный
899
+ // якорь, чтобы следующий рендер позиционировался ОТНОСИТЕЛЬНО текущего
900
+ // курсора (под списком). Сохранение DSR-якоря возвращало кадр на старую
901
+ // абсолютную строку, и его `\x1b[J` стирал список: на Linux/macOS кандидаты
902
+ // мигали и исчезали.
903
+ this.invalidateAnchor();
904
+ // Старт сессии циклирования с первым кандидатом в буфере:
905
+ // ↑/↓/Tab крутят напечатанный список на месте.
906
+ this.completionSession = {
907
+ items: matches,
908
+ index: 0,
909
+ from,
910
+ original: partial,
911
+ inserted: matches[0].value,
912
+ };
913
+ this.replaceCompletionToken(from, partial, matches[0].value);
807
914
  this.render();
808
915
  }
809
916
  clearScreen() {
@@ -811,7 +918,7 @@ export class LineEditor {
811
918
  this.invalidateAnchor();
812
919
  this.render();
813
920
  }
814
- // ---- rendering ----
921
+ // ── Рендеринг ──
815
922
  columns() {
816
923
  return this.output.columns || 80;
817
924
  }
@@ -819,28 +926,27 @@ export class LineEditor {
819
926
  const promptWidth = stringWidth(stripAnsi(this.promptStr));
820
927
  return buildLayout(this.lines, this.columns(), Math.max(1, this.columns() - promptWidth));
821
928
  }
822
- /** 1-based terminal height, or null when unknown (tests / non-TTY). */
929
+ /** 1-based высота терминала, null — неизвестна (тесты / не-TTY). */
823
930
  screenRows() {
824
931
  const r = this.output.rows;
825
932
  return typeof r === "number" && r > 3 ? r : null;
826
933
  }
827
934
  /**
828
- * Whether the cursor-position query (`ESC[6n`) may be sent. It is opt-in on
829
- * Windows: ConPTY can mis-decode the `ESC[row;colR` reply as typed input,
830
- * which corrupts answers (the setup wizard answered every prompt "R").
831
- * `MMA_DSR=1` forces it on, `MMA_DSR=0` off; elsewhere it defaults on.
935
+ * Можно ли слать запрос позиции курсора (`ESC[6n`). На Windows — opt-in:
936
+ * ConPTY может декодировать ответ `ESC[row;colR` как ввод, что портит ответы
937
+ * (визард настройки отвечал на каждый промпт "R").
938
+ * `MMA_DSR=1` форсит вкл, `MMA_DSR=0` — выкл; иначе по умолчанию вкл.
832
939
  */
833
940
  dsrEnabled() {
834
- if (process.env.MMA_DSR === "1")
941
+ if (readEnv("DSR") === "1")
835
942
  return true;
836
- if (process.env.MMA_DSR === "0")
943
+ if (readEnv("DSR") === "0")
837
944
  return false;
838
945
  return process.platform !== "win32";
839
946
  }
840
- /** Cap the rendered frame to the visible screen. A frame taller than the
841
- * terminal cannot be redrawn in place at all (its top scrolls off), so
842
- * only the tail is drawn; the absolute anchor keeps the next render
843
- * consistent regardless. */
947
+ /** Режет рисуемый кадр до видимого экрана. Кадр выше терминала нельзя
948
+ * перерисовать на месте (верх уехал в скролл), так что рисуется только хвост;
949
+ * абсолютный якорь держит следующий рендер консистентным в любом случае. */
844
950
  visibleLayout() {
845
951
  const layout = this.layout();
846
952
  const H = this.screenRows();
@@ -850,9 +956,9 @@ export class LineEditor {
850
956
  }
851
957
  return layout;
852
958
  }
853
- /** Forget the learned absolute frame position — the cursor has jumped to
854
- * an unknown location (external output, clear screen, fresh prompt).
855
- * The next render will re-query via DSR. */
959
+ /** Забывает выученную абсолютную позицию кадра: курсор прыгнул в неизвестное
960
+ * место (внешний вывод, чистка экрана, свежий промпт). Следующий рендер
961
+ * переспросит через DSR. */
856
962
  invalidateAnchor() {
857
963
  this.frameTopAbs = null;
858
964
  this.dsrPending = false;
@@ -863,10 +969,9 @@ export class LineEditor {
863
969
  this.dsrTimer = null;
864
970
  }
865
971
  }
866
- /** Parse `ESC[row;colR` cursor-position replies. Buffers partial escape
867
- * sequences across chunks. Accepts at most one reply (the outstanding
868
- * query); extras are silently dropped. Validates the reported row against
869
- * `screenRows()` to reject implausible ConPTY values. */
972
+ /** Парсит DSR-ответы `ESC[row;colR`. Буферит разрезанные escape между чанками.
973
+ * Принимает максимум один ответ (outstanding-запрос); лишние молча дропаются.
974
+ * Строку валидирует против `screenRows()`, отсекая неправдоподобные значения ConPTY. */
870
975
  consumeDsrReplies(chunk) {
871
976
  this.dsrTail = (this.dsrTail + chunk).slice(-80);
872
977
  const m = /\x1b\[(\d+);(\d+)R/.exec(this.dsrTail);
@@ -881,9 +986,9 @@ export class LineEditor {
881
986
  clearTimeout(this.dsrTimer);
882
987
  this.dsrTimer = null;
883
988
  }
884
- // The frame top is the reply row minus the cursor's visual row at query
885
- // time. `dsrQueryRow` — not `prevCursorRow` — because intermediate
886
- // renders may have advanced the cursor while the query was pending.
989
+ // Верх кадра = строка ответа минус визуальный ряд курсора на момент запроса.
990
+ // `dsrQueryRow`, а не `prevCursorRow`: промежуточные рендеры могли двинуть
991
+ // курсор, пока запрос летел.
887
992
  const top = row - this.dsrQueryRow;
888
993
  const H = this.screenRows();
889
994
  if (top >= 1 && (H === null || top <= H))
@@ -899,10 +1004,9 @@ export class LineEditor {
899
1004
  const rowsAfter = layout.length - idx - 1;
900
1005
  const ccol = (idx === 0 ? promptWidth : 0) + vcol;
901
1006
  const out = [];
902
- // Return to the frame's first row before redrawing. Absolute
903
- // positioning (learned via a single DSR query per prompt) is
904
- // scroll-proof; relative up-moves are the fallback until the
905
- // initial reply arrives.
1007
+ // Возврат на первую строку кадра перед перерисовкой. Абсолютное
1008
+ // позиционирование (выученное одним DSR-запросом на промпт) иммунно к
1009
+ // скроллу; относительные ходы вверх — фолбэк до прихода первого ответа.
906
1010
  if (this.frameTopAbs !== null) {
907
1011
  out.push(`\x1b[${this.frameTopAbs};1H`);
908
1012
  }
@@ -929,10 +1033,9 @@ export class LineEditor {
929
1033
  out.push(`\x1b[${ccol}C`);
930
1034
  this.output.write(out.join(""));
931
1035
  this.prevCursorRow = idx;
932
- // Deterministic scroll math: update the anchor for the NEXT render.
933
- // After writing L rows starting from frameTopAbs, if the frame extends
934
- // past the screen bottom the terminal scrolls; the new frame top is
935
- // computed locally — no further DSR queries needed.
1036
+ // Детерминированная скролл-математика: обновляем якорь для СЛЕДУЮЩЕГО рендера.
1037
+ // После записи L строк от frameTopAbs терминал скроллится, если кадр вылез за
1038
+ // низ экрана; новый верх кадра считается локально, новых DSR-запросов не надо.
936
1039
  if (this.frameTopAbs !== null) {
937
1040
  const H = this.screenRows();
938
1041
  if (H) {
@@ -944,9 +1047,9 @@ export class LineEditor {
944
1047
  }
945
1048
  }
946
1049
  else if (!this.dsrPending && this.questionCb === null && this.dsrEnabled()) {
947
- // One DSR query per frame, then the anchor is maintained by local scroll
948
- // math. Never queried in question mode (see dsrEnabled). A timeout clears
949
- // dsrPending so a non-responding terminal falls back to relative moves.
1050
+ // Один DSR-запрос на кадр, дальше якорь ведёт локальная скролл-математика.
1051
+ // В question-режиме не спрашиваем (см. dsrEnabled). Таймаут сбрасывает
1052
+ // dsrPending, чтобы неотвечающий терминал откатился на относительные ходы.
950
1053
  this.dsrPending = true;
951
1054
  this.dsrQueryRow = idx;
952
1055
  this.output.write("\x1b[6n");
@@ -970,6 +1073,11 @@ export class LineEditor {
970
1073
  this.listeners.line?.(line);
971
1074
  }
972
1075
  emitClose() {
1076
+ // Сброс финальной строки без перевода из чанкованного не-TTY ввода.
1077
+ if (this.pendingLine) {
1078
+ this.emitLine(this.pendingLine);
1079
+ this.pendingLine = "";
1080
+ }
973
1081
  this.listeners.close?.();
974
1082
  }
975
1083
  }