@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11

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 (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,106 @@
1
+ // Derived from `patch-console` (MIT, Vadim Demedes).
2
+ // Note: use the explicit `node:console` Console class — the global `console`
3
+ // is replaced by a proxy without `.Console` under some test runners.
4
+ import { Console } from "node:console";
5
+ import { PassThrough } from "node:stream";
6
+
7
+ const consoleMethods = [
8
+ "assert",
9
+ "count",
10
+ "countReset",
11
+ "debug",
12
+ "dir",
13
+ "dirxml",
14
+ "error",
15
+ "group",
16
+ "groupCollapsed",
17
+ "groupEnd",
18
+ "info",
19
+ "log",
20
+ "table",
21
+ "time",
22
+ "timeEnd",
23
+ "timeLog",
24
+ "trace",
25
+ "warn",
26
+ ] as const;
27
+
28
+ type ConsoleMethod = (typeof consoleMethods)[number];
29
+
30
+ export type Callback = (stream: "stdout" | "stderr", data: string) => void;
31
+ export type Restore = () => void;
32
+
33
+ export const patchConsole = (callback: Callback): Restore => {
34
+ const stdout = new PassThrough();
35
+ const stderr = new PassThrough();
36
+
37
+ stdout.write = (data: string | Uint8Array): boolean => {
38
+ callback("stdout", String(data));
39
+ return true;
40
+ };
41
+
42
+ stderr.write = (data: string | Uint8Array): boolean => {
43
+ callback("stderr", String(data));
44
+ return true;
45
+ };
46
+
47
+ const internalConsole = new Console(stdout, stderr);
48
+ const originalMethods = new Map<ConsoleMethod, unknown>();
49
+
50
+ for (const method of consoleMethods) {
51
+ originalMethods.set(method, console[method]);
52
+ (console as Record<ConsoleMethod, unknown>)[method] = internalConsole[method];
53
+ }
54
+
55
+ return () => {
56
+ for (const method of consoleMethods) {
57
+ (console as Record<ConsoleMethod, unknown>)[method] = originalMethods.get(method);
58
+ }
59
+
60
+ originalMethods.clear();
61
+ };
62
+ };
63
+
64
+ // Intercept direct `write` calls on a stream. The write is swallowed and the
65
+ // decoded chunk is handed to `onData`; write callbacks still fire so callers
66
+ // awaiting flushes don't hang.
67
+ export const patchStreamWrite = (
68
+ stream: NodeJS.WritableStream,
69
+ onData: (data: string) => void,
70
+ ): Restore => {
71
+ // Keep the unbound reference: restore below must reassign the exact
72
+ // original function object, not a bound copy.
73
+ // oxlint-disable-next-line typescript/unbound-method
74
+ const originalWrite = stream.write;
75
+
76
+ const patchedWrite = (
77
+ chunk: unknown,
78
+ encodingOrCallback?: unknown,
79
+ callback?: unknown,
80
+ ): boolean => {
81
+ const data =
82
+ typeof chunk === "string"
83
+ ? chunk
84
+ : chunk instanceof Uint8Array
85
+ ? Buffer.from(chunk).toString()
86
+ : String(chunk);
87
+
88
+ onData(data);
89
+
90
+ const done =
91
+ typeof encodingOrCallback === "function"
92
+ ? encodingOrCallback
93
+ : typeof callback === "function"
94
+ ? callback
95
+ : undefined;
96
+ done?.();
97
+
98
+ return true;
99
+ };
100
+
101
+ stream.write = patchedWrite;
102
+
103
+ return () => {
104
+ stream.write = originalWrite;
105
+ };
106
+ };
@@ -0,0 +1,85 @@
1
+ // Derived from `quick-lru` (MIT, Sindre Sorhus), reduced to the surface Ink
2
+ // uses. Implements the same two-generation LRU algorithm: when the hot map
3
+ // fills up, it becomes the cold generation and reads promote entries back.
4
+ export class QuickLru<KeyType, ValueType> {
5
+ #size = 0;
6
+ #cache = new Map<KeyType, ValueType>();
7
+ #oldCache = new Map<KeyType, ValueType>();
8
+ readonly #maxSize: number;
9
+
10
+ constructor({ maxSize }: { maxSize: number }) {
11
+ if (!(maxSize && maxSize > 0)) {
12
+ throw new TypeError("`maxSize` must be a number greater than 0");
13
+ }
14
+
15
+ this.#maxSize = maxSize;
16
+ }
17
+
18
+ get size(): number {
19
+ let oldCacheSize = 0;
20
+
21
+ for (const key of this.#oldCache.keys()) {
22
+ if (!this.#cache.has(key)) {
23
+ oldCacheSize++;
24
+ }
25
+ }
26
+
27
+ return Math.min(this.#size + oldCacheSize, this.#maxSize);
28
+ }
29
+
30
+ get(key: KeyType): ValueType | undefined {
31
+ if (this.#cache.has(key)) {
32
+ return this.#cache.get(key);
33
+ }
34
+
35
+ if (this.#oldCache.has(key)) {
36
+ const value = this.#oldCache.get(key)!;
37
+ this.#oldCache.delete(key);
38
+ this.#set(key, value);
39
+ return value;
40
+ }
41
+
42
+ return;
43
+ }
44
+
45
+ set(key: KeyType, value: ValueType): this {
46
+ if (this.#cache.has(key)) {
47
+ this.#cache.set(key, value);
48
+ } else {
49
+ this.#set(key, value);
50
+ }
51
+
52
+ return this;
53
+ }
54
+
55
+ has(key: KeyType): boolean {
56
+ return this.#cache.has(key) || this.#oldCache.has(key);
57
+ }
58
+
59
+ delete(key: KeyType): boolean {
60
+ const deleted = this.#cache.delete(key);
61
+
62
+ if (deleted) {
63
+ this.#size--;
64
+ }
65
+
66
+ return this.#oldCache.delete(key) || deleted;
67
+ }
68
+
69
+ clear(): void {
70
+ this.#cache.clear();
71
+ this.#oldCache.clear();
72
+ this.#size = 0;
73
+ }
74
+
75
+ #set(key: KeyType, value: ValueType): void {
76
+ this.#cache.set(key, value);
77
+ this.#size++;
78
+
79
+ if (this.#size >= this.#maxSize) {
80
+ this.#size = 0;
81
+ this.#oldCache = this.#cache;
82
+ this.#cache = new Map();
83
+ }
84
+ }
85
+ }
package/src/react.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Sigil ships its own React. Consumers reach React's runtime through this
2
+ // entry (and `jsxImportSource: "@alchemy.run/sigil"`) so the hooks a component
3
+ // calls and the dispatcher the reconciler installs share one module identity.
4
+ //
5
+ // `@types/react` is an `export =` module, so `export *` is rejected at the type
6
+ // level; the runtime surface is enumerated instead. Types stay type-only
7
+ // imports from `react` (erased at runtime, so no identity concern).
8
+ export {
9
+ Activity,
10
+ Children,
11
+ Component,
12
+ Fragment,
13
+ Profiler,
14
+ PureComponent,
15
+ StrictMode,
16
+ Suspense,
17
+ ViewTransition,
18
+ act,
19
+ addTransitionType,
20
+ cache,
21
+ cacheSignal,
22
+ captureOwnerStack,
23
+ cloneElement,
24
+ createContext,
25
+ createElement,
26
+ createRef,
27
+ forwardRef,
28
+ isValidElement,
29
+ lazy,
30
+ memo,
31
+ startTransition,
32
+ use,
33
+ useActionState,
34
+ useCallback,
35
+ useContext,
36
+ useDebugValue,
37
+ useDeferredValue,
38
+ useEffect,
39
+ useEffectEvent,
40
+ useId,
41
+ useImperativeHandle,
42
+ useInsertionEffect,
43
+ useLayoutEffect,
44
+ useMemo,
45
+ useOptimistic,
46
+ useReducer,
47
+ useRef,
48
+ useState,
49
+ useSyncExternalStore,
50
+ useTransition,
51
+ version,
52
+ } from "react";
53
+ export { default } from "react";
@@ -0,0 +1,476 @@
1
+ import { createContext } from "react";
2
+ import createReconciler, { type ReactContext } from "react-reconciler";
3
+ import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants.js";
4
+ import * as Scheduler from "scheduler";
5
+
6
+ import {
7
+ createTextNode,
8
+ appendChildNode,
9
+ insertBeforeNode,
10
+ removeChildNode,
11
+ detachYogaSubtree,
12
+ emitLayoutListeners,
13
+ setStyle,
14
+ setTextNodeValue,
15
+ createNode,
16
+ setAttribute,
17
+ type DOMNodeAttribute,
18
+ type TextNode,
19
+ type ElementNames,
20
+ type DOMElement,
21
+ } from "#/dom.ts";
22
+ import { isSigilDev } from "#/env.ts";
23
+ import type { SemanticTextStyle } from "#/semantic-text-style.ts";
24
+ import { styles as applyStyles, type Styles } from "#/styles.ts";
25
+ import { type AnsiTransformer } from "#/transform-adapter.ts";
26
+ import { Yoga } from "#/yoga/index.ts";
27
+
28
+ import pkg from "../package.json" with { type: "json" };
29
+
30
+ // We need to conditionally perform devtools connection to avoid
31
+ // accidentally breaking other third-party code.
32
+ // See https://github.com/vadimdemedes/ink/issues/384
33
+ // See https://github.com/vadimdemedes/ink/issues/648
34
+ if (isSigilDev) {
35
+ // Intentionally no warning when the package is missing.
36
+ // SIGIL_DEV may be set for other reasons; devtools is opt-in via installing the package.
37
+ await import("#/devtools.ts").catch(() => {});
38
+ }
39
+
40
+ type AnyObject = Record<string, unknown>;
41
+
42
+ const diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => {
43
+ if (before === after) {
44
+ return;
45
+ }
46
+
47
+ if (!before) {
48
+ return after;
49
+ }
50
+
51
+ const changed: AnyObject = {};
52
+ let isChanged = false;
53
+
54
+ for (const key of Object.keys(before)) {
55
+ const isDeleted = after ? !Object.hasOwn(after, key) : true;
56
+
57
+ if (isDeleted) {
58
+ changed[key] = undefined;
59
+ isChanged = true;
60
+ }
61
+ }
62
+
63
+ if (after) {
64
+ for (const key of Object.keys(after)) {
65
+ if (after[key] !== before[key]) {
66
+ changed[key] = after[key];
67
+ isChanged = true;
68
+ }
69
+ }
70
+ }
71
+
72
+ return isChanged ? changed : undefined;
73
+ };
74
+
75
+ const findRootNode = (node: DOMElement): DOMElement | undefined => {
76
+ let current: DOMElement | undefined = node;
77
+
78
+ while (current) {
79
+ if (current.nodeName === "ink-root") {
80
+ return current;
81
+ }
82
+
83
+ current = current.parentNode;
84
+ }
85
+
86
+ return;
87
+ };
88
+
89
+ /**
90
+ * Clear the root's cached `staticNode` when the node it points at is being
91
+ * removed as part of a larger subtree.
92
+ *
93
+ * The previous identity check (`staticNode === removeNode`) only caught direct
94
+ * removal of the `<Static>` element. When an *ancestor* of `<Static>` is
95
+ * removed, the stale `staticNode` reference survives and the next render would
96
+ * replay stale static output (and, before `detachYogaSubtree`, trap on detached
97
+ * WASM memory — see QwenLM/qwen-code#6820).
98
+ *
99
+ * The owning root is derived from the host parent passed to the removal hook,
100
+ * not a module-level global, so instances with separate stdout streams don't
101
+ * clobber each other's pointers.
102
+ */
103
+ const clearStaticNodeIfContained = (
104
+ rootNode: DOMElement | undefined,
105
+ removeNode: DOMElement | TextNode,
106
+ ): void => {
107
+ if (!rootNode?.staticNode) {
108
+ return;
109
+ }
110
+
111
+ // Walk up from staticNode to see if removeNode is an ancestor.
112
+ let current: DOMElement | undefined = rootNode.staticNode;
113
+
114
+ while (current) {
115
+ if (current === removeNode) {
116
+ // Only clear staticNode, not previousStaticNode. The inequality
117
+ // (undefined !== previousStaticNode) triggers onStaticChange in
118
+ // resetAfterCommit, which resets fullStaticOutput.
119
+ rootNode.staticNode = undefined;
120
+ return;
121
+ }
122
+
123
+ current = current.parentNode;
124
+ }
125
+ };
126
+
127
+ type Props = Record<string, unknown>;
128
+
129
+ type HostContext = {
130
+ isInsideText: boolean;
131
+ };
132
+
133
+ let currentUpdatePriority = NoEventPriority;
134
+
135
+ /**
136
+ * Host hooks react-reconciler 0.34 (React 19.3) added for View Transitions
137
+ * and Fragment refs. `@types/react-reconciler` (0.33) does not declare them
138
+ * yet, so they are spread in rather than written inline (excess-property
139
+ * checks would reject them on the literal).
140
+ *
141
+ * A terminal has no view transitions, so a `<ViewTransition>` commits like
142
+ * any other update: `startViewTransition` mirrors react-dom's fallback when
143
+ * the document can't animate — run the commit phases synchronously and
144
+ * return no transition handle. `suspendOnActiveViewTransition` is on the
145
+ * hot path: the reconciler calls it for every transition-lane commit, so
146
+ * without it `startTransition` / `useTransition` updates never land.
147
+ */
148
+ const viewTransitionHostConfig = {
149
+ suspendOnActiveViewTransition() {},
150
+ startViewTransition(
151
+ _suspendedState: unknown,
152
+ _rootContainer: DOMElement,
153
+ _transitionTypes: unknown,
154
+ mutationCallback: () => void,
155
+ layoutCallback: () => void,
156
+ _afterMutationCallback: () => void,
157
+ spawnedWorkCallback: () => void,
158
+ _passiveCallback: () => void,
159
+ _errorCallback: (error: unknown) => void,
160
+ _blockedCallback: (reason: string) => void,
161
+ finishedAnimation: () => void,
162
+ ): null {
163
+ mutationCallback();
164
+ layoutCallback();
165
+ finishedAnimation();
166
+ spawnedWorkCallback();
167
+ return null;
168
+ },
169
+ stopViewTransition() {},
170
+ createViewTransitionInstance: (name: string) => ({ name }),
171
+ measureInstance: () => null,
172
+ measureClonedInstance: () => null,
173
+ wasInstanceInViewport: () => false,
174
+ hasInstanceChanged: () => false,
175
+ hasInstanceAffectedParent: () => false,
176
+ applyViewTransitionName() {},
177
+ restoreViewTransitionName() {},
178
+ cancelViewTransitionName() {},
179
+ cancelRootViewTransitionName() {},
180
+ restoreRootViewTransitionName() {},
181
+ createFragmentInstance: (fiber: unknown) => ({ fiber }),
182
+ updateFragmentInstanceFiber() {},
183
+ };
184
+
185
+ export const reconciler = createReconciler<
186
+ ElementNames,
187
+ Props,
188
+ DOMElement,
189
+ DOMElement,
190
+ TextNode,
191
+ DOMElement,
192
+ unknown,
193
+ unknown,
194
+ unknown,
195
+ HostContext,
196
+ unknown,
197
+ unknown,
198
+ unknown,
199
+ unknown
200
+ >({
201
+ getRootHostContext: () => ({
202
+ isInsideText: false,
203
+ }),
204
+ prepareForCommit: () => null,
205
+ preparePortalMount: () => null,
206
+ clearContainer: () => false,
207
+ resetAfterCommit(rootNode) {
208
+ if (typeof rootNode.onComputeLayout === "function") {
209
+ rootNode.onComputeLayout();
210
+ }
211
+
212
+ emitLayoutListeners(rootNode);
213
+
214
+ /*
215
+ Fire `onStaticChange` BEFORE `onImmediateRender` so ink resets accumulated static output before the new instance emits. Without this, items from a replaced/removed <Static> stay in `fullStaticOutput` and get replayed on rewrites.
216
+ */
217
+ if (rootNode.staticNode !== rootNode.previousStaticNode) {
218
+ rootNode.previousStaticNode = rootNode.staticNode;
219
+ if (typeof rootNode.onStaticChange === "function") {
220
+ rootNode.onStaticChange();
221
+ }
222
+ }
223
+
224
+ // Since renders are throttled at the instance level and <Static> component children
225
+ // are rendered only once and then get deleted, we need an escape hatch to
226
+ // trigger an immediate render to ensure <Static> children are written to output before they get erased
227
+ if (rootNode.isStaticDirty) {
228
+ rootNode.isStaticDirty = false;
229
+ if (typeof rootNode.onImmediateRender === "function") {
230
+ rootNode.onImmediateRender();
231
+ }
232
+
233
+ return;
234
+ }
235
+
236
+ if (typeof rootNode.onRender === "function") {
237
+ rootNode.onRender();
238
+ }
239
+ },
240
+ getChildHostContext(parentHostContext, type) {
241
+ const previousIsInsideText = parentHostContext.isInsideText;
242
+ const isInsideText = type === "ink-text" || type === "ink-virtual-text";
243
+
244
+ if (previousIsInsideText === isInsideText) {
245
+ return parentHostContext;
246
+ }
247
+
248
+ return { isInsideText };
249
+ },
250
+ shouldSetTextContent: () => false,
251
+ createInstance(originalType, newProps, rootNode, hostContext) {
252
+ if (hostContext.isInsideText && originalType === "ink-box") {
253
+ throw new Error(`<Box> can't be nested inside <Text> component`);
254
+ }
255
+
256
+ const type =
257
+ originalType === "ink-text" && hostContext.isInsideText ? "ink-virtual-text" : originalType;
258
+
259
+ const node = createNode(type);
260
+
261
+ for (const [key, value] of Object.entries(newProps)) {
262
+ if (key === "children") {
263
+ continue;
264
+ }
265
+
266
+ if (key === "style") {
267
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
268
+ setStyle(node, value as Styles);
269
+
270
+ if (node.yogaNode) {
271
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
272
+ applyStyles(node.yogaNode, value as Styles);
273
+ }
274
+
275
+ continue;
276
+ }
277
+
278
+ if (key === "internal_transform") {
279
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
280
+ node.internal_transform = value as AnsiTransformer;
281
+ continue;
282
+ }
283
+
284
+ if (key === "internal_ansi") {
285
+ node.internal_ansi = value === true;
286
+ continue;
287
+ }
288
+
289
+ if (key === "internal_textStyle") {
290
+ node.internal_textStyle = value as SemanticTextStyle;
291
+ continue;
292
+ }
293
+
294
+ if (key === "internal_static") {
295
+ node.internal_static = true;
296
+ rootNode.isStaticDirty = true;
297
+
298
+ // Save reference to <Static> node to skip traversal of entire
299
+ // node tree to find it
300
+ rootNode.staticNode = node;
301
+ continue;
302
+ }
303
+
304
+ setAttribute(node, key, value as DOMNodeAttribute);
305
+ }
306
+
307
+ return node;
308
+ },
309
+ createTextInstance(text, _root, hostContext) {
310
+ if (!hostContext.isInsideText) {
311
+ throw new Error(`Text string "${text}" must be rendered inside <Text> component`);
312
+ }
313
+
314
+ return createTextNode(text);
315
+ },
316
+ resetTextContent() {},
317
+ hideTextInstance(node) {
318
+ setTextNodeValue(node, "");
319
+ },
320
+ unhideTextInstance(node, text) {
321
+ setTextNodeValue(node, text);
322
+ },
323
+ getPublicInstance: (instance) => instance,
324
+ hideInstance(node) {
325
+ node.yogaNode?.setDisplay(Yoga.DISPLAY_NONE);
326
+ },
327
+ unhideInstance(node) {
328
+ node.yogaNode?.setDisplay(Yoga.DISPLAY_FLEX);
329
+ },
330
+ appendInitialChild: appendChildNode,
331
+ appendChild: appendChildNode,
332
+ insertBefore: insertBeforeNode,
333
+ finalizeInitialChildren() {
334
+ return false;
335
+ },
336
+ isPrimaryRenderer: true,
337
+ supportsMutation: true,
338
+ supportsPersistence: false,
339
+ supportsHydration: false,
340
+ // Scheduler integration for concurrent mode
341
+ supportsMicrotasks: true,
342
+ scheduleMicrotask: queueMicrotask,
343
+ // @ts-expect-error @types/react-reconciler is outdated and doesn't include scheduleCallback
344
+ scheduleCallback: Scheduler.unstable_scheduleCallback,
345
+ cancelCallback: Scheduler.unstable_cancelCallback,
346
+ shouldYield: Scheduler.unstable_shouldYield,
347
+ now: Scheduler.unstable_now,
348
+ scheduleTimeout: setTimeout,
349
+ cancelTimeout: clearTimeout,
350
+ noTimeout: -1,
351
+ beforeActiveInstanceBlur() {},
352
+ afterActiveInstanceBlur() {},
353
+ detachDeletedInstance() {},
354
+ getInstanceFromNode: () => null,
355
+ prepareScopeUpdate() {},
356
+ getInstanceFromScope: () => null,
357
+ appendChildToContainer: appendChildNode,
358
+ insertInContainerBefore: insertBeforeNode,
359
+ removeChildFromContainer(node, removeNode) {
360
+ // `node` is the container, i.e. the root itself. Clear before
361
+ // removeChildNode breaks the parent chain.
362
+ clearStaticNodeIfContained(findRootNode(node), removeNode);
363
+
364
+ removeChildNode(node, removeNode);
365
+ detachYogaSubtree(removeNode);
366
+ },
367
+ commitUpdate(node, _type, oldProps, newProps) {
368
+ if (node.internal_static) {
369
+ const rootNode = findRootNode(node);
370
+
371
+ if (rootNode) {
372
+ rootNode.isStaticDirty = true;
373
+ }
374
+ }
375
+
376
+ const props = diff(oldProps, newProps);
377
+
378
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
379
+ const style = diff(oldProps["style"] as Styles, newProps["style"] as Styles);
380
+
381
+ if (!props && !style) {
382
+ return;
383
+ }
384
+
385
+ if (props) {
386
+ for (const [key, value] of Object.entries(props)) {
387
+ if (key === "style") {
388
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
389
+ setStyle(node, value as Styles);
390
+ continue;
391
+ }
392
+
393
+ if (key === "internal_transform") {
394
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
395
+ node.internal_transform = value as AnsiTransformer;
396
+ continue;
397
+ }
398
+
399
+ if (key === "internal_ansi") {
400
+ node.internal_ansi = value === true;
401
+ continue;
402
+ }
403
+
404
+ if (key === "internal_textStyle") {
405
+ node.internal_textStyle = value as SemanticTextStyle | undefined;
406
+ continue;
407
+ }
408
+
409
+ if (key === "internal_static") {
410
+ node.internal_static = true;
411
+ continue;
412
+ }
413
+
414
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
415
+ setAttribute(node, key, value as DOMNodeAttribute);
416
+ }
417
+ }
418
+
419
+ if (style && node.yogaNode) {
420
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
421
+ applyStyles(node.yogaNode, style, (newProps["style"] as Styles | undefined) ?? {});
422
+ }
423
+ },
424
+ commitTextUpdate(node, _oldText, newText) {
425
+ setTextNodeValue(node, newText);
426
+ },
427
+ removeChild(node, removeNode) {
428
+ // `node` is the host parent; its chain up to the root is still intact
429
+ // here, so derive the owning root from it rather than a global.
430
+ clearStaticNodeIfContained(findRootNode(node), removeNode);
431
+
432
+ removeChildNode(node, removeNode);
433
+ detachYogaSubtree(removeNode);
434
+ },
435
+ setCurrentUpdatePriority(newPriority: number) {
436
+ currentUpdatePriority = newPriority;
437
+ },
438
+ getCurrentUpdatePriority: () => currentUpdatePriority,
439
+ resolveUpdatePriority() {
440
+ if (currentUpdatePriority !== NoEventPriority) {
441
+ return currentUpdatePriority;
442
+ }
443
+
444
+ return DefaultEventPriority;
445
+ },
446
+ maySuspendCommit() {
447
+ // Return true to enable Suspense resource preloading
448
+ return true;
449
+ },
450
+ NotPendingTransition: undefined,
451
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
452
+ HostTransitionContext: createContext(null) as unknown as ReactContext<unknown>,
453
+ resetFormInstance() {},
454
+ requestPostPaintCallback() {},
455
+ shouldAttemptEagerTransition() {
456
+ return false;
457
+ },
458
+ trackSchedulerEvent() {},
459
+ resolveEventType() {
460
+ return null;
461
+ },
462
+ resolveEventTimeStamp() {
463
+ return -1.1;
464
+ },
465
+ preloadInstance() {
466
+ return true;
467
+ },
468
+ startSuspendingCommit() {},
469
+ suspendInstance() {},
470
+ waitForCommitToBeReady() {
471
+ return null;
472
+ },
473
+ rendererPackageName: pkg.name,
474
+ rendererVersion: pkg.version,
475
+ ...viewTransitionHostConfig,
476
+ });