@elench/testkit 0.1.113 → 0.1.115

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 (196) hide show
  1. package/lib/cli/assistant/app.mjs +4 -2
  2. package/lib/cli/assistant/session.mjs +5 -1
  3. package/lib/cli/assistant/state.mjs +1 -2
  4. package/lib/cli/components/blocks/run-tree.mjs +7 -2
  5. package/lib/cli/components/hooks/use-element-layout.mjs +63 -0
  6. package/lib/cli/components/hooks/use-spinner-frame.mjs +26 -0
  7. package/lib/playwright/index.d.ts +1 -0
  8. package/lib/playwright/index.mjs +1 -0
  9. package/lib/runner/default-runtime-runner.mjs +5 -28
  10. package/lib/runner/lifecycle.mjs +2 -51
  11. package/lib/runner/managed-processes.mjs +2 -1
  12. package/lib/runner/playwright-config.mjs +13 -1
  13. package/lib/runner/playwright-runner.mjs +85 -15
  14. package/lib/runner/processes.mjs +59 -3
  15. package/lib/runner/subprocess.mjs +155 -0
  16. package/lib/shared/file-timeout.mjs +1 -1
  17. package/lib/ui/index.d.ts +2 -0
  18. package/lib/ui/index.mjs +1 -0
  19. package/node_modules/@alcalzone/ansi-tokenize/README.md +0 -5
  20. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.d.ts +8 -0
  21. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js +10 -8
  22. package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js.map +1 -1
  23. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.d.ts +1 -5
  24. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js +9 -45
  25. package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js.map +1 -1
  26. package/node_modules/@alcalzone/ansi-tokenize/package.json +1 -1
  27. package/node_modules/@elench/next-analysis/package.json +1 -1
  28. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  29. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  30. package/node_modules/@elench/ts-analysis/package.json +1 -1
  31. package/node_modules/cli-boxes/index.d.ts +95 -90
  32. package/node_modules/cli-boxes/index.js +5 -2
  33. package/node_modules/cli-boxes/package.json +6 -13
  34. package/node_modules/cli-boxes/readme.md +15 -3
  35. package/node_modules/cli-truncate/index.d.ts +1 -1
  36. package/node_modules/cli-truncate/package.json +4 -4
  37. package/node_modules/cli-truncate/readme.md +1 -0
  38. package/node_modules/ink/build/apply-styles.js +175 -0
  39. package/node_modules/ink/build/build-layout.js +77 -0
  40. package/node_modules/ink/build/calculate-wrapped-text.js +53 -0
  41. package/node_modules/ink/build/components/App.d.ts +1 -4
  42. package/node_modules/ink/build/components/App.js +22 -142
  43. package/node_modules/ink/build/components/App.js.map +1 -1
  44. package/node_modules/ink/build/components/AppContext.d.ts +3 -23
  45. package/node_modules/ink/build/components/AppContext.js +4 -7
  46. package/node_modules/ink/build/components/AppContext.js.map +1 -1
  47. package/node_modules/ink/build/components/Box.d.ts +3 -16
  48. package/node_modules/ink/build/components/Color.js +62 -0
  49. package/node_modules/ink/build/components/Cursor.d.ts +83 -0
  50. package/node_modules/ink/build/components/Cursor.js +53 -0
  51. package/node_modules/ink/build/components/Cursor.js.map +1 -0
  52. package/node_modules/ink/build/components/ErrorBoundary.d.ts +2 -2
  53. package/node_modules/ink/build/components/ErrorOverview.js +6 -6
  54. package/node_modules/ink/build/components/ErrorOverview.js.map +1 -1
  55. package/node_modules/ink/build/components/Static.js.map +1 -1
  56. package/node_modules/ink/build/components/StdinContext.d.ts +1 -7
  57. package/node_modules/ink/build/components/StdinContext.js +0 -1
  58. package/node_modules/ink/build/components/StdinContext.js.map +1 -1
  59. package/node_modules/ink/build/components/Text.d.ts +1 -1
  60. package/node_modules/ink/build/components/Text.js +1 -1
  61. package/node_modules/ink/build/components/Text.js.map +1 -1
  62. package/node_modules/ink/build/components/Transform.d.ts +1 -1
  63. package/node_modules/ink/build/devtools-window-polyfill.js +4 -7
  64. package/node_modules/ink/build/devtools-window-polyfill.js.map +1 -1
  65. package/node_modules/ink/build/devtools.js +6 -31
  66. package/node_modules/ink/build/devtools.js.map +1 -1
  67. package/node_modules/ink/build/dom.d.ts +1 -5
  68. package/node_modules/ink/build/dom.js +1 -20
  69. package/node_modules/ink/build/dom.js.map +1 -1
  70. package/node_modules/ink/build/experimental/apply-style.js +140 -0
  71. package/node_modules/ink/build/experimental/dom.js +123 -0
  72. package/node_modules/ink/build/experimental/output.js +91 -0
  73. package/node_modules/ink/build/experimental/reconciler.js +141 -0
  74. package/node_modules/ink/build/experimental/renderer.js +81 -0
  75. package/node_modules/ink/build/hooks/use-app.d.ts +1 -1
  76. package/node_modules/ink/build/hooks/use-app.js +1 -1
  77. package/node_modules/ink/build/hooks/use-cursor.d.ts +1 -1
  78. package/node_modules/ink/build/hooks/use-cursor.js +1 -1
  79. package/node_modules/ink/build/hooks/use-focus-manager.d.ts +2 -17
  80. package/node_modules/ink/build/hooks/use-focus-manager.js +1 -2
  81. package/node_modules/ink/build/hooks/use-focus-manager.js.map +1 -1
  82. package/node_modules/ink/build/hooks/use-focus.d.ts +1 -2
  83. package/node_modules/ink/build/hooks/use-focus.js +4 -5
  84. package/node_modules/ink/build/hooks/use-focus.js.map +1 -1
  85. package/node_modules/ink/build/hooks/use-input.d.ts +1 -2
  86. package/node_modules/ink/build/hooks/use-input.js +80 -82
  87. package/node_modules/ink/build/hooks/use-input.js.map +1 -1
  88. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.d.ts +1 -2
  89. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js +1 -2
  90. package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
  91. package/node_modules/ink/build/hooks/use-stderr.d.ts +1 -1
  92. package/node_modules/ink/build/hooks/use-stderr.js +1 -1
  93. package/node_modules/ink/build/hooks/use-stdin.d.ts +2 -4
  94. package/node_modules/ink/build/hooks/use-stdin.js +1 -2
  95. package/node_modules/ink/build/hooks/use-stdin.js.map +1 -1
  96. package/node_modules/ink/build/hooks/use-stdout.d.ts +1 -1
  97. package/node_modules/ink/build/hooks/use-stdout.js +1 -1
  98. package/node_modules/ink/build/hooks/useInput.js +38 -0
  99. package/node_modules/ink/build/index.d.ts +1 -8
  100. package/node_modules/ink/build/index.js +0 -4
  101. package/node_modules/ink/build/index.js.map +1 -1
  102. package/node_modules/ink/build/ink.d.ts +3 -48
  103. package/node_modules/ink/build/ink.js +155 -325
  104. package/node_modules/ink/build/ink.js.map +1 -1
  105. package/node_modules/ink/build/input-parser.d.ts +1 -4
  106. package/node_modules/ink/build/input-parser.js +30 -70
  107. package/node_modules/ink/build/input-parser.js.map +1 -1
  108. package/node_modules/ink/build/instance.js +205 -0
  109. package/node_modules/ink/build/layout.d.ts +7 -0
  110. package/node_modules/ink/build/layout.js +33 -0
  111. package/node_modules/ink/build/layout.js.map +1 -0
  112. package/node_modules/ink/build/log-update.d.ts +0 -1
  113. package/node_modules/ink/build/log-update.js +1 -13
  114. package/node_modules/ink/build/log-update.js.map +1 -1
  115. package/node_modules/ink/build/measure-element.d.ts +0 -4
  116. package/node_modules/ink/build/measure-element.js +0 -4
  117. package/node_modules/ink/build/measure-element.js.map +1 -1
  118. package/node_modules/ink/build/options.d.ts +52 -0
  119. package/node_modules/ink/build/options.js +2 -0
  120. package/node_modules/ink/build/options.js.map +1 -0
  121. package/node_modules/ink/build/output.js +0 -25
  122. package/node_modules/ink/build/output.js.map +1 -1
  123. package/node_modules/ink/build/parse-keypress.d.ts +3 -1
  124. package/node_modules/ink/build/parse-keypress.js +17 -19
  125. package/node_modules/ink/build/parse-keypress.js.map +1 -1
  126. package/node_modules/ink/build/reconciler.js +27 -46
  127. package/node_modules/ink/build/reconciler.js.map +1 -1
  128. package/node_modules/ink/build/render-border.js +18 -29
  129. package/node_modules/ink/build/render-border.js.map +1 -1
  130. package/node_modules/ink/build/render-to-string.js +1 -2
  131. package/node_modules/ink/build/render-to-string.js.map +1 -1
  132. package/node_modules/ink/build/render.d.ts +2 -57
  133. package/node_modules/ink/build/render.js +11 -18
  134. package/node_modules/ink/build/render.js.map +1 -1
  135. package/node_modules/ink/build/screen-reader-update.d.ts +13 -0
  136. package/node_modules/ink/build/screen-reader-update.js +38 -0
  137. package/node_modules/ink/build/screen-reader-update.js.map +1 -0
  138. package/node_modules/ink/build/styles.d.ts +16 -78
  139. package/node_modules/ink/build/styles.js +31 -102
  140. package/node_modules/ink/build/styles.js.map +1 -1
  141. package/node_modules/ink/build/utils.d.ts +2 -9
  142. package/node_modules/ink/build/utils.js +3 -18
  143. package/node_modules/ink/build/utils.js.map +1 -1
  144. package/node_modules/ink/build/wrap-text.js +0 -7
  145. package/node_modules/ink/build/wrap-text.js.map +1 -1
  146. package/node_modules/ink/build/write-synchronized.d.ts +1 -1
  147. package/node_modules/ink/build/write-synchronized.js +2 -4
  148. package/node_modules/ink/build/write-synchronized.js.map +1 -1
  149. package/node_modules/ink/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
  150. package/node_modules/ink/node_modules/emoji-regex/README.md +107 -0
  151. package/node_modules/ink/node_modules/emoji-regex/index.d.ts +3 -0
  152. package/node_modules/ink/node_modules/emoji-regex/index.js +4 -0
  153. package/node_modules/ink/node_modules/emoji-regex/index.mjs +4 -0
  154. package/node_modules/ink/node_modules/emoji-regex/package.json +45 -0
  155. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/index.d.ts +1 -1
  156. package/node_modules/ink/node_modules/wrap-ansi/index.js +222 -0
  157. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +39 -0
  158. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.js +82 -0
  159. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/license +9 -0
  160. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/package.json +64 -0
  161. package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/readme.md +66 -0
  162. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/package.json +11 -11
  163. package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/readme.md +0 -2
  164. package/node_modules/ink/package.json +98 -34
  165. package/node_modules/ink/readme.md +48 -554
  166. package/node_modules/slice-ansi/index.d.ts +1 -1
  167. package/node_modules/slice-ansi/index.js +89 -146
  168. package/node_modules/slice-ansi/package.json +5 -5
  169. package/node_modules/slice-ansi/readme.md +0 -1
  170. package/node_modules/slice-ansi/tokenize-ansi.js +1 -1
  171. package/package.json +11 -10
  172. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
  173. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
  174. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
  175. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
  176. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
  177. package/node_modules/@alcalzone/ansi-tokenize/build/consts.d.ts +0 -17
  178. package/node_modules/@alcalzone/ansi-tokenize/build/consts.js +0 -28
  179. package/node_modules/@alcalzone/ansi-tokenize/build/consts.js.map +0 -1
  180. package/node_modules/ink/build/components/AnimationContext.d.ts +0 -9
  181. package/node_modules/ink/build/components/AnimationContext.js +0 -13
  182. package/node_modules/ink/build/components/AnimationContext.js.map +0 -1
  183. package/node_modules/ink/build/hooks/use-animation.d.ts +0 -49
  184. package/node_modules/ink/build/hooks/use-animation.js +0 -87
  185. package/node_modules/ink/build/hooks/use-animation.js.map +0 -1
  186. package/node_modules/ink/build/hooks/use-box-metrics.d.ts +0 -59
  187. package/node_modules/ink/build/hooks/use-box-metrics.js +0 -88
  188. package/node_modules/ink/build/hooks/use-box-metrics.js.map +0 -1
  189. package/node_modules/ink/build/hooks/use-paste.d.ts +0 -35
  190. package/node_modules/ink/build/hooks/use-paste.js +0 -62
  191. package/node_modules/ink/build/hooks/use-paste.js.map +0 -1
  192. package/node_modules/ink/build/hooks/use-window-size.d.ts +0 -18
  193. package/node_modules/ink/build/hooks/use-window-size.js +0 -22
  194. package/node_modules/ink/build/hooks/use-window-size.js.map +0 -1
  195. package/node_modules/wrap-ansi/index.js +0 -468
  196. /package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/license +0 -0
