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.
- package/README.md +4 -4
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +46 -7
- package/dist/cli/commands/approve.js +53 -8
- package/dist/cli/commands/plan.js +17 -2
- package/dist/cli/commands/report.js +15 -6
- package/dist/cli/commands/ui.js +13 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +19 -0
- package/dist/compile/collateral.js +90 -0
- package/dist/compile/index.js +18 -4
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +19 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/acceptance.js +50 -8
- package/dist/gates/llm.js +34 -19
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +173 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +18 -1
- package/dist/report/bundle.d.ts +6 -1
- package/dist/report/bundle.js +13 -3
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +188 -27
- package/dist/run/journal.d.ts +5 -0
- package/dist/run/journal.js +71 -1
- package/dist/tui/cockpit/capture.d.ts +107 -1
- package/dist/tui/cockpit/capture.js +287 -10
- package/dist/tui/cockpit/components.d.ts +40 -47
- package/dist/tui/cockpit/components.js +66 -50
- package/dist/tui/cockpit/derive.d.ts +73 -1
- package/dist/tui/cockpit/derive.js +211 -15
- package/dist/tui/cockpit/keys.d.ts +114 -19
- package/dist/tui/cockpit/keys.js +413 -44
- package/dist/tui/cockpit/layout.d.ts +231 -0
- package/dist/tui/cockpit/layout.js +307 -0
- package/dist/tui/cockpit/live.d.ts +65 -3
- package/dist/tui/cockpit/live.js +418 -48
- package/dist/tui/cockpit/pointer.d.ts +261 -0
- package/dist/tui/cockpit/pointer.js +610 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +93 -6
- package/dist/tui/cockpit/run-cockpit.js +805 -164
- package/dist/tui/cockpit/setup-cockpit.d.ts +146 -1
- package/dist/tui/cockpit/setup-cockpit.js +337 -5
- package/dist/tui/cockpit/views.d.ts +72 -0
- package/dist/tui/cockpit/views.js +56 -0
- package/dist/tui/cockpit/width.d.ts +105 -0
- package/dist/tui/cockpit/width.js +276 -0
- package/fixtures/speckit-sample/tasks.md +2 -2
- package/package.json +3 -1
package/dist/tui/cockpit/live.js
CHANGED
|
@@ -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,
|
|
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
|
|
8
|
-
import {
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
}
|