@termwright/probe-ink 0.2.0 → 0.3.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.
@@ -1,115 +1,637 @@
1
1
  import {
2
2
  PACKAGE_VERSION,
3
- canPublishInkGeometry,
3
+ acquireReactCommitBridge,
4
+ activateInkRendererObservation,
5
+ createInkMarkerWriter,
4
6
  createInkSession,
5
7
  onInkAnnotationChange,
6
8
  probeInfo
7
- } from "./chunk-Y5WYMWRU.js";
9
+ } from "./chunk-5ZU7V4OQ.js";
8
10
  import {
9
11
  isInstrumented
10
- } from "./chunk-LO7YF74P.js";
12
+ } from "./chunk-67M2GX5S.js";
13
+ import {
14
+ INK_FRAME_CONTEXT,
15
+ INK_RENDER_CAPTURE,
16
+ instrumentationSentinel
17
+ } from "./chunk-SLKX554P.js";
11
18
 
12
19
  // src/instrument.ts
13
20
  import { Stream } from "stream";
14
21
  import { createElement, Fragment } from "react";
15
- import { ENV_ENDPOINT, ENV_PROTOCOL, ENV_TOKEN, PROTOCOL_V2_ID } from "@termwright/protocol";
22
+ import { ENV_ENDPOINT, ENV_TOKEN } from "@termwright/protocol";
16
23
  import { connectProbe } from "@termwright/probe-runtime";