@@ -9,11 +9,11 @@ import patchConsole from 'patch-console';
9
9
  import { LegacyRoot, ConcurrentRoot } from 'react-reconciler/constants.js';
10
10
  import Yoga from 'yoga-layout';
11
11
  import wrapAnsi from 'wrap-ansi';
12
- import { getWindowSize } from './utils.js';
12
+ import terminalSize from 'terminal-size';
13
+ import { isDev } from './utils.js';
13
14
  import reconciler from './reconciler.js';
14
15
  import render from './renderer.js';
15
16
  import * as dom from './dom.js';
16
- import { hideCursorEscape, showCursorEscape } from './cursor-helpers.js';
17
17
  import logUpdate from './log-update.js';
18
18
  import { bsu, esu, shouldSynchronize } from './write-synchronized.js';
19
19
  import instances from './instances.js';
@@ -21,10 +21,6 @@ import App from './components/App.js';
21
21
  import { accessibilityContext as AccessibilityContext } from './components/AccessibilityContext.js';
22
22
  import { resolveFlags, } from './kitty-keyboard.js';
23
23
  const noop = () => { };
24
- const textEncoder = new TextEncoder();
25
- const yieldImmediate = async () => new Promise(resolve => {
26
- setImmediate(resolve);
27
- });
28
24
  const kittyQueryEscapeByte = 0x1b;
