tickmarkr 1.81.0 → 1.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +4 -4
  2. package/dist/brand.d.ts +4 -1
  3. package/dist/brand.js +46 -7
  4. package/dist/cli/commands/approve.js +53 -8
  5. package/dist/cli/commands/plan.js +17 -2
  6. package/dist/cli/commands/report.js +15 -6
  7. package/dist/cli/commands/ui.js +13 -1
  8. package/dist/cli/index.d.ts +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/compile/collateral.d.ts +19 -0
  11. package/dist/compile/collateral.js +90 -0
  12. package/dist/compile/index.js +18 -4
  13. package/dist/drivers/herdr.d.ts +1 -0
  14. package/dist/drivers/herdr.js +19 -0
  15. package/dist/drivers/types.d.ts +1 -0
  16. package/dist/gates/acceptance.js +50 -8
  17. package/dist/gates/llm.js +34 -19
  18. package/dist/gates/review.d.ts +9 -1
  19. package/dist/gates/review.js +173 -7
  20. package/dist/gates/run-gates.d.ts +1 -0
  21. package/dist/gates/run-gates.js +18 -1
  22. package/dist/report/bundle.d.ts +6 -1
  23. package/dist/report/bundle.js +13 -3
  24. package/dist/run/daemon.d.ts +5 -0
  25. package/dist/run/daemon.js +188 -27
  26. package/dist/run/journal.d.ts +5 -0
  27. package/dist/run/journal.js +71 -1
  28. package/dist/tui/cockpit/capture.d.ts +107 -1
  29. package/dist/tui/cockpit/capture.js +287 -10
  30. package/dist/tui/cockpit/components.d.ts +40 -47
  31. package/dist/tui/cockpit/components.js +66 -50
  32. package/dist/tui/cockpit/derive.d.ts +73 -1
  33. package/dist/tui/cockpit/derive.js +211 -15
  34. package/dist/tui/cockpit/keys.d.ts +114 -19
  35. package/dist/tui/cockpit/keys.js +413 -44
  36. package/dist/tui/cockpit/layout.d.ts +231 -0
  37. package/dist/tui/cockpit/layout.js +307 -0
  38. package/dist/tui/cockpit/live.d.ts +65 -3
  39. package/dist/tui/cockpit/live.js +418 -48
  40. package/dist/tui/cockpit/pointer.d.ts +261 -0
  41. package/dist/tui/cockpit/pointer.js +610 -0
  42. package/dist/tui/cockpit/run-cockpit.d.ts +93 -6
  43. package/dist/tui/cockpit/run-cockpit.js +805 -164
  44. package/dist/tui/cockpit/setup-cockpit.d.ts +146 -1
  45. package/dist/tui/cockpit/setup-cockpit.js +337 -5
  46. package/dist/tui/cockpit/views.d.ts +72 -0
  47. package/dist/tui/cockpit/views.js +56 -0
  48. package/dist/tui/cockpit/width.d.ts +105 -0
  49. package/dist/tui/cockpit/width.js +276 -0
  50. package/fixtures/speckit-sample/tasks.md +2 -2
  51. package/package.json +3 -1
@@ -1,11 +1,13 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { render, useApp, useInput } from "ink";
4
- import { createElement, useEffect, useState } from "react";
5
- import { stateDirName } from "../../graph/graph.js";
4
+ import { createElement, useCallback, useEffect, useRef, useSyncExternalStore } from "react";
5
+ import { graphPath, loadGraph, stateDirName } from "../../graph/graph.js";
6
6
  import { Journal, parseRunId } from "../../run/journal.js";
7
- import { deriveRunCockpitData, RunCockpitFrame } from "./run-cockpit.js";
8
- import { applyFilterInput, dispatchRunKey, initialRunInteractionState, SURFACE_KEY_BINDINGS, } from "./keys.js";
7
+ import { deriveRunCockpitData } from "./derive.js";
8
+ import { deriveRunViewRows, drawnRunViewRowIds, planRunCockpitFrame, runKeyColumns, RunCockpitFrame, } from "./run-cockpit.js";
9
+ import { dispatchRunSurfaceKey, openingRunSurfaceState, reconcileRunInteraction, RUN_INPUT_BINDINGS, selectableRunViewRowIds, } from "./keys.js";
10
+ import { applyPointerReport, borrowPointerTracking, createPointerReportReader, } from "./pointer.js";
9
11
  // ponytail: fixed 2s re-derive cadence, matching `status --watch`; promote to a