24
+
25
+ // src/frame-capture.ts
26
+ var latest = /* @__PURE__ */ new WeakMap();
27
+ function installInkCaptureHook() {
28
+ const globals = globalThis;
29
+ const previous = globals[INK_RENDER_CAPTURE];
30
+ const previousContext = globals[INK_FRAME_CONTEXT];
31
+ const hook = (root, rendered, screenReader) => {
32
+ if (screenReader) {
33
+ retainInkFrame({
34
+ root,
35
+ staticRoots: root.staticNode === void 0 ? [] : [root.staticNode],
36
+ staticChildren: snapshotStaticChildren(root.staticNode),
37
+ rendered,
38
+ screenReader,
39
+ geometry: /* @__PURE__ */ new Map(),
40
+ liveRows: visibleRows(rendered.output),
41
+ staticRows: visibleRows(rendered.staticOutput)
42
+ });
43
+ return;
44
+ }
45
+ retainInkFrame(captureInkLayout(root, rendered));
46
+ };
47
+ globals[INK_RENDER_CAPTURE] = hook;
48
+ const contextHook = (root, context) => {
49
+ const frame = latest.get(root);
50
+ if (frame !== void 0) latest.set(root, { ...frame, context });
51
+ };
52
+ globals[INK_FRAME_CONTEXT] = contextHook;
53
+ return () => {
54
+ if (globals[INK_RENDER_CAPTURE] === hook) {
55
+ if (previous === void 0) delete globals[INK_RENDER_CAPTURE];
56
+ else globals[INK_RENDER_CAPTURE] = previous;
57
+ }
58
+ if (globals[INK_FRAME_CONTEXT] === contextHook) {
59
+ if (previousContext === void 0) delete globals[INK_FRAME_CONTEXT];
60
+ else globals[INK_FRAME_CONTEXT] = previousContext;
61
+ }
62
+ };
63
+ }
64
+ function captureInkLayout(root, rendered, context) {
65
+ const inkRoot = root;
66
+ const geometry = /* @__PURE__ */ new Map();
67
+ const width = integer(inkRoot.yogaNode?.getComputedWidth());
68
+ const height = integer(inkRoot.yogaNode?.getComputedHeight());
69
+ walk(inkRoot, "live", 0, 0, rect(0, 0, width, height), geometry, true);
70
+ if (inkRoot.staticNode !== void 0) {
71
+ const staticRoot = inkRoot.staticNode;
72
+ walk(
73
+ staticRoot,
74
+ "static",
75
+ 0,
76
+ 0,
77
+ rect(
78
+ 0,
79
+ 0,
80
+ integer(staticRoot.yogaNode?.getComputedWidth()),
81
+ integer(staticRoot.yogaNode?.getComputedHeight())
82
+ ),
83
+ geometry,
84
+ false
85
+ );
86
+ }
87
+ return {
88
+ root: inkRoot,
89
+ staticRoots: inkRoot.staticNode === void 0 ? [] : [inkRoot.staticNode],
90
+ staticChildren: snapshotStaticChildren(inkRoot.staticNode),
91
+ rendered,
92
+ screenReader: false,
93
+ geometry,
94
+ liveRows: visibleRows(rendered.output),
95
+ staticRows: visibleRows(rendered.staticOutput),
96
+ ...context === void 0 ? {} : { context }
97
+ };
98
+ }
99
+ function capturedInkFrame(root) {
100
+ return latest.get(root);
101
+ }
102
+ function retainInkFrame(capture) {
103
+ const previous = latest.get(capture.root);
104
+ if (previous === void 0 || capture.screenReader) {
105
+ latest.set(capture.root, capture);
106
+ return;
107
+ }
108
+ const previousRoots = new Set(previous.staticRoots);
109
+ const addedRoots = capture.staticRoots.filter((root) => !previousRoots.has(root));
110
+ const newStaticNodes = new Set(
111
+ [...capture.staticChildren.keys()].filter((node) => !previous.staticChildren.has(node))
112
+ );
113
+ const hasNewStaticOutput = capture.staticRows > 0 && newStaticNodes.size > 0;
114
+ if (!hasNewStaticOutput) {
115
+ const retainedGeometry = new Map(capture.geometry);
116
+ for (const [node, geometry2] of previous.geometry) {
117
+ if (geometry2.region === "static") retainedGeometry.set(node, geometry2);
118
+ }
119
+ latest.set(capture.root, {
120
+ ...capture,
121
+ staticRoots: previous.staticRoots,
122
+ staticChildren: previous.staticChildren,
123
+ staticRows: previous.staticRows,
124
+ geometry: retainedGeometry
125
+ });
126
+ return;
127
+ }
128
+ const geometry = new Map(capture.geometry);
129
+ for (const [node, retained] of previous.geometry) {
130
+ if (retained.region === "static") geometry.set(node, retained);
131
+ }
132
+ for (const [node, current] of capture.geometry) {
133
+ if (current.region === "static") {
134
+ if (newStaticNodes.has(node)) {
135
+ geometry.set(node, {
136
+ ...current,
137
+ intended: shiftRows(current.intended, previous.staticRows),
138
+ visible: shiftRows(current.visible, previous.staticRows)
139
+ });
140
+ } else if (capture.staticRoots.includes(node)) {
141
+ const retained = previous.geometry.get(node);
142
+ if (retained !== void 0) {
143
+ geometry.set(node, {
144
+ ...retained,
145
+ intended: { ...retained.intended, height: previous.staticRows + capture.staticRows },
146
+ visible: { ...retained.visible, height: previous.staticRows + capture.staticRows }
147
+ });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ const staticChildren = new Map(previous.staticChildren);
153
+ for (const [parent, currentChildren] of capture.staticChildren) {
154
+ const retained = staticChildren.get(parent) ?? [];
155
+ staticChildren.set(parent, [
156
+ ...retained,
157
+ ...currentChildren.filter((child) => !retained.includes(child))
158
+ ]);
159
+ }
160
+ latest.set(capture.root, {
161
+ ...capture,
162
+ staticRoots: [...previous.staticRoots, ...addedRoots],
163
+ staticChildren,
164
+ staticRows: previous.staticRows + capture.staticRows,
165
+ geometry
166
+ });
167
+ }
168
+ function snapshotStaticChildren(root) {
169
+ const result = /* @__PURE__ */ new Map();
170
+ if (root === void 0) return result;
171
+ const stack = [root];
172
+ while (stack.length > 0) {
173
+ const node = stack.pop();
174
+ const children = [...node.childNodes];
175
+ result.set(node, children);
176
+ for (const child of children) if (child.nodeName !== "#text") stack.push(child);
177
+ }
178
+ return result;
179
+ }
180
+ function shiftRows(value, rows) {
181
+ return { ...value, row: value.row + rows };
182
+ }
183
+ function walk(node, region, offsetX, offsetY, ancestorClip, output, skipStatic) {
184
+ if (skipStatic && node.internal_static === true) return;
185
+ const yoga = node.yogaNode;
186
+ if (yoga === void 0 || yoga.getDisplay() === 1) return;
187
+ const x = offsetX + integer(yoga.getComputedLeft());
188
+ const y = offsetY + integer(yoga.getComputedTop());
189
+ const intended = rect(x, y, integer(yoga.getComputedWidth()), integer(yoga.getComputedHeight()));
190
+ const visible = intersection(intended, ancestorClip);
191
+ output.set(node, { intended, visible, region });
192
+ let childClip = ancestorClip;
193
+ if (node.nodeName === "ink-box") {
194
+ const horizontal = node.style?.overflowX === "hidden" || node.style?.overflow === "hidden";
195
+ const vertical = node.style?.overflowY === "hidden" || node.style?.overflow === "hidden";
196
+ if (horizontal || vertical) {
197
+ const left = horizontal ? x + integer(yoga.getComputedBorder(0)) : ancestorClip.column;
198
+ const right = horizontal ? x + intended.width - integer(yoga.getComputedBorder(2)) : ancestorClip.column + ancestorClip.width;
199
+ const top = vertical ? y + integer(yoga.getComputedBorder(1)) : ancestorClip.row;
200
+ const bottom = vertical ? y + intended.height - integer(yoga.getComputedBorder(3)) : ancestorClip.row + ancestorClip.height;
201
+ childClip = intersection(ancestorClip, rect(left, top, right - left, bottom - top));
202
+ }
203
+ }
204
+ for (const child of node.childNodes) {
205
+ if (child.nodeName !== "#text") {
206
+ walk(child, region, x, y, childClip, output, skipStatic);
207
+ }
208
+ }
209
+ }
210
+ function integer(value) {
211
+ return Number.isFinite(value) ? Math.trunc(value) : 0;
212
+ }
213
+ function rect(column, row, width, height) {
214
+ return {
215
+ row,
216
+ column,
217
+ width: Math.max(0, width),
218
+ height: Math.max(0, height)
219
+ };
220
+ }
221
+ function intersection(a, b) {
222
+ const column = Math.max(a.column, b.column);
223
+ const row = Math.max(a.row, b.row);
224
+ const right = Math.max(column, Math.min(a.column + a.width, b.column + b.width));
225
+ const bottom = Math.max(row, Math.min(a.row + a.height, b.row + b.height));
226
+ return rect(column, row, right - column, bottom - row);
227
+ }
228
+ function visibleRows(output) {
229
+ if (output === "") return 0;
230
+ const lines = output.split("\n");
231
+ return output.endsWith("\n") ? lines.length - 1 : lines.length;
232
+ }
233
+
234
+ // src/terminal-tracker.ts
235
+ import { createTerminal } from "@termwright/vt";
236
+ var ShadowWriteQueue = class {
237
+ #queue = Promise.resolve();
238
+ #failure;
239
+ enqueue(operation) {
240
+ this.#queue = this.#queue.then(async () => {
241
+ if (this.#failure !== void 0) return;
242
+ try {
243
+ await operation();
244
+ } catch (error) {
245
+ this.#failure = error instanceof Error ? error : new Error(String(error));
246
+ }
247
+ });
248
+ }
249
+ async drain() {
250
+ await this.#queue;
251
+ if (this.#failure !== void 0) throw this.#failure;
252
+ }
253
+ };
254
+ function trackTerminal(stdout, stderr) {
255
+ const built = createTerminal({
256
+ columns: positive(stdout.columns, 80),
257
+ rows: positive(stdout.rows, 24),
258
+ scrollback: 1e5
259
+ });
260
+ const terminal = built.terminal;
261
+ const queue = new ShadowWriteQueue();
262
+ let stopped = false;
263
+ const restorers = [];
264
+ const observe = (chunk, encoding) => {
265
+ if (stopped) return;
266
+ const bytes = Buffer.isBuffer(chunk) || chunk instanceof Uint8Array ? chunk : Buffer.from(
267
+ String(chunk),
268
+ typeof encoding === "string" ? encoding : "utf8"
269
+ );
270
+ const committed = stdout.isTTY ? withOnlcr(bytes) : bytes;
271
+ queue.enqueue(() => writeTerminal(terminal, committed));
272
+ };
273
+ for (const stream of /* @__PURE__ */ new Set([stdout, stderr])) restorers.push(intercept(stream, observe));
274
+ const onResize = () => terminal.resize(positive(stdout.columns, terminal.cols), positive(stdout.rows, terminal.rows));
275
+ stdout.on("resize", onResize);
276
+ restorers.push(() => stdout.off("resize", onResize));
277
+ return {
278
+ drain: () => queue.drain(),
279
+ position() {
280
+ const buffer = terminal.buffer.active;
281
+ return { row: buffer.cursorY, column: buffer.cursorX, buffer: buffer.type };
282
+ },
283
+ resize(columns, rows) {
284
+ terminal.resize(columns, rows);
285
+ },
286
+ stop() {
287
+ if (stopped) return;
288
+ stopped = true;
289
+ for (const restore of restorers.reverse()) restore();
290
+ terminal.dispose();
291
+ }
292
+ };
293
+ }
294
+ function withOnlcr(bytes) {
295
+ let newlines = 0;
296
+ for (const byte of bytes) if (byte === 10) newlines += 1;
297
+ if (newlines === 0) return bytes;
298
+ const output = new Uint8Array(bytes.length + newlines);
299
+ let index = 0;
300
+ for (const byte of bytes) {
301
+ if (byte === 10) output[index++] = 13;
302
+ output[index++] = byte;
303
+ }
304
+ return output;
305
+ }
306
+ function intercept(stream, observe) {
307
+ const target = stream;
308
+ const original = target.write;
309
+ const wrapped = function(...args) {
310
+ observe(args[0], args[1]);
311
+ return Reflect.apply(original, this, args);
312
+ };
313
+ try {
314
+ target.write = wrapped;
315
+ } catch (error) {
316
+ throw new Error("Ink terminal stream cannot be instrumented exactly", { cause: error });
317
+ }
318
+ return () => {
319
+ if (target.write === wrapped) target.write = original;
320
+ };
321
+ }
322
+ function writeTerminal(terminal, bytes) {
323
+ return new Promise((resolve) => terminal.write(bytes, resolve));
324
+ }
325
+ function positive(value, fallback) {
326
+ return Number.isSafeInteger(value) && value > 0 ? value : fallback;
327
+ }
328
+
329
+ // src/render-boundary.ts
330
+ var RenderBoundaryQueue = class {
331
+ #pending = [];
332
+ #preparing = /* @__PURE__ */ new Set();
333
+ #nextGeneration = 1;
334
+ #stopped = false;
335
+ take(committedGeneration) {
336
+ const boundary = this.#pending[0];
337
+ if (boundary === void 0 || boundary.generation !== committedGeneration) return void 0;
338
+ return this.#pending.shift();
339
+ }
340
+ afterCurrentRender(waitForCurrentRender, mutate) {
341
+ if (this.#stopped) return Promise.reject(stoppedError());
342
+ return new Promise((resolve, reject) => {
343
+ const boundary = { generation: this.#nextGeneration, resolve, reject };
344
+ this.#nextGeneration += 1;
345
+ this.#preparing.add(boundary);
346
+ let flush;
347
+ try {
348
+ flush = waitForCurrentRender();
349
+ } catch (error) {
350
+ this.#preparing.delete(boundary);
351
+ reject(asError(error));
352
+ return;
353
+ }
354
+ void flush.then(
355
+ () => {
356
+ this.#preparing.delete(boundary);
357
+ if (this.#stopped) return;
358
+ this.#pending.push(boundary);
359
+ try {
360
+ mutate(boundary.generation);
361
+ } catch (error) {
362
+ const index = this.#pending.indexOf(boundary);
363
+ if (index !== -1) this.#pending.splice(index, 1);
364
+ reject(asError(error));
365
+ }
366
+ },
367
+ (error) => {
368
+ this.#preparing.delete(boundary);
369
+ reject(asError(error));
370
+ }
371
+ );
372
+ });
373
+ }
374
+ stop() {
375
+ if (this.#stopped) return;
376
+ this.#stopped = true;
377
+ for (const boundary of this.#preparing) boundary.reject(stoppedError());
378
+ for (const boundary of this.#pending.splice(0)) {
379
+ boundary.reject(stoppedError());
380
+ }
381
+ }
382
+ };
383
+ function stoppedError() {
384
+ return new Error("Ink probe stopped before the render boundary");
385
+ }
386
+ function asError(error) {
387
+ return error instanceof Error ? error : new Error(String(error));
388
+ }
389
+
390
+ // src/instrument.ts
391
+ var processTracker = trackTerminal(process.stdout, process.stderr);
17
392
  var ADAPTER_NAME = "@termwright/probe-ink";
18
393
  var ADAPTER_VERSION = PACKAGE_VERSION;
394
+ var INK_CAPABILITIES = [
395
+ "tree",
396
+ "intended-geometry",
397
+ "clipped-geometry",
398
+ "states",
399
+ "actions",
400
+ "render-revisions"
401
+ ];
402
+ var INK_FLUSH_NEXT_RENDER = /* @__PURE__ */ Symbol.for("@termwright/probe-ink/flush-next-render");
403
+ var COMMIT_GENERATION_ATTRIBUTE = "__termwrightCommitGeneration";
19
404
  function wrapInkRender(ink, options = {}) {
20
405
  const env = options.env ?? process.env;
21
406
  const wrapped = (node, suppliedOptions) => {
22
407
  if (!isInstrumented(env)) return ink.render(node, suppliedOptions);
23
- try {
24
- return instrumentedRender(ink, node, suppliedOptions, env);
25
- } catch {
26
- return ink.render(node, suppliedOptions);
27
- }
408
+ return instrumentedRender(
409
+ ink,
410
+ node,
411
+ suppliedOptions,
412
+ env,
413
+ options.certifiedHarness === true,
414
+ options.reconciler,
415
+ options.connect ?? connectProbe
416
+ );
28
417
  };
29
418
  Object.defineProperty(wrapped, "__termwright__", { value: true });
30
419
  return wrapped;
31
420
  }
32
- function instrumentedRender(ink, node, suppliedOptions, env) {
33
- const options = normalizeOptions(suppliedOptions);
421
+ function instrumentedRender(ink, node, suppliedOptions, env, certifiedHarness, reconciler, connector) {
422
+ const certifiedRuntime = instrumentationSentinel() !== void 0;
423
+ if (!certifiedRuntime && !certifiedHarness) return ink.render(node, suppliedOptions);
424
+ let options;
425
+ try {
426
+ options = normalizeOptions(suppliedOptions);
427
+ } catch (error) {
428
+ return renderAfterSetupFailure(ink, node, suppliedOptions, connector, env, error);
429
+ }
430
+ let currentNode = node;
431
+ let commitGeneration = 0;
34
432
  const stdout = options.stdout ?? process.stdout;
433
+ const stderr = options.stderr ?? process.stderr;
434
+ const ownsTracker = stdout !== process.stdout || stderr !== process.stderr;
435
+ let tracker = processTracker;
35
436
  const probeRef = { current: null };
36
437
  const state = {
37
438
  channel: null,
38
439
  session: null
39
440
  };
40
441
  let disposed = false;
41
- const releaseAnnotations = onInkAnnotationChange(() => state.session?.notifyRender());
442
+ const renderBoundaries = new RenderBoundaryQueue();
443
+ let reactRoot = null;
444
+ let reactBridge;
445
+ let releaseCapture;
446
+ let releaseReactBridge;
447
+ let releaseReactBridgeHook;
448
+ let releaseAnnotations;
449
+ const stop = () => {
450
+ if (disposed) return;
451
+ disposed = true;
452
+ renderBoundaries.stop();
453
+ releaseCapture?.();
454
+ releaseReactBridge?.();
455
+ releaseReactBridgeHook?.();
456
+ releaseAnnotations?.();
457
+ if (ownsTracker) tracker.stop();
458
+ state.session?.stop();
459
+ state.channel?.close();
460
+ };
461
+ try {
462
+ releaseCapture = installInkCaptureHook();
463
+ tracker = ownsTracker ? trackTerminal(stdout, stderr) : processTracker;
464
+ if (reconciler !== void 0) {
465
+ const bridgeLease = acquireReactCommitBridge();
466
+ reactBridge = bridgeLease.bridge;
467
+ releaseReactBridgeHook = bridgeLease.release;
468
+ if (env["DEV"] !== "true") {
469
+ reactBridge = activateInkRendererObservation(reconciler);
470
+ }
471
+ releaseReactBridge = reactBridge.subscribe((event) => {
472
+ if (event.type === "commit" && probeRef.current?.parentNode === event.root) {
473
+ reactRoot = event.root;
474
+ }
475
+ });
476
+ }
477
+ releaseAnnotations = onInkAnnotationChange(() => {
478
+ setImmediate(() => {
479
+ if (!disposed) state.session?.notifyRender({ allowUnsettled: true });
480
+ });
481
+ });
482
+ } catch (error) {
483
+ stop();
484
+ return renderAfterSetupFailure(ink, node, suppliedOptions, connector, env, error);
485
+ }
42
486
  const wrap = (child) => createElement(
43
487
  Fragment,
44
488
  null,
45
- createElement(ink.Box, { ref: probeRef, display: "none" }),
489
+ createElement(ink.Box, {
490
+ ref: probeRef,
491
+ display: "none",
492
+ [COMMIT_GENERATION_ATTRIBUTE]: commitGeneration
493
+ }),
46
494
  child
47
495
  );
48
496
  const userOnRender = options.onRender;
49
- const instance = ink.render(wrap(node), {
50
- ...options,
51
- onRender(metrics) {
497
+ let instrumentedNode;
498
+ let instrumentedOptions;
499
+ try {
500
+ instrumentedNode = wrap(node);
501
+ instrumentedOptions = {
502
+ ...options,
503
+ onRender(metrics) {
504
+ const generation = probeRef.current?.style?.[COMMIT_GENERATION_ATTRIBUTE];
505
+ const boundary = typeof generation === "number" ? renderBoundaries.take(generation) : void 0;
506
+ try {
507
+ if (!certifiedRuntime) {
508
+ const root = probeRef.current?.parentNode ?? null;
509
+ if (root !== null) {
510
+ const measured = ink.measureElement(root);
511
+ const staticNode = root.staticNode;
512
+ const staticRows = staticNode === void 0 ? 0 : ink.measureElement(staticNode).height;
513
+ retainInkFrame(
514
+ captureInkLayout(
515
+ root,
516
+ {
517
+ output: "",
518
+ outputHeight: measured.height,
519
+ staticOutput: "\n".repeat(staticRows)
520
+ },
521
+ {
522
+ interactive: options.interactive === true,
523
+ alternateScreen: options.alternateScreen === true,
524
+ debug: options.debug === true,
525
+ stdoutIsTTY: stdout.isTTY === true,
526
+ rows: stdout.rows ?? 24
527
+ }
528
+ )
529
+ );
530
+ }
531
+ }
532
+ const publication = state.session?.notifyRender({
533
+ awaitPublication: boundary !== void 0
534
+ });
535
+ if (boundary !== void 0) {
536
+ if (publication === void 0) {
537
+ boundary.reject(new Error("Ink semantic session is not attached"));
538
+ } else {
539
+ void publication.then(
540
+ (revision) => {
541
+ if (revision === null) boundary.reject(new Error("Ink render was not published"));
542
+ else boundary.resolve(revision);
543
+ },
544
+ (error) => boundary.reject(error instanceof Error ? error : new Error(String(error)))
545
+ );
546
+ }
547
+ }
548
+ } catch (error) {
549
+ boundary?.reject(error instanceof Error ? error : new Error(String(error)));
550
+ state.session?.stop();
551
+ }
552
+ userOnRender?.(metrics);
553
+ }
554
+ };
555
+ } catch (error) {
556
+ stop();
557
+ return renderAfterSetupFailure(ink, node, suppliedOptions, connector, env, error);
558
+ }
559
+ let instance;
560
+ try {
561
+ instance = ink.render(instrumentedNode, instrumentedOptions);
562
+ } catch (error) {
563
+ stop();
564
+ throw error;
565
+ }
566
+ if (reconciler !== void 0 && reactBridge?.hasInkRenderer() !== true) {
567
+ const error = new Error(
568
+ "Ink semantic probe unavailable: React renderer instrumentation did not register Ink."
569
+ );
570
+ stop();
571
+ reportSetupFailure(connector, env, error);
572
+ return instance;
573
+ }
574
+ let connection;
575
+ try {
576
+ connection = connector({
577
+ endpoint: env[ENV_ENDPOINT],
578
+ token: env[ENV_TOKEN],
579
+ probe: probeInfo(),
580
+ capabilities: INK_CAPABILITIES,
581
+ adapterName: ADAPTER_NAME,
582
+ adapterVersion: ADAPTER_VERSION
583
+ }).then(async (channel) => {
584
+ if (channel === null || disposed) {
585
+ channel?.close();
586
+ return;
587
+ }
588
+ state.channel = channel;
589
+ state.session = createInkSession({
590
+ channel,
591
+ // The React bridge observes the same committed Ink host root through
592
+ // FiberRoot.containerInfo. Keep the hidden ref as the certified
593
+ // control while differential conformance is still in progress.
594
+ resolveRoot: () => reactRoot ?? probeRef.current?.parentNode ?? null,
595
+ resolveExcluded: () => probeRef.current,
596
+ resolveCapture: (root) => capturedInkFrame(root),
597
+ waitForRenderFlush: () => instance.waitUntilRenderFlush(),
598
+ stdout,
599
+ writeMarker: createInkMarkerWriter(stdout, { certifiedHarness }),
600
+ tracker,
601
+ onGuaranteeViolation: (error) => {
602
+ state.session?.stop();
603
+ channel.fail("adapter-guarantee-violation", error.message);
604
+ if (channel.isOpen) channel.close();
605
+ }
606
+ });
52
607
  try {
53
- state.session?.notifyRender();
608
+ await renderBoundaries.afterCurrentRender(
609
+ () => instance.waitUntilRenderFlush(),
610
+ (generation) => {
611
+ commitGeneration = generation;
612
+ instance.rerender(wrap(currentNode));
613
+ }
614
+ );
54
615
  } catch {
55
- state.session?.stop();
616
+ state.session.stop();
617
+ return;
56
618
  }
57
- userOnRender?.(metrics);
58
- }
59
- });
60
- const includeGeometry = canPublishInkGeometry({
61
- alternateScreen: options.alternateScreen === true,
62
- ...options.interactive === void 0 ? {} : { interactive: options.interactive },
63
- stdoutIsTTY: stdout.isTTY === true
64
- });
65
- const baseCapabilities = includeGeometry ? ["tree", "bounds", "absolute-bounds", "states", "actions", "render-revisions"] : ["tree", "states", "actions", "render-revisions"];
66
- const qualified = env[ENV_PROTOCOL] === PROTOCOL_V2_ID;
67
- const capabilities = qualified ? [...baseCapabilities, "qualified-observations"] : baseCapabilities;
68
- const connection = connectProbe({
69
- endpoint: env[ENV_ENDPOINT],
70
- token: env[ENV_TOKEN],
71
- probe: probeInfo(),
72
- capabilities,
73
- adapterName: ADAPTER_NAME,
74
- adapterVersion: ADAPTER_VERSION,
75
- ...qualified ? { protocol: PROTOCOL_V2_ID } : {}
76
- }).then(async (channel) => {
77
- if (channel === null || disposed) {
78
- channel?.close();
79
- return;
80
- }
81
- state.channel = channel;
82
- state.session = createInkSession({
83
- channel,
84
- resolveRoot: () => probeRef.current?.parentNode ?? null,
85
- resolveExcluded: () => probeRef.current,
86
- measureElement: ink.measureElement,
87
- stdout,
88
- includeGeometry
89
- });
90
- try {
91
- await instance.waitUntilRenderFlush();
92
- } catch {
93
- state.session.stop();
94
- return;
95
- }
96
- if (!disposed && state.session.frames === 0) state.session.notifyRender();
97
- }).catch(() => void 0);
98
- const stop = () => {
99
- if (disposed) return;
100
- disposed = true;
101
- releaseAnnotations();
102
- state.session?.stop();
103
- state.channel?.close();
104
- };
619
+ }).catch(() => void 0);
620
+ } catch (error) {
621
+ stop();
622
+ reportSetupFailure(connector, env, error);
623
+ return instance;
624
+ }
105
625
  void instance.waitUntilExit().catch(() => void 0).then(async () => {
626
+ renderBoundaries.stop();
106
627
  await connection;
107
628
  await state.session?.flush();
108
629
  stop();
109
630
  }).catch(stop);
110
- return {
631
+ const wrappedInstance = {
111
632
  ...instance,
112
633
  rerender(next) {
634
+ currentNode = next;
113
635
  instance.rerender(wrap(next));
114
636
  },
115
637
  unmount(error) {
@@ -118,8 +640,18 @@ function instrumentedRender(ink, node, suppliedOptions, env) {
118
640
  cleanup() {
119
641
  stop();
120
642
  instance.cleanup();
643
+ },
644
+ [INK_FLUSH_NEXT_RENDER](mutate) {
645
+ return renderBoundaries.afterCurrentRender(
646
+ () => instance.waitUntilRenderFlush(),
647
+ (generation) => {
648
+ commitGeneration = generation;
649
+ mutate();
650
+ }
651
+ );
121
652
  }
122
653
  };
654
+ return wrappedInstance;
123
655
  }
124
656
  function normalizeOptions(supplied) {
125
657
  if (supplied === void 0) return {};
@@ -128,7 +660,36 @@ function normalizeOptions(supplied) {
128
660
  }
129
661
  return supplied;
130
662
  }
663
+ function reportSetupFailure(connector, env, failure) {
664
+ const error = failure instanceof Error ? failure : new Error(String(failure));
665
+ try {
666
+ void connector({
667
+ endpoint: env[ENV_ENDPOINT],
668
+ token: env[ENV_TOKEN],
669
+ probe: probeInfo(),
670
+ capabilities: INK_CAPABILITIES,
671
+ adapterName: ADAPTER_NAME,
672
+ adapterVersion: ADAPTER_VERSION
673
+ }).then((channel) => {
674
+ if (channel === null) return;
675
+ channel.fail("adapter-guarantee-violation", error.message);
676
+ if (channel.isOpen) channel.close();
677
+ }).catch(() => void 0);
678
+ } catch {
679
+ }
680
+ }
681
+ function renderAfterSetupFailure(ink, node, suppliedOptions, connector, env, setupFailure) {
682
+ try {
683
+ const instance = ink.render(node, suppliedOptions);
684
+ reportSetupFailure(connector, env, setupFailure);
685
+ return instance;
686
+ } catch (applicationError) {
687
+ reportSetupFailure(connector, env, setupFailure);
688
+ throw applicationError;
689
+ }
690
+ }
131
691
  export {
692
+ INK_FLUSH_NEXT_RENDER,
132
693
  wrapInkRender
133
694
  };
134
695
  //# sourceMappingURL=instrument.js.map