29
25
  const kittyQueryOpenBracketByte = 0x5b;
30
26
  const kittyQueryQuestionMarkByte = 0x3f;
@@ -80,51 +76,10 @@ const stripKittyQueryResponsesAndTrailingPartial = (buffer) => {
80
76
  }
81
77
  return keptBytes;
82
78
  };
83
- const shouldClearTerminalForFrame = ({ isTty, viewportRows, previousOutputHeight, nextOutputHeight, isUnmounting, }) => {
84
- if (!isTty) {
85
- return false;
86
- }
87
- const hadPreviousFrame = previousOutputHeight > 0;
88
- const wasFullscreen = previousOutputHeight >= viewportRows;
89
- const wasOverflowing = previousOutputHeight > viewportRows;
90
- const isOverflowing = nextOutputHeight > viewportRows;
91
- const isLeavingFullscreen = wasFullscreen && nextOutputHeight < viewportRows;
92
- const shouldClearOnUnmount = isUnmounting && wasFullscreen;
93
- return (
94
- // Overflowing frames still need full clear fallback.
95
- wasOverflowing ||
96
- (isOverflowing && hadPreviousFrame) ||
97
- // Clear when shrinking from fullscreen to non-fullscreen output.
98
- isLeavingFullscreen ||
99
- // Preserve legacy unmount behavior for fullscreen frames: final teardown
100
- // render should clear once to avoid leaving a scrolled viewport state.
101
- shouldClearOnUnmount);
102
- };
103
79
  const isErrorInput = (value) => {
104
80
  return (value instanceof Error ||
105
81
  Object.prototype.toString.call(value) === '[object Error]');
106
82
  };