10
12
  // config knob only if an operator asks.
11
13
  const REFRESH_MS = 2_000;
@@ -30,6 +32,22 @@ export function loadEngagementSource(cwd, runId) {
30
32
  const raw = readFileSync(path, "utf8");
31
33
  return { fileName: `${id}.journal.jsonl`, raw };
32
34
  }
35
+ /**
36
+ * The compiled graph, when the repository has one. It lends the tasks view its
37
+ * identities and nothing else, so an engagement with no compiled graph at all
38
+ * still draws every task the journal mentions rather than refusing to open.
39
+ *
40
+ * Absence is the only fallback. A graph that exists and cannot be read, parsed
41
+ * or validated is a fault, and swallowing it would quietly demote the surface:
42
+ * every task the graph alone knows about would vanish from the rows, and a
43
+ * selection standing on one of them would be cleared as if the engagement had
44
+ * dropped it. So it throws, and the caller keeps its last good frame instead.
45
+ */
46
+ export function loadEngagementGraph(cwd) {
47
+ if (!existsSync(graphPath(cwd)))
48
+ return undefined;
49
+ return loadGraph(cwd);
50
+ }
33
51
  // The same s/m/h age vocabulary `status` speaks (status.ts fmtAge).
34
52
  export function formatEventAge(ageMs) {
35
53
  const safe = Math.max(0, ageMs);
@@ -64,72 +82,424 @@ function lastEventMs(raw) {
64
82
  * than as current. Throws on unreadable bytes (empty capture, no run-start) —
65
83
  * the caller refuses instead of rendering a plausible surface from nothing.
66
84
  */
67
- export function deriveLiveRunCockpitData(source, binaryVersion, now = Date.now) {
68
- const data = deriveRunCockpitData(source, binaryVersion);
85
+ export function deriveLiveRunCockpitData(source, binaryVersion, now = Date.now, graph) {
86
+ const data = deriveRunCockpitData(source, binaryVersion, { graph });
69
87
  const lastMs = lastEventMs(source.raw);
70
88
  const staleness = lastMs === undefined
71
89
  ? "last event unknown"
72
90
  : `last event ${formatEventAge(now() - lastMs)} ago`;
73
91
  return { ...data, elapsed: `${data.elapsed} · ${staleness}` };
74
92
  }
75
- function LiveApp({ cwd, runId, binaryVersion, columns, rows, refreshMs, now, }) {
76
- const { exit } = useApp();
77
- const [data, setData] = useState(() => deriveLiveRunCockpitData(loadEngagementSource(cwd, runId), binaryVersion, now));
78
- const [interaction, setInteraction] = useState(initialRunInteractionState);
79
- useEffect(() => {
80
- const timer = setInterval(() => {
93
+ /**
94
+ * Repair the interaction state against the rows a refreshed view now owns.
95
+ * Selection is held by the row's source identity, so it survives rows landing
96
+ * above it and is dropped — never slid onto whoever inherited its index —
97
+ * when the refreshed engagement no longer carries it.
98
+ */
99
+ export function reconcileLiveRunInteraction(interaction, data) {
100
+ return reconcileRunInteraction(interaction, liveRunViewRowIds(interaction, data));
101
+ }
102
+ /**
103
+ * The rows the live surface hands its keys — the engagement's own identities,
104
+ * minus the journal's, which are never selectable in either tab. This is the
105
+ * one place rows enter the dispatcher, so no key can reach a journal row.
106
+ */
107
+ export function liveRunViewRowIds(interaction, data) {
108
+ return selectableRunViewRowIds(interaction.activeView,
109
+ // The frame's own rows, so a key can never stand on a row the surface did
110
+ // not draw — one derivation, read by both.
111
+ deriveRunViewRows(data, interaction.activeView, interaction.filterQuery)
112
+ .map((row) => row.id));
113
+ }
114
+ /**
115
+ * The pointer surface a committed frame carries: the plan the paint drew and
116
+ * the rows drawn into its body, both read off the renderer's own output. This
117
+ * is the only composition the live input path resolves through — it holds no
118
+ * planner and no geometry of its own, so a hit can never land on bands or
119
+ * rows the operator has not seen.
120
+ */
121
+ function committedRunPointerSurface(planned, data) {
122
+ if (planned.plan.kind !== "frame" || planned.content === undefined) {
123
+ return undefined;
124
+ }
125
+ return {
126
+ interaction: planned.interaction,
127
+ // The plan is the whole geometry the pointer receives: the item rows it
128
+ // resolves a hit through are the plan's own `items` band, the same region
129
+ // the paint draws its list into, so there is no second reading of where the
130
+ // list starts.
131
+ plan: planned.plan,
132
+ drawnRowIds: drawnRunViewRowIds(data, planned.interaction, planned.content.items.rows),
133
+ rowIds: liveRunViewRowIds(planned.interaction, data),
134
+ };
135
+ }
136
+ /**
137
+ * The suite's own planning of the surface a frame at this data, state and
138
+ * measured size commits — the same composition the renderer publishes through
139
+ * its commit seam, so a hit is tested against the frame production actually
140
+ * painted. The live input path never calls this: it resolves through the
141
+ * committed frame, and what nothing has painted yet has no geometry.
142
+ */
143
+ export function liveRunPointerSurface(data, interaction, size) {
144
+ return committedRunPointerSurface(planRunCockpitFrame({
145
+ data,
146
+ columns: size.columns,
147
+ rows: size.rows,
148
+ interaction,
149
+ }), data);
150
+ }
151
+ function createLiveCockpitDelivery({ cwd, runId, binaryVersion, now, size, }) {
152
+ // Both sources are read every refresh: a recompile between refreshes changes
153
+ // which tasks exist, and the surface must draw the graph the repository has
154
+ // now rather than the one it had when the cockpit opened.
155
+ const derive = () => deriveLiveRunCockpitData(loadEngagementSource(cwd, runId), binaryVersion, now, loadEngagementGraph(cwd));
156
+ const opening = openingRunSurfaceState();
157
+ let surface = {
158
+ data: derive(),
159
+ interaction: opening.interaction,
160
+ stashed: opening.stashed,
161
+ };
162
+ const listeners = new Set();
163
+ let batchDepth = 0;
164
+ let pendingDraw = false;
165
+ /**
166
+ * The frame on the screen, published by the renderer each time it commits
167
+ * one. Pointer reports resolve through this and only this: the input path
168
+ * plans nothing and caches no geometry, so a refresh, resize or key that
169
+ * nothing has painted yet cannot move a hit — the target set is the bands
170
+ * and rows the operator is actually looking at, until the paint replaces
171
+ * them.
172
+ */
173
+ let committedPointerSurface;
174
+ const publish = () => {
175
+ if (batchDepth > 0) {
176
+ pendingDraw = true;
177
+ return;
178
+ }
179
+ for (const listener of listeners)
180
+ listener();
181
+ };
182
+ const transition = (apply) => {
183
+ const next = apply(surface);
184
+ if (next === surface)
185
+ return false;
186
+ surface = next;
187
+ publish();
188
+ return true;
189
+ };
190
+ const delivery = {
191
+ snapshot: () => surface,
192
+ subscribe: (listener) => {
193
+ listeners.add(listener);
194
+ return () => listeners.delete(listener);
195
+ },
196
+ batch: (deliver) => {
197
+ batchDepth += 1;
81
198
  try {
82
- setData(deriveLiveRunCockpitData(loadEngagementSource(cwd, runId), binaryVersion, now));
199
+ return deliver();
200
+ }
201
+ finally {
202
+ batchDepth -= 1;
203
+ if (batchDepth === 0 && pendingDraw) {
204
+ // The batch ends and the surface is drawn again; the renderer's
205
+ // commit of that draw publishes the frame the next report resolves
206
+ // through.
207
+ pendingDraw = false;
208
+ publish();
209
+ }
210
+ }
211
+ },
212
+ refresh: () => {
213
+ try {
214
+ const refreshed = derive();
215
+ return transition((current) => ({
216
+ ...current,
217
+ data: refreshed,
218
+ interaction: reconcileLiveRunInteraction(current.interaction, refreshed),
219
+ }));
83
220
  }
84
221
  catch {
85
- // a torn read mid-append keeps the last good frame rather than blanking the surface
222
+ // A torn read mid-append, or a graph caught mid-recompile, keeps the
223
+ // last good frame rather than demoting or blanking the surface.
224
+ return false;
225
+ }
226
+ },
227
+ // Every key goes to the one surface dispatcher, in every state the surface
228
+ // can be in. The prompt is a scope inside that registry rather than an
229
+ // owner in front of it, so a global key — Tab above all — reaches its
230
+ // handler with a prompt open exactly as it does without one.
231
+ key: (event) => transition((current) => {
232
+ const next = dispatchRunSurfaceKey(event, { interaction: current.interaction, stashed: current.stashed }, RUN_INPUT_BINDINGS, liveRunViewRowIds(current.interaction, current.data),
233
+ // The dispatcher decides on rail visibility from the width the surface
234
+ // is measured at. A default here routed every key as if the rail were
235
+ // drawn, at widths where the plan draws no rail at all.
236
+ runKeyColumns(size().columns));
237
+ if (next === undefined)
238
+ return current;
239
+ const interaction = reconcileLiveRunInteraction(next.interaction, current.data);
240
+ return interaction === current.interaction && next.stashed === current.stashed
241
+ ? current
242
+ : { ...current, interaction, stashed: next.stashed };
243
+ }),
244
+ // A pointer report resolves through the committed frame — the plan and
245
+ // the drawn rows the paint has on the screen, and nothing the input path
246
+ // derives for itself. Adjacent reports — a double click above all —
247
+ // arrive in one batch, which defers the redraw: they all resolve through
248
+ // the ONE committed frame still on the screen, and only the state each
249
+ // transition is dispatched FROM moves. The first press's own new scroll
250
+ // window cannot slide the list under the second press, because that
251
+ // window does not exist until the paint draws it.
252
+ pointer: (report) => transition((current) => {
253
+ const drawn = committedPointerSurface;
254
+ if (drawn === undefined)
255
+ return current;
256
+ const next = applyPointerReport(report, {
257
+ ...drawn,
258
+ interaction: current.interaction,
259
+ });
260
+ if (next === undefined)
261
+ return current;
262
+ const interaction = reconcileLiveRunInteraction(next, current.data);
263
+ return interaction === current.interaction
264
+ ? current
265
+ : { ...current, interaction };
266
+ }),
267
+ commitPointerSurface: (surface) => {
268
+ committedPointerSurface = surface;
269
+ },
270
+ };
271
+ return delivery;
272
+ }
273
+ /** The measured size a snapshot states, in the whole cells a plan is made of. */
274
+ function parseMeasuredSize(snapshot) {
275
+ const [columns, rows] = snapshot.split(":").map((part) => Math.max(0, Math.floor(Number(part))));
276
+ return { columns, rows };
277
+ }
278
+ function measureOutput(output) {
279
+ const read = () => `${output.columns ?? 80}:${output.rows ?? 24}`;
280
+ let size = read();
281
+ const listeners = new Set();
282
+ const heldInkResizeListeners = new Set();
283
+ const resized = () => {
284
+ size = read();
285
+ for (const listener of listeners)
286
+ listener();
287
+ };
288
+ output.on("resize", resized);
289
+ // Ink otherwise repaints its previous tree as soon as the stream resizes.
290
+ // Hold that repaint until React has published the newly measured plan.
291
+ const subscribe = (event, listener) => {
292
+ if (event === "resize")
293
+ heldInkResizeListeners.add(listener);
294
+ else
295
+ output.on(event, listener);
296
+ return stdout;
297
+ };
298
+ const unsubscribe = (event, listener) => {
299
+ if (event === "resize")
300
+ heldInkResizeListeners.delete(listener);
301
+ else
302
+ output.off(event, listener);
303
+ return stdout;
304
+ };
305
+ const stdout = new Proxy(output, {
306
+ get(target, property) {
307
+ if (property === "on" || property === "addListener")
308
+ return subscribe;
309
+ if (property === "off" || property === "removeListener")
310
+ return unsubscribe;
311
+ const value = Reflect.get(target, property);
312
+ return typeof value === "function" && !Object.hasOwn(target, property)
313
+ ? value.bind(target)
314
+ : value;
315
+ },
316
+ });
317
+ return {
318
+ subscribe: (listener) => {
319
+ listeners.add(listener);
320
+ return () => listeners.delete(listener);
321
+ },
322
+ snapshot: () => size,
323
+ stdout,
324
+ settle: () => {
325
+ for (const listener of heldInkResizeListeners)
326
+ listener();
327
+ },
328
+ close: () => output.off("resize", resized),
329
+ };
330
+ }
331
+ /**
332
+ * The stdin the surface's keyboard reads: the real stream with every pointer
333
+ * report taken out of it. Reports and keys arrive interleaved on one stream, and
334
+ * a reader beside Ink's would not be enough — Ink would still see the report
335
+ * bytes and read a whole one as an unnameable escape sequence, or half of one as
336
+ * the single character its chunk happens to end on, dispatching either into
337
+ * whatever scope is live. So the split sits in the pull Ink already does
338
+ * (`read()` under its `readable` listener): reports are handed to the pointer
339
+ * boundary, and only what was left is ever read as a keystroke. A chunk that was
340
+ * nothing but reports reads as no input at all rather than as empty input.
341
+ */
342
+ function pointerFilteredInput(input, deliver) {
343
+ const reader = createPointerReportReader();
344
+ const tokens = [];
345
+ let pendingEscape;
346
+ const clearPendingEscape = () => {
347
+ if (pendingEscape === undefined)
348
+ return;
349
+ clearImmediate(pendingEscape);
350
+ pendingEscape = undefined;
351
+ };
352
+ const wake = () => {
353
+ input.emit("readable");
354
+ };
355
+ const schedulePendingEscape = () => {
356
+ clearPendingEscape();
357
+ // The same next-turn grace Ink gives an ambiguous ESC: a continuation that
358
+ // arrives first completes the report; otherwise ESC becomes keyboard input.
359
+ if (reader.pending() !== "\x1b")
360
+ return;
361
+ pendingEscape = setImmediate(() => {
362
+ pendingEscape = undefined;
363
+ tokens.push(...reader.flush().tokens);
364
+ wake();
365
+ });
366
+ };
367
+ const read = (...args) => {
368
+ while (true) {
369
+ const token = tokens.shift();
370
+ if (token?.type === "pointer") {
371
+ const reports = [token.report];
372
+ while (tokens[0]?.type === "pointer") {
373
+ const adjacent = tokens.shift();
374
+ if (adjacent?.type === "pointer")
375
+ reports.push(adjacent.report);
376
+ }
377
+ deliver(reports);
378
+ continue;
86
379
  }
87
- }, refreshMs);
380
+ if (token?.type === "keys")
381
+ return token.bytes;
382
+ const chunk = input.read(...args);
383
+ if (chunk === null || chunk === undefined)
384
+ return null;
385
+ clearPendingEscape();
386
+ const readout = reader(String(chunk));
387
+ tokens.push(...readout.tokens);
388
+ schedulePendingEscape();
389
+ }
390
+ };
391
+ const stdin = new Proxy(input, {
392
+ get(target, property) {
393
+ if (property === "read")
394
+ return read;
395
+ const value = Reflect.get(target, property);
396
+ return typeof value === "function" && !Object.hasOwn(target, property)
397
+ ? value.bind(target)
398
+ : value;
399
+ },
400
+ });
401
+ return { stdin, close: clearPendingEscape };
402
+ }
403
+ function LiveApp({ delivery, size, refreshMs, }) {
404
+ const { exit } = useApp();
405
+ const surface = useSyncExternalStore(delivery.subscribe, delivery.snapshot, delivery.snapshot);
406
+ const measuredSize = useSyncExternalStore(size.subscribe, size.snapshot, size.snapshot);
407
+ const { columns, rows } = parseMeasuredSize(measuredSize);
408
+ const drawnSize = useRef(measuredSize);
409
+ useEffect(() => {
410
+ if (drawnSize.current === measuredSize)
411
+ return;
412
+ drawnSize.current = measuredSize;
413
+ size.settle();
414
+ }, [measuredSize, size]);
415
+ useEffect(() => {
416
+ const timer = setInterval(delivery.refresh, refreshMs);
88
417
  return () => clearInterval(timer);
89
- }, [cwd, runId, binaryVersion, refreshMs, now]);
418
+ }, [delivery, refreshMs]);
419
+ useEffect(() => {
420
+ if (surface.interaction.quit)
421
+ exit();
422
+ }, [exit, surface.interaction.quit]);
90
423
  useInput((input, key) => {
91
424
  if (key.ctrl && input === "c") {
92
425
  exit();
93
426
  return;
94
427
  }
95
- // While the / prompt is open it owns the keyboard: characters grow the
96
- // query and no advertised binding fires.
97
- if (interaction.filterPrompt) {
98
- const prompted = applyFilterInput({ input, key }, interaction);
99
- if (prompted !== undefined)
100
- setInteraction(prompted);
101
- return;
102
- }
103
- const next = dispatchRunKey({ input, key }, interaction, SURFACE_KEY_BINDINGS.run);
104
- if (next === undefined)
105
- return;
106
- setInteraction(next);
107
- if (next.quit)
108
- exit();
428
+ delivery.key({ input, key });
109
429
  });
110
- return createElement(RunCockpitFrame, { data, columns, rows, interaction });
111
- }
112
- export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, }) {
113
- const columns = output.columns ?? 80;
114
- const rows = output.rows ?? 24;
115
- const app = render(createElement(LiveApp, {
116
- cwd,
117
- runId,
118
- binaryVersion,
430
+ // Every frame the renderer commits is published to the delivery, so a
431
+ // pointer report resolves against the plan and the drawn rows of the frame
432
+ // on the screen — the input path never plans one of its own.
433
+ const commitFrame = useCallback((planned, frameData) => {
434
+ delivery.commitPointerSurface(committedRunPointerSurface(planned, frameData));
435
+ }, [delivery]);
436
+ return createElement(RunCockpitFrame, {
437
+ data: surface.data,
119
438
  columns,
120
439
  rows,
121
- refreshMs,
122
- now,
123
- }), {
124
- stdin: input,
125
- stdout: output,
126
- exitOnCtrlC: false,
127
- patchConsole: false,
440
+ interaction: surface.interaction,
441
+ onCommittedFrame: commitFrame,
128
442
  });
443
+ }
444
+ export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, debug = false, onDelivery, }) {
445
+ // Subscribe before Ink mounts so no stale-size repaint can overtake the
446
+ // surface's newly planned frame — and before the delivery exists, which
447
+ // routes every input against this measurement.
448
+ const size = measureOutput(output);
449
+ let app;
450
+ let filteredInput;
451
+ let releasePointerTracking;
129
452
  try {
453
+ const delivery = createLiveCockpitDelivery({
454
+ cwd,
455
+ runId,
456
+ binaryVersion,
457
+ now,
458
+ size: () => parseMeasuredSize(size.snapshot()),
459
+ });
460
+ onDelivery?.(delivery);
461
+ // Pointer reports are their own input class: they are taken off the stream
462
+ // before the keyboard reads it and delivered to the pointer boundary, never
463
+ // handed to a key handler. A batch keeps adjacent reports to one redraw.
464
+ filteredInput = pointerFilteredInput(input, (reports) => {
465
+ delivery.batch(() => {
466
+ for (const report of reports)
467
+ delivery.pointer(report);
468
+ });
469
+ });
470
+ // One loan owns the complete live lifecycle. Its signal repayments stand
471
+ // before the ask, then remain installed through mount, paint, input and
472
+ // unmount. Non-tty output borrows nothing and writes nothing.
473
+ releasePointerTracking = borrowPointerTracking(output);
474
+ app = render(createElement(LiveApp, {
475
+ delivery,
476
+ size,
477
+ refreshMs,
478
+ }), {
479
+ stdin: filteredInput.stdin,
480
+ stdout: size.stdout,
481
+ debug,
482
+ exitOnCtrlC: false,
483
+ patchConsole: false,
484
+ });
130
485
  await app.waitUntilExit();
131
486
  }
132
487
  finally {
133
- app.unmount();
488
+ try {
489
+ filteredInput?.close();
490
+ }
491
+ finally {
492
+ try {
493
+ app?.unmount();
494
+ }
495
+ finally {
496
+ try {
497
+ releasePointerTracking?.();
498
+ }
499
+ finally {
500
+ size.close();
501
+ }
502
+ }
503
+ }
134
504
  }
135
505
  }