micro-models-agent 0.63.3 → 1.1.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 (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. package/package.json +1 -1
@@ -29,6 +29,12 @@ export class LineEditor {
29
29
  questionCb = null;
30
30
  rawMode = false;
31
31
  listeners = {};
32
+ /** stdin handlers registered in the constructor — removed in close() so
33
+ * re-creating the editor never accumulates stale listeners. */
34
+ dsrDataHandler;
35
+ keypressHandler;
36
+ dataHandler;
37
+ endHandler;
32
38
  /** Visual row (within the drawn frame) where the cursor was placed by the
33
39
  * last render — fallback for moving back to the frame's first row before
34
40
  * redrawing when no absolute anchor is known yet. Relative moves break
@@ -46,6 +52,18 @@ export class LineEditor {
46
52
  dsrPending = false;
47
53
  /** Buffer for partial DSR escape sequences split across data chunks. */
48
54
  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. */
58
+ dsrQueryRow = 0;
59
+ /** When false, keystrokes are ignored (except Ctrl+C) while the agent
60
+ * streams — the host owns the terminal during that phase. */
61
+ inputEnabled = true;
62
+ /** While true the prompt frame is not drawn; the agent owns the screen. */
63
+ frameSuspended = false;
64
+ /** Clears `dsrPending` when no DSR reply arrives, falling back to
65
+ * relative cursor moves instead of wedging on a broken terminal. */
66
+ dsrTimer = null;
49
67
  constructor(opts) {
50
68
  this.input = opts.input;
51
69
  this.output = opts.output;
@@ -65,13 +83,19 @@ export class LineEditor {
65
83
  // Raw data listener for DSR cursor-position replies. Runs alongside
66
84
  // readline's keypress emitter (both receive every chunk); replies may
67
85
  // arrive merged with regular keystrokes, hence the global regex.
68
- this.input.on("data", (buf) => this.consumeDsrReplies(buf.toString("utf-8")));
86
+ this.dsrDataHandler = (buf) => this.consumeDsrReplies(buf.toString("utf-8"));
87
+ this.input.on("data", this.dsrDataHandler);
69
88
  readline.emitKeypressEvents(this.input);
70
- this.input.on("keypress", (str, key) => this.onKey(str, key));
89
+ this.keypressHandler = (str, key) => this.onKey(str, key);
90
+ 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?.();
71
94
  }
72
95
  else {
73
- this.input.on("data", (buf) => this.onData(buf));
74
- this.input.on("end", () => {
96
+ this.dataHandler = (buf) => this.onData(buf);
97
+ this.input.on("data", this.dataHandler);
98
+ this.endHandler = () => {
75
99
  const last = this.lines.join("\n");
76
100
  this.lines = [""];
77
101
  this.row = 0;
@@ -79,7 +103,8 @@ export class LineEditor {
79
103
  if (last)
80
104
  this.emitLine(last);
81
105
  this.emitClose();
82
- });
106
+ };
107
+ this.input.on("end", this.endHandler);
83
108
  }
84
109
  }
85
110
  on(event, listener) {
@@ -103,6 +128,61 @@ export class LineEditor {
103
128
  write(s) {
104
129
  this.insertText(s);
105
130
  }
131
+ setInputEnabled(enabled) {
132
+ this.inputEnabled = enabled;
133
+ }
134
+ isInputEnabled() {
135
+ return this.inputEnabled;
136
+ }
137
+ isFrameSuspended() {
138
+ return this.frameSuspended;
139
+ }
140
+ /** Clear the prompt frame (the agent takes over the terminal) and stop
141
+ * rendering until `resumeFrame()`. */
142
+ suspendFrame() {
143
+ if (!this.input.isTTY || this.isDone)
144
+ return;
145
+ const out = [];
146
+ if (this.frameTopAbs !== null)
147
+ out.push(`\x1b[${this.frameTopAbs};1H`);
148
+ else if (this.prevCursorRow > 0)
149
+ out.push(`\x1b[${this.prevCursorRow}A`);
150
+ out.push("\r\x1b[J");
151
+ this.output.write(out.join(""));
152
+ this.invalidateAnchor();
153
+ this.frameSuspended = true;
154
+ }
155
+ /** Restore the frame after streaming. */
156
+ resumeFrame() {
157
+ this.frameSuspended = false;
158
+ this.invalidateAnchor();
159
+ this.render();
160
+ }
161
+ /** Print a block of lines above the current prompt frame without erasing
162
+ * the input buffer. When the frame is suspended the block is printed in
163
+ * place and the suspension is preserved. */
164
+ printAbove(text) {
165
+ if (!this.input.isTTY || this.isDone) {
166
+ this.output.write(text.endsWith("\n") ? text : `${text}\n`);
167
+ return;
168
+ }
169
+ const wasSuspended = this.frameSuspended;
170
+ this.frameSuspended = false;
171
+ const out = [];
172
+ if (this.frameTopAbs !== null)
173
+ out.push(`\x1b[${this.frameTopAbs};1H`);
174
+ else if (this.prevCursorRow > 0)
175
+ out.push(`\x1b[${this.prevCursorRow}A`);
176
+ out.push("\r\x1b[J");
177
+ out.push(text.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n").join("\r\n"));
178
+ out.push("\r\n");
179
+ this.output.write(out.join(""));
180
+ this.invalidateAnchor();
181
+ if (wasSuspended)
182
+ this.frameSuspended = true;
183
+ else
184
+ this.render();
185
+ }
106
186
  reset() {
107
187
  this.lines = [""];
108
188
  this.row = 0;
@@ -128,9 +208,22 @@ export class LineEditor {
128
208
  return;
129
209
  this.isDone = true;
130
210
  this.disableTerminalProtocols();
211
+ const inp = this.input;
212
+ if (this.dsrDataHandler)
213
+ inp.removeListener("data", this.dsrDataHandler);
214
+ if (this.keypressHandler)
215
+ inp.removeListener("keypress", this.keypressHandler);
216
+ if (this.dataHandler)
217
+ inp.removeListener("data", this.dataHandler);
218
+ if (this.endHandler)
219
+ inp.removeListener("end", this.endHandler);
131
220
  if (this.rawMode && this.input.setRawMode) {
132
221
  this.input.setRawMode(false);
133
222
  }
223
+ if (this.dsrTimer) {
224
+ clearTimeout(this.dsrTimer);
225
+ this.dsrTimer = null;
226
+ }
134
227
  this.emitClose();
135
228
  }
136
229
  // ---- input handling ----
@@ -158,6 +251,20 @@ export class LineEditor {
158
251
  onKey(str, key) {
159
252
  if (this.isDone)
160
253
  return;
254
+ 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.
257
+ const k = key;
258
+ if (k?.ctrl && k.name === "c") {
259
+ this.ctrlC();
260
+ return;
261
+ }
262
+ if (k?.name === "escape") {
263
+ this.onKeyInput?.(str, key);
264
+ return;
265
+ }
266
+ return;
267
+ }
161
268
  if (this.onKeyInput && this.onKeyInput(str, key))
162
269
  return;
163
270
  const k = key;
@@ -170,6 +277,12 @@ export class LineEditor {
170
277
  }
171
278
  return;
172
279
  }
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.
284
+ if (/^\x1b\[\d+;\d+R$/.test(seq))
285
+ return;
173
286
  if (name === "paste-start" || seq === "\x1b[200~") {
174
287
  this.isPaste = true;
175
288
  this.pasteBuffer = "";
@@ -665,7 +778,18 @@ export class LineEditor {
665
778
  return;
666
779
  }
667
780
  if (matches.length === 1) {
668
- const from = line.lastIndexOf(partial);
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);
789
+ if (from === -1) {
790
+ this.output.write("\x07");
791
+ return;
792
+ }
669
793
  const head = line.slice(0, from);
670
794
  const tail = line.slice(from + partial.length);
671
795
  this.lines[this.row] = head + matches[0] + tail;
@@ -673,9 +797,13 @@ export class LineEditor {
673
797
  this.render();
674
798
  return;
675
799
  }
676
- const lines = matches.map((m) => " " + m);
677
- this.output.write("\r\n" + lines.join("\r\n") + "\r\n");
678
- this.output.write(`\x1b[${lines.length + 1}A`);
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;
679
807
  this.render();
680
808
  }
681
809
  clearScreen() {
@@ -696,6 +824,19 @@ export class LineEditor {
696
824
  const r = this.output.rows;
697
825
  return typeof r === "number" && r > 3 ? r : null;
698
826
  }
827
+ /**
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.
832
+ */
833
+ dsrEnabled() {
834
+ if (process.env.MMA_DSR === "1")
835
+ return true;
836
+ if (process.env.MMA_DSR === "0")
837
+ return false;
838
+ return process.platform !== "win32";
839
+ }
699
840
  /** Cap the rendered frame to the visible screen. A frame taller than the
700
841
  * terminal cannot be redrawn in place at all (its top scrolls off), so
701
842
  * only the tail is drawn; the absolute anchor keeps the next render
@@ -717,6 +858,10 @@ export class LineEditor {
717
858
  this.dsrPending = false;
718
859
  this.dsrTail = "";
719
860
  this.prevCursorRow = 0;
861
+ if (this.dsrTimer) {
862
+ clearTimeout(this.dsrTimer);
863
+ this.dsrTimer = null;
864
+ }
720
865
  }
721
866
  /** Parse `ESC[row;colR` cursor-position replies. Buffers partial escape
722
867
  * sequences across chunks. Accepts at most one reply (the outstanding
@@ -732,17 +877,20 @@ export class LineEditor {
732
877
  return;
733
878
  this.dsrPending = false;
734
879
  const row = Number(m[1]);
735
- const idx = Number(m[2]);
736
- // idx is not the layout index — it's the column from the reply; ignore it.
737
- // We compute frameTop from the reply row minus the cursor's visual row
738
- // at query time. The visual row was stored in `prevCursorRow` when the
739
- // query was sent (right after render).
740
- const top = row - this.prevCursorRow;
741
- if (top >= 1)
880
+ if (this.dsrTimer) {
881
+ clearTimeout(this.dsrTimer);
882
+ this.dsrTimer = null;
883
+ }
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.
887
+ const top = row - this.dsrQueryRow;
888
+ const H = this.screenRows();
889
+ if (top >= 1 && (H === null || top <= H))
742
890
  this.frameTopAbs = top;
743
891
  }
744
892
  render() {
745
- if (!this.input.isTTY || this.isDone)
893
+ if (!this.input.isTTY || this.isDone || this.frameSuspended)
746
894
  return;
747
895
  const layout = this.visibleLayout();
748
896
  const promptWidth = stringWidth(stripAnsi(this.promptStr));
@@ -780,6 +928,7 @@ export class LineEditor {
780
928
  if (ccol > 0)
781
929
  out.push(`\x1b[${ccol}C`);
782
930
  this.output.write(out.join(""));
931
+ this.prevCursorRow = idx;
783
932
  // Deterministic scroll math: update the anchor for the NEXT render.
784
933
  // After writing L rows starting from frameTopAbs, if the frame extends
785
934
  // past the screen bottom the terminal scrolls; the new frame top is
@@ -794,16 +943,20 @@ export class LineEditor {
794
943
  // else: no scroll, anchor unchanged
795
944
  }
796
945
  }
797
- else if (!this.dsrPending) {
798
- // First render after prompt send exactly ONE DSR query.
799
- // Disabled for now: ConPTY may buffer stdout while processing
800
- // the DSR reply, hiding agent responses. Will revisit once
801
- // scroll-proof rendering is solved via deterministic tracking.
802
- //
803
- // this.dsrPending = true;
804
- // this.output.write("\x1b[6n");
946
+ 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.
950
+ this.dsrPending = true;
951
+ this.dsrQueryRow = idx;
952
+ this.output.write("\x1b[6n");
953
+ if (this.dsrTimer)
954
+ clearTimeout(this.dsrTimer);
955
+ this.dsrTimer = setTimeout(() => {
956
+ this.dsrPending = false;
957
+ this.dsrTimer = null;
958
+ }, 150);
805
959
  }
806
- this.prevCursorRow = idx;
807
960
  }
808
961
  enableTerminalProtocols() {
809
962
  this.output.write("\x1b[?2004h");
@@ -15,19 +15,36 @@ export function wrapLine(line, width, firstRowWidth) {
15
15
  const rows = [];
16
16
  let start = 0;
17
17
  let cells = 0;
18
+ let lastSpaceIdx = -1;
19
+ let lastSpaceCells = 0;
18
20
  for (let i = 0; i < chars.length; i++) {
19
21
  const ch = chars[i];
20
22
  if (ch === "\n") {
21
23
  rows.push({ text: chars.slice(start, i).join(""), charStart: start });
22
24
  start = i + 1;
23
25
  cells = 0;
26
+ lastSpaceIdx = -1;
27
+ lastSpaceCells = 0;
24
28
  continue;
25
29
  }
26
30
  const cw = stringWidth(ch);
31
+ if (ch === " ") {
32
+ lastSpaceIdx = i;
33
+ lastSpaceCells = cells;
34
+ }
27
35
  if (cells + cw > limit && cells > 0) {
28
- rows.push({ text: chars.slice(start, i).join(""), charStart: start });
29
- start = i;
30
- cells = cw;
36
+ if (lastSpaceIdx > start) {
37
+ rows.push({ text: chars.slice(start, lastSpaceIdx).join(""), charStart: start });
38
+ start = lastSpaceIdx + 1;
39
+ cells = Math.max(0, cells - lastSpaceCells - 1);
40
+ }
41
+ else {
42
+ rows.push({ text: chars.slice(start, i).join(""), charStart: start });
43
+ start = i;
44
+ cells = cw;
45
+ }
46
+ lastSpaceIdx = -1;
47
+ lastSpaceCells = 0;
31
48
  limit = w;
32
49
  }
33
50
  else {
@@ -1,4 +1,5 @@
1
1
  import { pc } from "./colors";
2
+ import stringWidth from "string-width";
2
3
  import { t } from "../i18n/index";
3
4
  import { formatTable, getTerminalWidth } from "./table";
4
5
  const KW = /\b(import|export|from|const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|class|extends|implements|interface|type|enum|namespace|module|declare|abstract|async|await|yield|throw|try|catch|finally|typeof|instanceof|in|of|as|is|keyof|readonly|static|private|public|protected|get|set|constructor|this|super|void|never|any|unknown|boolean|string|number|symbol|object|true|false|null|undefined|default)\b/g;
@@ -52,10 +53,25 @@ export class FormattingStream {
52
53
  codeLines = [];
53
54
  tableBuffer = [];
54
55
  onWrite;
56
+ onRawWrite;
55
57
  width;
56
- constructor(onWrite, width) {
58
+ /** Display columns of the raw partial currently shown on the terminal. */
59
+ partialWritten = 0;
60
+ /** How many chars of the pending line have already been streamed raw. */
61
+ streamedChars = 0;
62
+ /** Columns reserved on the left (caller prefix) so a partial never wraps. */
63
+ reservedCols;
64
+ /**
65
+ * Stream the current (not-yet-newline-terminated) line raw for live
66
+ * feedback. Off by default — offline callers use `formatMarkdown`.
67
+ */
68
+ streamPartials;
69
+ constructor(onWrite, width, onRawWrite, opts = {}) {
57
70
  this.onWrite = onWrite;
71
+ this.onRawWrite = onRawWrite ?? onWrite;
58
72
  this.width = width ?? getTerminalWidth();
73
+ this.reservedCols = opts.reservedCols ?? 0;
74
+ this.streamPartials = opts.streamPartials ?? false;
59
75
  }
60
76
  write(chunk) {
61
77
  this.buffer += chunk;
@@ -63,27 +79,101 @@ export class FormattingStream {
63
79
  while ((idx = this.buffer.indexOf("\n")) !== -1) {
64
80
  const line = this.buffer.slice(0, idx);
65
81
  this.buffer = this.buffer.slice(idx + 1);
82
+ this.emitPartialRewind();
66
83
  this.processLine(line);
67
84
  }
85
+ if (this.buffer.length > 0 && !this.inCodeBlock) {
86
+ this.streamPartial();
87
+ }
68
88
  }
69
- flush() {
70
- if (this.inCodeBlock && this.codeLines.length > 0) {
71
- this.emitCodeBlock();
89
+ /**
90
+ * Stream the pending line raw for real-time feedback; the formatted line
91
+ * replaces it once the newline arrives.
92
+ *
93
+ * The replacement uses a RELATIVE cursor move (`\x1b[<cols>D`), which is only
94
+ * correct while the streamed text stays on the single visual line it started
95
+ * on. If the partial would exceed the terminal width it wraps and every
96
+ * relative move lands in the wrong place — the garbled output observed after
97
+ * the 1.0.0 output refactor (missing/swapped characters, leaked markers,
98
+ * broken tables). So streaming freezes at the last chunk that fits and the
99
+ * line is rendered when it completes.
100
+ */
101
+ streamPartial() {
102
+ if (!this.streamPartials)
103
+ return;
104
+ const available = Math.max(1, this.width - this.reservedCols);
105
+ if (stringWidth(this.buffer) > available)
106
+ return;
107
+ const delta = this.buffer.slice(this.streamedChars);
108
+ if (!delta)
109
+ return;
110
+ this.onRawWrite(delta);
111
+ this.streamedChars = this.buffer.length;
112
+ this.partialWritten = stringWidth(this.buffer);
113
+ }
114
+ emitPartialRewind() {
115
+ if (this.partialWritten > 0) {
116
+ // Move back over the streamed partial and erase any tail left by a
117
+ // shorter formatted replacement before the caller renders the line.
118
+ this.onRawWrite(`\x1b[${this.partialWritten}D\x1b[K`);
119
+ this.partialWritten = 0;
120
+ }
121
+ this.streamedChars = 0;
122
+ }
123
+ /** True when there is displayed partial output or unprocessed buffered text. @internal */
124
+ hasPartial() {
125
+ return this.partialWritten > 0 || this.buffer.length > 0 || this.tableBuffer.length > 0;
126
+ }
127
+ /**
128
+ * Finalize a displayed raw partial so the next write starts on a fresh line.
129
+ * Only the already-streamed partial is terminated (with a single newline via
130
+ * `onRawWrite`); any pending table buffer is flushed. An open code block is
131
+ * intentionally left untouched — closing it here would make the real closing
132
+ * fence look like a new opening fence and corrupt all following prose. The
133
+ * code block is emitted by the final `flush()`. Idempotent.
134
+ */
135
+ flushPartialNow() {
136
+ if (this.partialWritten > 0) {
137
+ // The line is already on screen raw; terminate it as-is (it may be
138
+ // interrupted mid-markdown) and drop it so it is not rendered twice.
139
+ this.onRawWrite("\n");
140
+ this.partialWritten = 0;
141
+ this.streamedChars = 0;
142
+ this.buffer = "";
143
+ }
144
+ else if (this.buffer.length > 0 && !this.inCodeBlock) {
145
+ // Never streamed (plain mode) — render the pending line now.
146
+ const line = this.buffer;
147
+ this.buffer = "";
148
+ this.streamedChars = 0;
149
+ this.onWrite(this.formatLine(line));
72
150
  }
73
151
  if (this.tableBuffer.length > 0) {
74
152
  this.flushTable();
75
153
  }
154
+ }
155
+ flush() {
156
+ if (this.tableBuffer.length > 0) {
157
+ this.flushTable();
158
+ }
159
+ if (this.inCodeBlock) {
160
+ if (this.buffer.length > 0) {
161
+ this.codeLines.push(this.buffer);
162
+ this.buffer = "";
163
+ }
164
+ if (this.codeLines.length > 0) {
165
+ this.emitCodeBlock();
166
+ }
167
+ this.streamedChars = 0;
168
+ return;
169
+ }
76
170
  if (this.buffer.length > 0) {
77
171
  const line = this.buffer;
78
172
  this.buffer = "";
79
- if (line.trim() === "```" || line.trim().startsWith("```")) {
80
- if (this.codeLines.length > 0) {
81
- this.emitCodeBlock();
82
- }
83
- return;
84
- }
173
+ this.emitPartialRewind();
85
174
  this.onWrite(this.formatLine(line));
86
175
  }
176
+ this.streamedChars = 0;
87
177
  }
88
178
  processLine(line) {
89
179
  if (line.trim() === "```" || line.trim().startsWith("```")) {
package/dist/ui/output.js CHANGED
@@ -1,13 +1,14 @@
1
+ import { getDefaultChannel } from "../output";
1
2
  import { formatMarkdown, formatError, formatSuccess, formatWarning } from "./md-formatter";
2
3
  export function writeOutput(text) {
3
- process.stdout.write(formatMarkdown(text) + "\n");
4
+ getDefaultChannel().writeRaw(formatMarkdown(text) + "\n");
4
5
  }
5
6
  export function writeError(text) {
6
- process.stderr.write(formatError(text) + "\n");
7
+ getDefaultChannel().writeRaw(formatError(text) + "\n", "stderr");
7
8
  }
8
9
  export function writeSuccess(text) {
9
- process.stdout.write(formatSuccess(text) + "\n");
10
+ getDefaultChannel().writeRaw(formatSuccess(text) + "\n");
10
11
  }
11
12
  export function writeWarning(text) {
12
- process.stdout.write(formatWarning(text) + "\n");
13
+ getDefaultChannel().writeLine(formatWarning(text));
13
14
  }
@@ -28,8 +28,7 @@ export function readActivePlan(baseDir) {
28
28
  }
29
29
  }
30
30
  export function currentStepIndex(plan) {
31
- const idx = plan.steps.findIndex((s) => s.status !== "done" && s.status !== "skipped");
32
- return idx === -1 ? 0 : idx;
31
+ return plan.steps.findIndex((s) => s.status !== "done" && s.status !== "skipped");
33
32
  }
34
33
  function stepMark(step) {
35
34
  switch (step.status) {
@@ -95,9 +94,5 @@ export function stepContextForTool(plan, tool, args) {
95
94
  }
96
95
  }
97
96
  }
98
- const cur = currentStepIndex(plan);
99
- const step = plan.steps[cur];
100
- if (!step)
101
- return null;
102
- return t("ui.step_context", { id: step.id, desc: truncate(step.description) });
97
+ return null;
103
98
  }