107
- const getWritableStreamState = (stdout) => {
108
- const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);
109
- const hasWritableState = stdout._writableState !== undefined || stdout.writableLength !== undefined;
110
- return {
111
- canWriteToStdout,
112
- hasWritableState,
113
- };
114
- };
115
- const settleThrottle = (throttled, canWriteToStdout) => {
116
- if (!throttled ||
117
- typeof throttled.flush !== 'function') {
118
- return;
119
- }
120
- const throttledValue = throttled;
121
- if (canWriteToStdout) {
122
- throttledValue.flush();
123
- }
124
- else if (typeof throttledValue.cancel === 'function') {
125
- throttledValue.cancel();
126
- }
127
- };
128
83
  export default class Ink {
129
84
  /**
130
85
  Whether this instance is using concurrent rendering mode.
@@ -135,9 +90,6 @@ export default class Ink {
135
90
  cursorPosition;
136
91
  throttledLog;
137
92
  isScreenReaderEnabled;
138
- interactive;
139
- renderThrottleMs;
140
- alternateScreen;
141
93
  // Ignore last render after unmounting a tree to prevent empty output before exit
142
94
  isUnmounted;
143
95
  isUnmounting;
@@ -159,7 +111,6 @@ export default class Ink {
159
111
  hasPendingThrottledRender = false;
160
112
  kittyProtocolEnabled = false;
161
113
  cancelKittyDetection;
162
- nextRenderCommit;
163
114
  constructor(options) {
164
115
  autoBind(this);
165
116
  this.options = options;
@@ -168,18 +119,9 @@ export default class Ink {
168
119
  this.isScreenReaderEnabled =
169
120
  options.isScreenReaderEnabled ??
170
121
  process.env['INK_SCREEN_READER'] === 'true';
171
- // CI detection takes precedence: even a TTY stdout in CI defaults to non-interactive.
172
- // Using Boolean(isTTY) (rather than an 'in' guard) correctly handles piped streams
173
- // where the property is absent (e.g. `node app.js | cat`).
174
- this.interactive = this.resolveInteractiveOption(options.interactive);
175
- this.alternateScreen = false;
176
122
  const unthrottled = options.debug || this.isScreenReaderEnabled;
177
123
  const maxFps = options.maxFps ?? 30;
178
- // Treat non-positive maxFps as an internal fallback case, not a supported
179
- // "disable throttling" mode. Keep animation scheduling on a normal cadence
180
- // so future changes don't accidentally reintroduce zero-delay loops.
181
124
  const renderThrottleMs = maxFps > 0 ? Math.max(1, Math.ceil(1000 / maxFps)) : 0;
182
- this.renderThrottleMs = unthrottled ? 0 : renderThrottleMs;
183
125
  if (unthrottled) {
184
126
  this.rootNode.onRender = this.onRender;
185
127
  this.throttledOnRender = undefined;
@@ -204,7 +146,7 @@ export default class Ink {
204
146
  ? this.log
205
147
  : throttle((output) => {
206
148
  const shouldWrite = this.log.willRender(output);
207
- const sync = this.shouldSync();
149
+ const sync = shouldSynchronize(this.options.stdout);
208
150
  if (sync && shouldWrite) {
209
151
  this.options.stdout.write(bsu);
210
152
  }
@@ -225,7 +167,7 @@ export default class Ink {
225
167
  this.lastOutput = '';
226
168
  this.lastOutputToRender = '';
227
169
  this.lastOutputHeight = 0;
228
- this.lastTerminalWidth = getWindowSize(this.options.stdout).columns;
170
+ this.lastTerminalWidth = this.getTerminalWidth();
229
171
  // This variable is used only in debug mode to store full static output
230
172
  // so that it's rerendered every time, not just new static parts, like in non-debug mode
231
173
  this.fullStaticOutput = '';
@@ -235,31 +177,32 @@ export default class Ink {
235
177
  this.container = reconciler.createContainer(this.rootNode, rootTag, null, false, null, 'id', () => { }, () => { }, () => { }, () => { });
236
178
  // Unmount when process exits
237
179
  this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false });
238
- this.setAlternateScreen(Boolean(options.alternateScreen));
239
- if (process.env['DEV'] === 'true') {
180
+ if (isDev()) {
240
181
  // @ts-expect-error outdated types
241
182
  reconciler.injectIntoDevTools();
242
183
  }
243
184
  if (options.patchConsole) {
244
185
  this.patchConsole();
245
186
  }
246
- if (this.interactive) {
187
+ if (!isInCi) {
247
188
  options.stdout.on('resize', this.resized);
248
189
  this.unsubscribeResize = () => {
249
190
  options.stdout.off('resize', this.resized);
250
191
  };
251
192
  }
252
193
  this.initKittyKeyboard();
253
- this.exitPromise = new Promise((resolve, reject) => {
254
- this.resolveExitPromise = resolve;
255
- this.rejectExitPromise = reject;
256
- });
257
- // Prevent global unhandled-rejection crashes when app code exits with an
258
- // error but consumers never call waitUntilExit().
259
- void this.exitPromise.catch(noop);
260
194
  }
195
+ getTerminalWidth = () => {
196
+ // The 'columns' property can be undefined or 0 when not using a TTY.
197
+ // Use terminal-size as a fallback for piped processes, then default to 80.
198
+ if (this.options.stdout.columns) {
199
+ return this.options.stdout.columns;
200
+ }
201
+ const size = terminalSize();
202
+ return size?.columns ?? 80;
203
+ };
261
204
  resized = () => {
262
- const currentWidth = getWindowSize(this.options.stdout).columns;
205
+ const currentWidth = this.getTerminalWidth();
263
206
  if (currentWidth < this.lastTerminalWidth) {
264
207
  // We clear the screen when decreasing terminal width to prevent duplicate overlapping re-renders.
265
208
  this.log.clear();
@@ -289,16 +232,13 @@ export default class Ink {
289
232
  this.log.setCursorPosition(position);
290
233
  };
291
234
  restoreLastOutput = () => {
292
- if (!this.interactive) {
293
- return;
294
- }
295
235
  // Clear() resets log-update's cursor state, so replay the latest cursor intent
296
236
  // before restoring output after external stdout/stderr writes.
297
237
  this.log.setCursorPosition(this.cursorPosition);
298
238
  this.log(this.lastOutputToRender || this.lastOutput + '\n');
299
239
  };
300
240
  calculateLayout = () => {
301
- const terminalWidth = getWindowSize(this.options.stdout).columns;
241
+ const terminalWidth = this.getTerminalWidth();
302
242
  this.rootNode.yogaNode.setWidth(terminalWidth);
303
243
  this.rootNode.yogaNode.calculateLayout(undefined, undefined, Yoga.DIRECTION_LTR);
304
244
  };
@@ -307,10 +247,6 @@ export default class Ink {
307
247
  if (this.isUnmounted) {
308
248
  return;
309
249
  }
310
- if (this.nextRenderCommit) {
311
- this.nextRenderCommit.resolve();
312
- this.nextRenderCommit = undefined;
313
- }
314
250
  const startTime = performance.now();
315
251
  const { output, outputHeight, staticOutput } = render(this.rootNode, this.isScreenReaderEnabled);
316
252
  this.options.onRender?.({ renderTime: performance.now() - startTime });
@@ -320,13 +256,10 @@ export default class Ink {
320
256
  if (hasStaticOutput) {
321
257
  this.fullStaticOutput += staticOutput;
322
258
  }
323
- this.lastOutput = output;
324
- this.lastOutputToRender = output;
325
- this.lastOutputHeight = outputHeight;
326
259
  this.options.stdout.write(this.fullStaticOutput + output);
327
260
  return;
328
261
  }
329
- if (!this.interactive) {
262
+ if (isInCi) {
330
263
  if (hasStaticOutput) {
331
264
  this.options.stdout.write(staticOutput);
332
265
  }
@@ -336,7 +269,7 @@ export default class Ink {
336
269
  return;
337
270
  }
338
271
  if (this.isScreenReaderEnabled) {
339
- const sync = this.shouldSync();
272
+ const sync = shouldSynchronize(this.options.stdout);
340
273
  if (sync) {
341
274
  this.options.stdout.write(bsu);
342
275
  }
@@ -355,7 +288,7 @@ export default class Ink {
355
288
  }
356
289
  return;
357
290
  }
358
- const terminalWidth = getWindowSize(this.options.stdout).columns;
291
+ const terminalWidth = this.getTerminalWidth();
359
292
  const wrappedOutput = wrapAnsi(output, terminalWidth, {
360
293
  trim: false,
361
294
  hard: true,
@@ -382,11 +315,49 @@ export default class Ink {
382
315
  if (hasStaticOutput) {
383
316
  this.fullStaticOutput += staticOutput;
384
317
  }
385
- this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '');
318
+ // Detect fullscreen: output fills or exceeds terminal height.
319
+ // Only apply when writing to a real TTY — piped output always gets trailing newlines.
320
+ const isFullscreen = this.options.stdout.isTTY && outputHeight >= this.options.stdout.rows;
321
+ const outputToRender = isFullscreen ? output : output + '\n';
322
+ if (this.lastOutputHeight >= this.options.stdout.rows) {
323
+ const sync = shouldSynchronize(this.options.stdout);
324
+ if (sync) {
325
+ this.options.stdout.write(bsu);
326
+ }
327
+ this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
328
+ this.lastOutput = output;
329
+ this.lastOutputToRender = outputToRender;
330
+ this.lastOutputHeight = outputHeight;
331
+ this.log.sync(outputToRender);
332
+ if (sync) {
333
+ this.options.stdout.write(esu);
334
+ }
335
+ return;
336
+ }
337
+ // To ensure static output is cleanly rendered before main output, clear main output first
338
+ if (hasStaticOutput) {
339
+ const sync = shouldSynchronize(this.options.stdout);
340
+ if (sync) {
341
+ this.options.stdout.write(bsu);
342
+ }
343
+ this.log.clear();
344
+ this.options.stdout.write(staticOutput);
345
+ this.log(outputToRender);
346
+ if (sync) {
347
+ this.options.stdout.write(esu);
348
+ }
349
+ }
350
+ else if (output !== this.lastOutput || this.log.isCursorDirty()) {
351
+ // ThrottledLog manages its own bsu/esu at actual write time
352
+ this.throttledLog(outputToRender);
353
+ }
354
+ this.lastOutput = output;
355
+ this.lastOutputToRender = outputToRender;
356
+ this.lastOutputHeight = outputHeight;
386
357
  };
387
358
  render(node) {
388
359
  const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
389
- React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, interactive: this.interactive, renderThrottleMs: this.renderThrottleMs, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, onExit: this.handleAppExit, onWaitUntilRenderFlush: this.waitUntilRenderFlush }, node)));
360
+ React.createElement(App, { stdin: this.options.stdin, stdout: this.options.stdout, stderr: this.options.stderr, exitOnCtrlC: this.options.exitOnCtrlC, writeToStdout: this.writeToStdout, writeToStderr: this.writeToStderr, setCursorPosition: this.setCursorPosition, onExit: this.handleAppExit }, node)));
390
361
  if (this.options.concurrent) {
391
362
  // Concurrent mode: use updateContainer (async scheduling)
392
363
  reconciler.updateContainer(tree, this.container, null, noop);
@@ -405,11 +376,11 @@ export default class Ink {
405
376
  this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput);
406
377
  return;
407
378
  }
408
- if (!this.interactive) {
379
+ if (isInCi) {
409
380
  this.options.stdout.write(data);
410
381
  return;
411
382
  }
412
- const sync = this.shouldSync();
383
+ const sync = shouldSynchronize(this.options.stdout);
413
384
  if (sync) {
414
385
  this.options.stdout.write(bsu);
415
386
  }
@@ -429,11 +400,11 @@ export default class Ink {
429
400
  this.options.stdout.write(this.fullStaticOutput + this.lastOutput);
430
401
  return;
431
402
  }
432
- if (!this.interactive) {
403
+ if (isInCi) {
433
404
  this.options.stderr.write(data);
434
405
  return;
435
406
  }
436
- const sync = this.shouldSync();
407
+ const sync = shouldSynchronize(this.options.stdout);
437
408
  if (sync) {
438
409
  this.options.stdout.write(bsu);
439
410
  }
@@ -444,7 +415,7 @@ export default class Ink {
444
415
  this.options.stdout.write(esu);
445
416
  }
446
417
  }
447
- // eslint-disable-next-line @typescript-eslint/no-restricted-types
418
+ // eslint-disable-next-line @typescript-eslint/ban-types
448
419
  unmount(error) {
449
420
  if (this.isUnmounted || this.isUnmounting) {
450
421
  return;
@@ -455,10 +426,21 @@ export default class Ink {
455
426
  this.beforeExitHandler = undefined;
456
427
  }
457
428
  const stdout = this.options.stdout;
458
- const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
429
+ const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded && (stdout.writable ?? true);
430
+ const settleThrottle = (throttled) => {
431
+ if (typeof throttled.flush !== 'function') {
432
+ return;
433
+ }
434
+ if (canWriteToStdout) {
435
+ throttled.flush();
436
+ }
437
+ else if (typeof throttled.cancel === 'function') {
438
+ throttled.cancel();
439
+ }
440
+ };
459
441
  // Clear any pending throttled render timer on unmount. When stdout is writable,
460
442
  // flush so the final frame is emitted; otherwise cancel to avoid delayed callbacks.
461
- settleThrottle(this.throttledOnRender, canWriteToStdout);
443
+ settleThrottle(this.throttledOnRender ?? {});
462
444
  if (canWriteToStdout) {
463
445
  // If throttling is enabled and there is already a pending render, flushing above
464
446
  // is sufficient. Also avoid calling onRender() again when static output already
@@ -474,95 +456,84 @@ export default class Ink {
474
456
  // that could re-enter exit() via synchronous write callbacks.
475
457
  this.isUnmounted = true;
476
458
  this.unsubscribeExit();
477
- // Flush any pending throttled log writes if possible, otherwise cancel to
478
- // prevent delayed callbacks from writing to a closed stream.
479
- settleThrottle(this.throttledLog, canWriteToStdout);
480
459
  if (typeof this.restoreConsole === 'function') {
481
- // Once unmount starts, Ink stops trying to manage teardown-time
482
- // console output. Restoring the native console before React cleanup keeps
483
- // unmount behavior simple and avoids special-case handling for custom
484
- // streams, fullscreen frames, and alternate-screen teardown.
485
460
  this.restoreConsole();
486
461
  }
487
- const finishUnmount = () => {
488
- if (typeof this.unsubscribeResize === 'function') {
489
- this.unsubscribeResize();
490
- }
491
- // Cancel any in-progress auto-detection before checking protocol state
492
- if (this.cancelKittyDetection) {
493
- this.cancelKittyDetection();
494
- }
495
- if (canWriteToStdout) {
496
- if (this.kittyProtocolEnabled) {
497
- this.writeBestEffort(this.options.stdout, '\u001B[<u');
498
- }
499
- // Alternate-screen content is disposable by design. We intentionally
500
- // leave it active until React cleanup finishes, then restore the
501
- // primary buffer without replaying prior frames, hook writes, or
502
- // diagnostics onto it. Trying to preserve teardown output across the
503
- // buffer switch adds fragile lifecycle-specific behavior, so Ink keeps
504
- // alternate-screen teardown intentionally simple and best-effort.
505
- if (this.alternateScreen) {
506
- this.writeBestEffort(this.options.stdout, ansiEscapes.exitAlternativeScreen);
507
- this.writeBestEffort(this.options.stdout, showCursorEscape);
508
- this.alternateScreen = false;
509
- }
510
- if (!this.interactive) {
511
- // Non-interactive environments don't handle erasing ansi escapes well.
512
- // In debug mode, each render already writes to stdout, so only a trailing
513
- // newline is needed. In non-debug mode, write the last frame now (it was
514
- // deferred during rendering).
515
- this.options.stdout.write(this.options.debug ? '\n' : this.lastOutput + '\n');
516
- }
517
- else if (!this.options.debug) {
518
- this.log.done();
519
- }
520
- }
521
- this.kittyProtocolEnabled = false;
522
- instances.delete(this.options.stdout);
523
- // Ensure all queued writes have been processed before resolving the
524
- // exit promise. For real writable streams, queue an empty write as a
525
- // barrier — its callback fires only after all prior writes complete.
526
- // For non-stream objects (e.g. test spies), resolve on next tick.
527
- //
528
- // When called from signal-exit during process shutdown (error is a
529
- // number or null rather than undefined/Error), resolve synchronously
530
- // because the event loop is draining and async callbacks won't fire.
531
- const { exitResult } = this;
532
- const resolveOrReject = () => {
533
- if (isErrorInput(error)) {
534
- this.rejectExitPromise(error);
462
+ if (typeof this.unsubscribeResize === 'function') {
463
+ this.unsubscribeResize();
464
+ }
465
+ // Cancel any in-progress auto-detection before checking protocol state
466
+ if (this.cancelKittyDetection) {
467
+ this.cancelKittyDetection();
468
+ }
469
+ // Flush any pending throttled log writes if possible, otherwise cancel to
470
+ // prevent delayed callbacks from writing to a closed stream.
471
+ const throttledLog = this.throttledLog;
472
+ settleThrottle(throttledLog);
473
+ if (canWriteToStdout) {
474
+ if (this.kittyProtocolEnabled) {
475
+ try {
476
+ this.options.stdout.write('\u001B[<u');
535
477
  }
536
- else {
537
- this.resolveExitPromise(exitResult);
478
+ catch {
479
+ // Best-effort: stdout may already be destroyed during shutdown
538
480
  }
539
- };
540
- const isProcessExiting = error !== undefined && !isErrorInput(error);
541
- if (isProcessExiting) {
542
- resolveOrReject();
543
481
  }
544
- else if (canWriteToStdout && hasWritableState) {
545
- this.options.stdout.write('', resolveOrReject);
482
+ // CIs don't handle erasing ansi escapes well, so it's better to
483
+ // only render last frame of non-static output
484
+ if (isInCi) {
485
+ this.options.stdout.write(this.lastOutput + '\n');
546
486
  }
547
- else {
548
- setImmediate(resolveOrReject);
487
+ else if (!this.options.debug) {
488
+ this.log.done();
549
489
  }
550
- };
551
- const concurrentReconciler = reconciler;
490
+ }
491
+ this.kittyProtocolEnabled = false;
552
492
  if (this.options.concurrent) {
553
- reconciler.updateContainerSync(null, this.container, null, noop);
554
- reconciler.flushSyncWork();
555
- concurrentReconciler.flushPassiveEffects?.();
556
- finishUnmount();
493
+ // Concurrent mode: use updateContainer (async scheduling)
494
+ reconciler.updateContainer(null, this.container, null, noop);
557
495
  }
558
496
  else {
559
497
  // Legacy mode: use updateContainerSync + flushSyncWork (sync)
560
498
  reconciler.updateContainerSync(null, this.container, null, noop);
561
499
  reconciler.flushSyncWork();
562
- finishUnmount();
500
+ }
501
+ instances.delete(this.options.stdout);
502
+ // Ensure all queued writes have been processed before resolving the
503
+ // exit promise. For real writable streams, queue an empty write as a
504
+ // barrier — its callback fires only after all prior writes complete.
505
+ // For non-stream objects (e.g. test spies), resolve on next tick.
506
+ //
507
+ // When called from signal-exit during process shutdown (error is a
508
+ // number or null rather than undefined/Error), resolve synchronously
509
+ // because the event loop is draining and async callbacks won't fire.
510
+ const { exitResult } = this;
511
+ const resolveOrReject = () => {
512
+ if (isErrorInput(error)) {
513
+ this.rejectExitPromise(error);
514
+ }
515
+ else {
516
+ this.resolveExitPromise(exitResult);
517
+ }
518
+ };
519
+ const isProcessExiting = error !== undefined && !isErrorInput(error);
520
+ const hasWritableState = stdout._writableState !== undefined ||
521
+ stdout.writableLength !== undefined;
522
+ if (isProcessExiting) {
523
+ resolveOrReject();
524
+ }
525
+ else if (canWriteToStdout && hasWritableState) {
526
+ this.options.stdout.write('', resolveOrReject);
527
+ }
528
+ else {
529
+ setImmediate(resolveOrReject);
563
530
  }
564
531
  }
565
532
  async waitUntilExit() {
533
+ this.exitPromise ||= new Promise((resolve, reject) => {
534
+ this.resolveExitPromise = resolve;
535
+ this.rejectExitPromise = reject;
536
+ });
566
537
  if (!this.beforeExitHandler) {
567
538
  this.beforeExitHandler = () => {
568
539
  this.unmount();
@@ -571,46 +542,8 @@ export default class Ink {
571
542
  }
572
543
  return this.exitPromise;
573
544
  }
574
- async waitUntilRenderFlush() {
575
- if (this.isUnmounted || this.isUnmounting) {
576
- await this.awaitExit();
577
- return;
578
- }
579
- // Yield to the macrotask queue so that React's scheduler has a chance to
580
- // fire passive effects and process any work they enqueued.
581
- await yieldImmediate();
582
- if (this.isUnmounted || this.isUnmounting) {
583
- await this.awaitExit();
584
- return;
585
- }
586
- // In concurrent mode, React's scheduler may still be mid-render after
587
- // the yield. Wait for the next render commit instead of polling.
588
- if (this.isConcurrent && this.hasPendingConcurrentWork()) {
589
- await Promise.race([this.awaitNextRender(), this.awaitExit()]);
590
- if (this.isUnmounted || this.isUnmounting) {
591
- this.nextRenderCommit = undefined;
592
- await this.awaitExit();
593
- return;
594
- }
595
- }
596
- reconciler.flushSyncWork();
597
- const stdout = this.options.stdout;
598
- const { canWriteToStdout, hasWritableState } = getWritableStreamState(stdout);
599
- // Flush pending throttled render/log timers so their output is included in this wait.
600
- settleThrottle(this.throttledOnRender, canWriteToStdout);
601
- settleThrottle(this.throttledLog, canWriteToStdout);
602
- if (canWriteToStdout && hasWritableState) {
603
- await new Promise(resolve => {
604
- this.options.stdout.write('', () => {
605
- resolve();
606
- });
607
- });
608
- return;
609
- }
610
- await yieldImmediate();
611
- }
612
545
  clear() {
613
- if (this.interactive && !this.options.debug) {
546
+ if (!isInCi && !this.options.debug) {
614
547
  this.log.clear();
615
548
  // Sync lastOutput so that unmount's final onRender
616
549
  // sees it as unchanged and log-update skips it
@@ -633,106 +566,6 @@ export default class Ink {
633
566
  }
634
567
  });
635
568
  }
636
- setAlternateScreen(enabled) {
637
- this.alternateScreen = this.resolveAlternateScreenOption(enabled, this.interactive);
638
- if (this.alternateScreen) {
639
- this.writeBestEffort(this.options.stdout, ansiEscapes.enterAlternativeScreen);
640
- this.writeBestEffort(this.options.stdout, hideCursorEscape);
641
- }
642
- }
643
- resolveInteractiveOption(interactive) {
644
- return interactive ?? (!isInCi && Boolean(this.options.stdout.isTTY));
645
- }
646
- resolveAlternateScreenOption(alternateScreen, interactive) {
647
- return (Boolean(alternateScreen) &&
648
- interactive &&
649
- Boolean(this.options.stdout.isTTY));
650
- }
651
- shouldSync() {
652
- return shouldSynchronize(this.options.stdout, this.interactive);
653
- }
654
- // Best-effort write: streams may already be destroyed during shutdown.
655
- writeBestEffort(stream, data) {
656
- try {
657
- stream.write(data);
658
- }
659
- catch { }
660
- }
661
- // Waits for the exit promise to settle, suppressing any rejection.
662
- // Errors are surfaced via waitUntilExit() instead.
663
- async awaitExit() {
664
- try {
665
- await this.exitPromise;
666
- }
667
- catch { }
668
- }
669
- hasPendingConcurrentWork() {
670
- const concurrentContainer = this.container;
671
- return ((concurrentContainer.pendingLanes ?? 0) !== 0 &&
672
- concurrentContainer.callbackNode !== undefined &&
673
- concurrentContainer.callbackNode !== null);
674
- }
675
- async awaitNextRender() {
676
- if (!this.nextRenderCommit) {
677
- let resolveRender;
678
- const promise = new Promise(resolve => {
679
- resolveRender = resolve;
680
- });
681
- this.nextRenderCommit = { promise, resolve: resolveRender };
682
- }
683
- return this.nextRenderCommit.promise;
684
- }
685
- renderInteractiveFrame(output, outputHeight, staticOutput) {
686
- const hasStaticOutput = staticOutput !== '';
687
- const isTty = this.options.stdout.isTTY;
688
- // Detect fullscreen: output fills or exceeds terminal height.
689
- // Only apply when writing to a real TTY — piped output always gets trailing newlines.
690
- const viewportRows = isTty ? getWindowSize(this.options.stdout).rows : 24;
691
- const isFullscreen = isTty && outputHeight >= viewportRows;
692
- const outputToRender = isFullscreen ? output : output + '\n';
693
- const shouldClearTerminal = shouldClearTerminalForFrame({
694
- isTty,
695
- viewportRows,
696
- previousOutputHeight: this.lastOutputHeight,
697
- nextOutputHeight: outputHeight,
698
- isUnmounting: this.isUnmounting,
699
- });
700
- if (shouldClearTerminal) {
701
- const sync = this.shouldSync();
702
- if (sync) {
703
- this.options.stdout.write(bsu);
704
- }
705
- this.options.stdout.write(ansiEscapes.clearTerminal + this.fullStaticOutput + output);
706
- this.lastOutput = output;
707
- this.lastOutputToRender = outputToRender;
708
- this.lastOutputHeight = outputHeight;
709
- this.log.sync(outputToRender);
710
- if (sync) {
711
- this.options.stdout.write(esu);
712
- }
713
- return;
714
- }
715
- // To ensure static output is cleanly rendered before main output, clear main output first
716
- if (hasStaticOutput) {
717
- const sync = this.shouldSync();
718
- if (sync) {
719
- this.options.stdout.write(bsu);
720
- }
721
- this.log.clear();
722
- this.options.stdout.write(staticOutput);
723
- this.log(outputToRender);
724
- if (sync) {
725
- this.options.stdout.write(esu);
726
- }
727
- }
728
- else if (output !== this.lastOutput || this.log.isCursorDirty()) {
729
- // ThrottledLog manages its own bsu/esu at actual write time
730
- this.throttledLog(outputToRender);
731
- }
732
- this.lastOutput = output;
733
- this.lastOutputToRender = outputToRender;
734
- this.lastOutputHeight = outputHeight;
735
- }
736
569
  initKittyKeyboard() {
737
570
  // Protocol is opt-in: if kittyKeyboard is not specified, do nothing
738
571
  if (!this.options.kittyKeyboard) {
@@ -740,29 +573,26 @@ export default class Ink {
740
573
  }
741
574
  const opts = this.options.kittyKeyboard;
742
575
  const mode = opts.mode ?? 'auto';
743
- if (mode === 'disabled') {
576
+ if (mode === 'disabled' ||
577
+ !this.options.stdin.isTTY ||
578
+ !this.options.stdout.isTTY) {
744
579
  return;
745
580
  }
746
581
  const flags = opts.flags ?? ['disambiguateEscapeCodes'];
747
- // 'enabled' force-enables the protocol as long as both streams are TTYs,
748
- // regardless of the interactive setting (e.g. even in CI).
749
582
  if (mode === 'enabled') {
750
- if (this.options.stdin.isTTY && this.options.stdout.isTTY) {
751
- this.enableKittyProtocol(flags);
752
- }
583
+ this.enableKittyProtocol(flags);
753
584
  return;
754
585
  }
755
- // Auto mode: require interactive + TTY
756
- if (!this.interactive ||
757
- !this.options.stdin.isTTY ||
758
- !this.options.stdout.isTTY) {
759
- return;
586
+ // Auto mode: use heuristic precheck, then confirm with protocol query
587
+ const term = process.env['TERM'] ?? '';
588
+ const termProgram = process.env['TERM_PROGRAM'] ?? '';
589
+ const isKnownSupportingTerminal = 'KITTY_WINDOW_ID' in process.env ||
590
+ term === 'xterm-kitty' ||
591
+ termProgram === 'WezTerm' ||
592
+ termProgram === 'ghostty';
593
+ if (!isInCi && isKnownSupportingTerminal) {
594
+ this.confirmKittySupport(flags);
760
595
  }
761
- // Auto mode: query the terminal for kitty keyboard protocol support.
762
- // The CSI ? u query is safe to send to any terminal — unsupporting
763
- // terminals simply won't respond, and the 200ms timeout handles that.
764
- // This avoids maintaining a hardcoded whitelist of terminal names.
765
- this.confirmKittySupport(flags);
766
596
  }
767
597
  confirmKittySupport(flags) {
768
598
  const { stdin, stdout } = this.options;
@@ -777,11 +607,11 @@ export default class Ink {
777
607
  const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
778
608
  responseBuffer = [];
779
609
  if (remaining.length > 0) {
780
- stdin.unshift(Uint8Array.from(remaining));
610
+ stdin.unshift(Buffer.from(remaining));
781
611
  }
782
612
  };
783
613
  const onData = (data) => {
784
- const chunk = typeof data === 'string' ? textEncoder.encode(data) : data;
614
+ const chunk = typeof data === 'string' ? Buffer.from(data) : data;
785
615
  for (const byte of chunk) {
786
616
  responseBuffer.push(byte);
787
617
  }