tickmarkr 1.83.0 → 1.85.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 (37) hide show
  1. package/dist/adapters/claude-code.d.ts +1 -0
  2. package/dist/adapters/claude-code.js +57 -1
  3. package/dist/adapters/fake.js +9 -0
  4. package/dist/adapters/types.d.ts +3 -0
  5. package/dist/adapters/types.js +21 -0
  6. package/dist/cli/commands/status.js +160 -30
  7. package/dist/compile/collateral.d.ts +86 -2
  8. package/dist/compile/collateral.js +294 -3
  9. package/dist/config/config.d.ts +62 -0
  10. package/dist/config/config.js +157 -2
  11. package/dist/drivers/herdr.d.ts +20 -3
  12. package/dist/drivers/herdr.js +288 -105
  13. package/dist/gates/baseline.d.ts +1 -0
  14. package/dist/gates/baseline.js +91 -13
  15. package/dist/gates/review.d.ts +7 -0
  16. package/dist/gates/review.js +99 -6
  17. package/dist/gates/run-gates.d.ts +9 -0
  18. package/dist/gates/run-gates.js +285 -41
  19. package/dist/run/daemon.d.ts +48 -2
  20. package/dist/run/daemon.js +1417 -315
  21. package/dist/run/journal.d.ts +56 -3
  22. package/dist/run/journal.js +275 -1
  23. package/dist/run/stall.d.ts +35 -1
  24. package/dist/run/stall.js +118 -8
  25. package/dist/tui/cockpit/components.d.ts +30 -1
  26. package/dist/tui/cockpit/components.js +19 -3
  27. package/dist/tui/cockpit/derive.d.ts +29 -2
  28. package/dist/tui/cockpit/derive.js +219 -23
  29. package/dist/tui/cockpit/layout.d.ts +75 -6
  30. package/dist/tui/cockpit/layout.js +97 -19
  31. package/dist/tui/cockpit/live.d.ts +14 -1
  32. package/dist/tui/cockpit/live.js +223 -29
  33. package/dist/tui/cockpit/pointer.d.ts +261 -0
  34. package/dist/tui/cockpit/pointer.js +610 -0
  35. package/dist/tui/cockpit/run-cockpit.d.ts +36 -5
  36. package/dist/tui/cockpit/run-cockpit.js +270 -51
  37. package/package.json +1 -1
@@ -0,0 +1,610 @@
1
+ import { dispatchRunKey, RUN_INPUT_BINDINGS, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, } from "./keys.js";
2
+ import { FRAME_VIEWS, } from "./layout.js";
3
+ /**
4
+ * THE pointer layer of the run cockpit. Three laws govern every line of it:
5
+ *
6
+ * 1. HIT-TESTING RESOLVES THROUGH THE PLAN. A cell becomes a target by being
7
+ * looked up in planFrame's own output — the plan the conformance oracle
8
+ * pins — and every offset inside it is planned too: the bands from
9
+ * `plan.regions`, which the item rows are a member of, and the rail's view
10
+ * rows from `plan.sidebar.viewRows`. Both are read, never reconstructed:
11
+ * this layer performs no geometric arithmetic at all, so there is no
12
+ * expression here for a plan change to leave behind. The plan is the whole
13
+ * input: nothing is passed beside it, so no caller can hand this layer a
14
+ * rectangle the plan did not produce. It owns no constant of the drawn
15
+ * chrome and imports none:
16
+ * it never measures the terminal, subtracts a border, re-derives a band or
17
+ * caches a rectangle. v1.83 deleted withBandGeometry for exactly that
18
+ * defect, and it does not return wearing a mouse.
19
+ * 2. EVERY POINTER ACTION IS A KEY'S TRANSITION. The pointer dispatches only
20
+ * keys through the one registry; it never manufactures focus, selection or
21
+ * prompt state beside the keyboard and then restores it afterward.
22
+ * 3. A POINTER ACTION IS NEVER ROUTED THROUGH WHATEVER SCOPE IS LIVE. A click
23
+ * means the target it hit, so the / prompt — a keyboard scope in which a
24
+ * digit is filter text and ⏎ applies a filter — is retired before any
25
+ * transition is dispatched. Otherwise clicking Gates would type "3". The
26
+ * wheel dispatches through that same normal roster but restores the prompt
27
+ * afterwards: a scroll names no target, so it may not retire one panel's
28
+ * prompt on its way to scrolling another.
29
+ * 4. WHERE THE POINTER IS RESTING IS DRAWN STATE, NOT A TRANSITION. It is the
30
+ * one piece of pointer state no key has an equivalent for, so it travels
31
+ * beside the transition rather than inside it — see `pointerRestingCell`.
32
+ */
33
+ /** SGR-1006 (`ESC [ < b ; x ; y M|m`) — the one pointer reporting mode read. */
34
+ const SGR_POINTER_REPORT = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/gu;
35
+ /**
36
+ * The terminal reports no pointer at all until it is asked to, so the parser
37
+ * above reads nothing until these bytes are written. DECSET 1000 turns on normal
38
+ * tracking — presses, releases and the wheel. DECSET 1006 asks for them in SGR
39
+ * encoding: the one grammar `SGR_POINTER_REPORT` reads, and the only one that
40
+ * can state a cell past column 223 at all. DECSET 1003 widens that to motion
41
+ * with no button held — the only way a terminal ever says where the pointer is
42
+ * merely resting, and therefore the whole reason a hover highlight can exist
43
+ * at all. It is asked for last, so the widest tracking mode is the one asked
44
+ * for in the grammar already selected. The request lives beside the parser so a
45
+ * change to what is read cannot leave what is asked for behind.
46
+ */
47
+ export const POINTER_TRACKING_ON = "\x1b[?1000h\x1b[?1006h\x1b[?1003h";
48
+ /** The same three modes turned off, in exact reverse of the order they were
49
+ * asked for — a terminal left tracking writes reports into whatever runs after
50
+ * the cockpit exits. */
51
+ export const POINTER_TRACKING_OFF = "\x1b[?1003l\x1b[?1006l\x1b[?1000l";
52
+ /**
53
+ * The signals that end a process where nothing else repays the loan: they run
54
+ * no `finally`, unwind no stack and unmount no renderer, so a surface killed by
55
+ * one would leave the operator's terminal reporting a pointer into whatever
56
+ * shell comes next. Listening for them is the only way to hand the modes back.
57
+ *
58
+ * The roster is every catchable POSIX terminator an interactive cockpit is
59
+ * actually killed by — ⌃C, a `kill`, a closed terminal, and ⌃\ — not a
60
+ * shortlist of the three that came to mind. A signal missing from here is a
61
+ * terminal left reporting, so the test that guards it names its own required
62
+ * set rather than reading this one back.
63
+ */
64
+ export const POINTER_RELEASE_SIGNALS = [
65
+ "SIGINT",
66
+ "SIGTERM",
67
+ "SIGHUP",
68
+ "SIGQUIT",
69
+ ];
70
+ /**
71
+ * The roster a given host can actually receive. SIGQUIT is a POSIX terminator
72
+ * with no Windows counterpart — libuv refuses the listener there — so the one
73
+ * platform that can never be killed by it is the one that does not listen for
74
+ * it. Every other platform listens for all four.
75
+ */
76
+ function releaseSignals(host) {
77
+ return (host.platform ?? process.platform) === "win32"
78
+ ? POINTER_RELEASE_SIGNALS.filter((signal) => signal !== "SIGQUIT")
79
+ : POINTER_RELEASE_SIGNALS;
80
+ }
81
+ /**
82
+ * WHERE THE POINTER IS RESTING — the fourth law's state, and the only pointer
83
+ * state the keyboard has no equivalent for.
84
+ *
85
+ * It is drawn state and nothing else: no key produces it, no interaction
86
+ * transition carries it, no journal records it, nothing reads it back to decide
87
+ * anything, and no capture ever asks for it. So it travels BESIDE the
88
+ * transition `applyPointerReport` returns rather than inside it, which is what
89
+ * lets a resting pointer stay no transition at all — it moves no marker, opens
90
+ * no view and selects nothing.
91
+ *
92
+ * Both writers are in this file, and both are the terminal telling us something
93
+ * rather than a surface deciding something: every report states where the
94
+ * pointer now is, and the tracking loan's release states that the terminal has
95
+ * stopped saying. Release restores exactly the state that stands before the
96
+ * first report ever arrives, so a surface nobody has pointed at yet and a
97
+ * surface whose loan was repaid are the same surface.
98
+ *
99
+ * ponytail: one process drives one terminal and a terminal has one pointer, so
100
+ * this is one cell rather than a store per surface; make it surface-owned if a
101
+ * process ever paints two cockpits at once.
102
+ */
103
+ let restingCell = null;
104
+ const restWatchers = new Set();
105
+ function reportPointerRest(cell) {
106
+ if (cell === null
107
+ ? restingCell === null
108
+ : restingCell !== null && restingCell.column === cell.column
109
+ && restingCell.row === cell.row)
110
+ return;
111
+ // Copied rather than aliased: a report object stays its reader's own.
112
+ restingCell = cell === null ? null : { column: cell.column, row: cell.row };
113
+ for (const watcher of restWatchers)
114
+ watcher();
115
+ }
116
+ /**
117
+ * The cell the pointer is resting on, or none. The same reference for as long
118
+ * as it has not moved, so a watcher redraws when the pointer moves and at no
119
+ * other time.
120
+ */
121
+ export function pointerRestingCell() {
122
+ return restingCell;
123
+ }
124
+ /** Watch the resting cell. The returned call stops watching. */
125
+ export function watchPointerRest(watcher) {
126
+ restWatchers.add(watcher);
127
+ return () => {
128
+ restWatchers.delete(watcher);
129
+ };
130
+ }
131
+ /**
132
+ * THE SESSION PANEL OVERRIDE — the rail columns the operator dragged the
133
+ * rail/body boundary to, this session only. Like the resting cell it is drawn
134
+ * state no key has an equivalent for, so it travels beside the transitions
135
+ * `applyPointerReport` returns rather than inside one: a drag moves no marker,
136
+ * opens no view and selects nothing, it re-PLANS. The override is an input to
137
+ * planFrame (`FrameState.railColumns`), never a bypass of it — the plan
138
+ * recomputes from the measured size plus this override, clamps it at the
139
+ * panels' readable floors, and the renderer draws the recomputed plan
140
+ * unmodified, so drawn still equals planned everywhere.
141
+ *
142
+ * It lives only in the running process: nothing writes it to disk, the
143
+ * journal, or anywhere else, and a relaunch starts with none — the default
144
+ * layout draws. The process boundary is marked by the tracking loan: a fresh
145
+ * `borrowPointerTracking` is a fresh session and clears whatever a previous
146
+ * session dragged to.
147
+ */
148
+ let railOverride = null;
149
+ const overrideWatchers = new Set();
150
+ /** Set by a press on the panel boundary, released by the button's release. */
151
+ let boundaryDrag = false;
152
+ function reportRailOverride(columns) {
153
+ if (railOverride === columns)
154
+ return;
155
+ railOverride = columns;
156
+ for (const watcher of overrideWatchers)
157
+ watcher();
158
+ }
159
+ /**
160
+ * The rail columns the session dragged the boundary to, or none. The same
161
+ * reference for as long as it has not changed, so a watcher redraws when the
162
+ * drag moves the boundary and at no other time.
163
+ */
164
+ export function sessionRailOverride() {
165
+ return railOverride;
166
+ }
167
+ /** Watch the session override. The returned call stops watching. */
168
+ export function watchSessionRailOverride(watcher) {
169
+ overrideWatchers.add(watcher);
170
+ return () => {
171
+ overrideWatchers.delete(watcher);
172
+ };
173
+ }
174
+ /**
175
+ * The session boundary the relaunch contract is stated against: the override
176
+ * and any drag in progress are gone, exactly as a new process starts. The
177
+ * production marker is the tracking loan — a fresh borrow is a fresh session.
178
+ */
179
+ export function resetSessionRailOverride() {
180
+ boundaryDrag = false;
181
+ reportRailOverride(null);
182
+ }
183
+ /**
184
+ * Borrow pointer reporting, and return the one way to repay it.
185
+ *
186
+ * The loan is the whole lifecycle in one expression, so no caller can hold half
187
+ * of it. Only an interactive terminal is asked at all: writing mode requests
188
+ * into a pipe puts escape bytes in a capture rather than a mouse on a surface
189
+ * nobody is pointing at, so the non-tty and CI surfaces carry no enable
190
+ * sequence because none is ever written — not because a later guard strips one.
191
+ *
192
+ * Repayment is idempotent and reachable from every exit: the returned release
193
+ * for an ordinary return, the same release run from a `finally` or a renderer's
194
+ * cleanup for a thrown failure, and the signal listeners registered here for
195
+ * the exits that run neither. A listener repays and then re-raises its own
196
+ * signal with our listener removed, so the process still ends exactly the way
197
+ * the signal meant it to.
198
+ */
199
+ export function borrowPointerTracking(terminal, host = process) {
200
+ // A fresh loan is a fresh session: the override a previous session dragged
201
+ // the panel boundary to dies with it — a relaunch draws the original layout.
202
+ resetSessionRailOverride();
203
+ if (terminal.isTTY !== true)
204
+ return () => { };
205
+ const listeners = [];
206
+ let lent = true;
207
+ const release = () => {
208
+ if (!lent)
209
+ return;
210
+ lent = false;
211
+ for (const [signal, listener] of listeners)
212
+ host.off(signal, listener);
213
+ // The terminal stops saying where the pointer is, so nothing is resting
214
+ // anywhere any more — and a frame drawn after this draws no highlight.
215
+ reportPointerRest(null);
216
+ terminal.write(POINTER_TRACKING_OFF);
217
+ };
218
+ for (const signal of releaseSignals(host)) {
219
+ const listener = () => {
220
+ release();
221
+ host.kill(host.pid, signal);
222
+ };
223
+ listeners.push([signal, listener]);
224
+ host.on(signal, listener);
225
+ }
226
+ // Asked for only once the repayment path stands: a loan taken before that
227
+ // could be one the signal listeners were never registered for.
228
+ terminal.write(POINTER_TRACKING_ON);
229
+ return release;
230
+ }
231
+ /** SGR-1006 encodes the wheel in bit 6 and pointer motion in bit 5. */
232
+ const WHEEL_BIT = 64;
233
+ const MOTION_BIT = 32;
234
+ /**
235
+ * The pointer reports carried by a chunk of terminal input. Bytes that are not
236
+ * a report are not pointer input and are left for whoever else reads the
237
+ * stream; a torn or malformed sequence simply reports nothing.
238
+ */
239
+ export function parsePointerReports(bytes) {
240
+ const reports = [];
241
+ for (const match of bytes.matchAll(SGR_POINTER_REPORT)) {
242
+ const button = Number(match[1]);
243
+ // The terminal counts cells from one; the plan counts them from zero.
244
+ const column = Number(match[2]) - 1;
245
+ const row = Number(match[3]) - 1;
246
+ if (column < 0 || row < 0)
247
+ continue;
248
+ const action = (button & WHEEL_BIT) !== 0
249
+ ? ((button & 1) === 0 ? "wheel-up" : "wheel-down")
250
+ : (button & MOTION_BIT) !== 0
251
+ ? "move"
252
+ : match[4] === "m"
253
+ ? "release"
254
+ : "press";
255
+ reports.push({ action, column, row, button });
256
+ }
257
+ return reports;
258
+ }
259
+ /**
260
+ * The longest suffix of `bytes` that is still on its way to being a report: an
261
+ * escape whose `M` or `m` has not arrived yet. A completed report never matches
262
+ * (it ends at its own terminator), so a carried tail can never be reported
263
+ * twice. ponytail: a tail longer than any real report is not one — it is
264
+ * dropped rather than accumulated, which is also what bounds this buffer.
265
+ */
266
+ const TORN_POINTER_REPORT = /\x1b(?:\[(?:<[\d;]*)?)?$/u;
267
+ const MAX_TORN_REPORT_BYTES = 32;
268
+ function tornPointerTail(bytes) {
269
+ const torn = TORN_POINTER_REPORT.exec(bytes)?.[0] ?? "";
270
+ return torn.length > MAX_TORN_REPORT_BYTES ? "" : torn;
271
+ }
272
+ /**
273
+ * A reader over the input stream rather than over one chunk. The terminal
274
+ * writes bytes, not messages: a single report is free to arrive split across
275
+ * chunks, and chunk-at-a-time parsing silently drops every report that lands on
276
+ * a boundary. The reader carries the torn tail into the next chunk and reports
277
+ * only sequences that have completed.
278
+ *
279
+ * It also states what was left — the bytes of that chunk that belonged to no
280
+ * report, including none at all while a torn one is still arriving. Reports and
281
+ * keys share one stream, so this is the only place that can tell them apart:
282
+ * downstream, a whole report is an unnameable escape sequence and half a report
283
+ * is the single character it happens to end on, either of which would be read as
284
+ * a keystroke by whatever scope is live.
285
+ */
286
+ export function createPointerReportReader() {
287
+ let carry = "";
288
+ const readComplete = (complete) => {
289
+ const tokens = [];
290
+ let offset = 0;
291
+ for (const match of complete.matchAll(SGR_POINTER_REPORT)) {
292
+ const index = match.index;
293
+ if (index > offset) {
294
+ tokens.push({ type: "keys", bytes: complete.slice(offset, index) });
295
+ }
296
+ const report = parsePointerReports(match[0])[0];
297
+ if (report !== undefined)
298
+ tokens.push({ type: "pointer", report });
299
+ offset = index + match[0].length;
300
+ }
301
+ if (offset < complete.length) {
302
+ tokens.push({ type: "keys", bytes: complete.slice(offset) });
303
+ }
304
+ return {
305
+ tokens,
306
+ reports: tokens.flatMap((token) => token.type === "pointer" ? [token.report] : []),
307
+ keys: tokens.flatMap((token) => token.type === "keys" ? [token.bytes] : [])
308
+ .join(""),
309
+ };
310
+ };
311
+ const reader = ((chunk) => {
312
+ const bytes = carry + chunk;
313
+ carry = tornPointerTail(bytes);
314
+ const complete = carry.length > 0 ? bytes.slice(0, -carry.length) : bytes;
315
+ return readComplete(complete);
316
+ });
317
+ reader.flush = () => {
318
+ const pending = carry;
319
+ carry = "";
320
+ return readComplete(pending);
321
+ };
322
+ reader.pending = () => carry;
323
+ return reader;
324
+ }
325
+ /**
326
+ * The width the key registry resolves at for the bands THIS plan drew. The
327
+ * focus order follows the plan rather than a second reading of the terminal:
328
+ * between 64 and 79 columns the plan draws the rail at a width the key floor
329
+ * alone would deny it, and the click targets and the focus order have to agree
330
+ * about that band or a click would act on a panel the frame does not show.
331
+ */
332
+ export function plannedKeyColumns(plan) {
333
+ return plan.band === "sidebar"
334
+ ? Math.max(plan.size.columns, RUN_SIDE_RAIL_COLUMN_FLOOR)
335
+ : plan.size.columns;
336
+ }
337
+ /** Which planned band holds which focusable panel — the frame's own two. */
338
+ const REGION_PANELS = {
339
+ rail: "VIEWS",
340
+ body: "CONTENT",
341
+ // The item rows are the body band's own nested band: a cell on them is a cell
342
+ // in the body, and focusing it focuses the panel the body draws.
343
+ items: "CONTENT",
344
+ };
345
+ /** The tightest planned region containing a cell — regions nest (the item rows
346
+ * refine the body band, the caption the header's row), so the smallest one owns
347
+ * the cell. */
348
+ function plannedRegionAt(plan, cell) {
349
+ let hit;
350
+ for (const region of plan.regions) {
351
+ if (!contains(region, cell))
352
+ continue;
353
+ if (hit === undefined || region.rows * region.columns < hit.rows * hit.columns) {
354
+ hit = region;
355
+ }
356
+ }
357
+ return hit;
358
+ }
359
+ /**
360
+ * What the plan says is under a cell. The rail's view rows are
361
+ * `plan.sidebar.viewRows` — frame rows planFrame itself computed, where the
362
+ * label row was decided — so the menu's shape is looked up rather than
363
+ * reconstructed from `menuRows` out here. The body's item rows are a planned
364
+ * band of their own, so the tightest-region lookup lands on them directly and
365
+ * the first item's offset is the region's own row. The view strip the plan draws
366
+ * instead of a rail below 64 columns is one span with no per-view columns in it,
367
+ * so nothing here invents any: a strip cell resolves to the strip band and no
368
+ * further.
369
+ */
370
+ export function resolvePointerTarget(plan, cell) {
371
+ const region = plannedRegionAt(plan, cell);
372
+ if (region === undefined)
373
+ return undefined;
374
+ if (region.id === "rail" && plan.sidebar !== null && plan.tab === "watch") {
375
+ const rows = plan.sidebar.viewRows;
376
+ const view = FRAME_VIEWS.findIndex((name) => rows[name] === cell.row);
377
+ return view < 0 ? { region } : { region, view };
378
+ }
379
+ if (region.id === "items")
380
+ return { region, row: cell.row - region.row };
381
+ return { region };
382
+ }
383
+ function contains(region, cell) {
384
+ return cell.row >= region.row
385
+ && cell.row < region.row + region.rows
386
+ && cell.column >= region.column
387
+ && cell.column < region.column + region.columns;
388
+ }
389
+ /**
390
+ * The focus index a target's band carries at the width the plan drew — the same
391
+ * order the frame paints its focus ring from, so a click can never focus a
392
+ * panel the frame does not show as focusable.
393
+ */
394
+ export function pointerFocusPanel(plan, target) {
395
+ const panel = REGION_PANELS[target.region.id];
396
+ if (panel === undefined)
397
+ return undefined;
398
+ const index = runPanelFocusOrder(plannedKeyColumns(plan)).indexOf(panel);
399
+ return index < 0 ? undefined : index;
400
+ }
401
+ /**
402
+ * The item a cell acts on — THE resolution, shared by every pointer path.
403
+ * A click reads it to decide what to select, and a hover highlight reads the
404
+ * same call to decide what to mark, so the highlight cannot name a row a click
405
+ * at that cell would miss: there is one answer, not two agreeing ones. A cell
406
+ * the plan does not place on an item row, and an item row the paint drew
407
+ * nothing into, are both nothing here rather than a nearest guess.
408
+ */
409
+ export function pointerRowAt(surface, cell) {
410
+ const target = resolvePointerTarget(surface.plan, cell);
411
+ return target?.row === undefined ? undefined : surface.drawnRowIds[target.row];
412
+ }
413
+ /**
414
+ * The frame's one panel boundary: the rail's own last column, the grab cell
415
+ * beside the body's border. The body's border cell itself stays a click
416
+ * target of the body panel — a press there retires the prompt and focuses
417
+ * content exactly as the keyboard's grammar advertises — so the drag handle
418
+ * is the cell this side of it — on the rows the plan gives no other target.
419
+ * A view row's last column already belongs to the view itself, so the handle
420
+ * yields those rows to the plan's own resolution. Read off the plan's own
421
+ * regions like every other target; a band with no rail has no boundary.
422
+ */
423
+ function panelBoundaryCell(plan, cell) {
424
+ if (plan.band !== "sidebar")
425
+ return false;
426
+ const rail = plan.regions.find((region) => region.id === "rail");
427
+ if (rail === undefined
428
+ || cell.column !== rail.column + rail.columns - 1
429
+ || cell.row < rail.row
430
+ || cell.row >= rail.row + rail.rows) {
431
+ return false;
432
+ }
433
+ // A view row's cell is the view's own click target wherever in the rail the
434
+ // plan puts it — the grab column included (the first law: hit-testing
435
+ // resolves through the plan, and the plan resolves that cell to the view).
436
+ // The handle owns the rail's remaining rows; at the readable floor the
437
+ // rail's last column sits on the final letter of a drawn view name, and a
438
+ // press there is still that view's number key, not a grab.
439
+ return plan.sidebar === null || plan.tab !== "watch" || FRAME_VIEWS.every((name) => plan.sidebar.viewRows[name] !== cell.row);
440
+ }
441
+ /**
442
+ * The transition a pointer report makes, or undefined when nothing owns the
443
+ * cell. A click on a view name is that view's own number key; a click on the
444
+ * row already marked is ⏎; moving between panels is the advertised dive/back
445
+ * grammar; and a wheel is ↑↓ only when the panel under it already owns ↑↓.
446
+ * An off-focus wheel is not an action: borrowing focus or prompt scope and then
447
+ * restoring it would create a frame no advertised key sequence can reach.
448
+ *
449
+ * A drag of the panel boundary is the exception that proves the second law:
450
+ * it is no transition at all. The press landing on the boundary grabs it; each
451
+ * move hands the plan a new input through the session override; the release
452
+ * lets go — and when the release never arrives because the pointer was let go
453
+ * outside the window, the first no-button motion says so in its stead, so no
454
+ * latch outlives the drag it latched. The interaction comes back untouched —
455
+ * the drag re-plans the frame rather than moving anything inside it.
456
+ */
457
+ export function applyPointerReport(report, surface) {
458
+ // Every report states where the pointer now is, whatever else it does or does
459
+ // not do — a move above all, whose whole content is that. This is the one
460
+ // call the live delivery makes for every report it receives, so the resting
461
+ // cell a frame draws its highlight from is fed by the production input path
462
+ // rather than by anything a caller supplies beside it.
463
+ reportPointerRest(report);
464
+ if (report.action === "press" && panelBoundaryCell(surface.plan, report)) {
465
+ boundaryDrag = true;
466
+ return undefined;
467
+ }
468
+ if (boundaryDrag) {
469
+ // A drag-motion — a move with a button still held — is the drag's only
470
+ // content. The requested rail width is where the rail's last column was
471
+ // dragged to; the plan owns the floors and clamps it there, so a drag
472
+ // past a panel's readable floor leaves the panel at the floor rather
473
+ // than collapsing it.
474
+ if (report.action === "move"
475
+ && (report.button === undefined || (report.button & 3) !== 3)) {
476
+ reportRailOverride(report.column + 1);
477
+ return undefined;
478
+ }
479
+ // Every other report ENDS the drag rather than feeding it or being
480
+ // swallowed by it. A release lets go. A no-button motion is the terminal
481
+ // saying the button is already up — the only word of the release that
482
+ // never arrives when the pointer is let go outside the window, and the
483
+ // report that would otherwise keep resizing the panel on a bare hover
484
+ // for the rest of the session. And anything else — a press, the wheel —
485
+ // is a fresh action a dead latch may not eat: the latch dies with the
486
+ // drag and the report takes its ordinary route.
487
+ boundaryDrag = false;
488
+ if (report.action === "release")
489
+ return undefined;
490
+ }
491
+ return pointerTransition(report, surface);
492
+ }
493
+ /**
494
+ * The transition alone, with no report of where the pointer now is. Asking what
495
+ * a press WOULD do is not the pointer coming to rest anywhere, so the hover
496
+ * resolution below reaches the click's own route through here rather than
497
+ * through the entry point that also moves the resting cell.
498
+ */
499
+ function pointerTransition(report, surface) {
500
+ const target = resolvePointerTarget(surface.plan, report);
501
+ if (target === undefined)
502
+ return undefined;
503
+ const panel = pointerFocusPanel(surface.plan, target);
504
+ const columns = plannedKeyColumns(surface.plan);
505
+ const focusOrder = runPanelFocusOrder(columns);
506
+ const focusedPanel = (state) => state.panel % focusOrder.length;
507
+ const dispatch = (event, from) => dispatchRunKey(event, from, RUN_INPUT_BINDINGS, surface.rowIds, columns);
508
+ if (report.action === "wheel-up" || report.action === "wheel-down") {
509
+ if (panel === undefined || focusedPanel(surface.interaction) !== panel) {
510
+ return undefined;
511
+ }
512
+ return dispatch({
513
+ input: "",
514
+ key: report.action === "wheel-up"
515
+ ? { upArrow: true }
516
+ : { downArrow: true },
517
+ }, surface.interaction);
518
+ }
519
+ if (report.action !== "press")
520
+ return undefined;
521
+ // A click is outside the prompt's input class. Apply the prompt through its
522
+ // own advertised Enter before dispatching the click's advertised route.
523
+ let state = surface.interaction;
524
+ if (state.filterPrompt) {
525
+ const applied = dispatch({ input: "", key: { return: true } }, state);
526
+ if (applied === undefined)
527
+ return undefined;
528
+ state = applied;
529
+ }
530
+ if (target.view !== undefined) {
531
+ // Number keys are one-based positions in the same rail order.
532
+ return dispatch({ input: String(target.view + 1), key: {} }, state);
533
+ }
534
+ // Focus is never assigned. Dive or back through the advertised grammar until
535
+ // the target panel owns the keyboard, including closing an open detail before
536
+ // backing from content to the rail.
537
+ if (panel !== undefined) {
538
+ for (let attempts = 0; focusedPanel(state) !== panel && attempts < 2; attempts += 1) {
539
+ const next = dispatch(panel === focusOrder.indexOf("CONTENT")
540
+ ? { input: "", key: { return: true } }
541
+ : { input: "", key: { leftArrow: true } }, state);
542
+ if (next === undefined || next === state)
543
+ return undefined;
544
+ state = next;
545
+ }
546
+ if (focusedPanel(state) !== panel)
547
+ return undefined;
548
+ }
549
+ if (target.row === undefined)
550
+ return state;
551
+ // Diving from a mismatched rail marker may legitimately open a different
552
+ // view. The clicked row belonged to the old committed frame, so no stale row
553
+ // transition follows that key-owned view change.
554
+ if (state.activeView !== surface.interaction.activeView)
555
+ return state;
556
+ const id = pointerRowAt(surface, report);
557
+ const targetIndex = id === undefined ? -1 : surface.rowIds.indexOf(id);
558
+ if (id === undefined || targetIndex < 0)
559
+ return state;
560
+ if (id === state.selection) {
561
+ return dispatch({ input: "", key: { return: true } }, state) ?? state;
562
+ }
563
+ // A row click is the same finite ↑↓ journey from the current marker. Work on
564
+ // a candidate state so an unreachable target causes no partial transition.
565
+ let candidate = state;
566
+ for (let attempts = 0; attempts <= surface.rowIds.length; attempts += 1) {
567
+ if (candidate.selection === id)
568
+ return candidate;
569
+ const currentIndex = candidate.selection === null
570
+ ? -1
571
+ : surface.rowIds.indexOf(candidate.selection);
572
+ const next = dispatch({
573
+ input: "",
574
+ key: currentIndex > targetIndex
575
+ ? { upArrow: true }
576
+ : { downArrow: true },
577
+ }, candidate);
578
+ if (next === undefined || next === candidate)
579
+ return undefined;
580
+ candidate = next;
581
+ }
582
+ return undefined;
583
+ }
584
+ /**
585
+ * The item a hover highlight marks: the one a click at that cell would ACT ON.
586
+ *
587
+ * `pointerRowAt` answers a smaller question — which drawn row the plan places
588
+ * under the cell — and a click is not only that lookup. It is a route: the /
589
+ * prompt applied, focus dived or backed to the panel under the pointer, and only
590
+ * then the row. Any of those may legitimately land somewhere else, the plainest
591
+ * case is the rail marker standing on a view the body is not drawing: the click
592
+ * that dives into content opens the MARKED view and selects no row at all, while
593
+ * the rows under the pointer belong to the view being left behind.
594
+ *
595
+ * So the highlight is not decided by the lookup. It is decided by running the
596
+ * click's own transition and asking what it settled on — the same
597
+ * `pointerTransition` the live report path dispatches through, on the same
598
+ * surface, so an answer these two could disagree about is not expressible. A
599
+ * click that acts on something else, or on nothing, lights nothing up.
600
+ *
601
+ * It reports no rest and returns no state: asking is not pointing and not
602
+ * clicking, so the surface is exactly as it was.
603
+ */
604
+ export function pointerHoverRow(surface, cell) {
605
+ const id = pointerRowAt(surface, cell);
606
+ if (id === undefined)
607
+ return undefined;
608
+ const clicked = pointerTransition({ action: "press", ...cell }, surface);
609
+ return clicked?.selection === id ? id : undefined;
610
+ }
@@ -7,7 +7,11 @@ import { type FramePlan, type FrameRegion, type FrameState, type FrameCockpitLay
7
7
  export { deriveRunCockpitData } from "./derive.js";
8
8
  export type { RunCockpitData } from "./derive.js";
9
9
  export { PANEL_CHROME_ROWS } from "./components.js";
10
- /** The width focus and key routing resolve at. */
10
+ /**
11
+ * The width focus and key routing resolve at, from a measured width — the form
12
+ * used before a plan exists. Once one does, `plannedKeyColumns` (pointer.ts)
13
+ * reads the same answer off the bands the plan actually drew.
14
+ */
11
15
  export declare function runKeyColumns(columns: number): number;
12
16
  /**
13
17
  * How many rows one line of body text occupies once the renderer wraps it into
@@ -25,11 +29,25 @@ export declare function panelRows(lines: readonly string[], columns: number): nu
25
29
  * identities assigned by the production journal derivation.
26
30
  */
27
31
  export declare function deriveRunViewRows(data: RunCockpitData, viewId: RunViewId, filterQuery?: string): readonly JournalRow[];
32
+ /**
33
+ * The identities the body's rows carry in the order the body draws them — the
34
+ * same window `SizedJournalPanel` paints, so a hit on a body row resolves to
35
+ * the row an operator is looking at rather than to a position in a collection
36
+ * the frame scrolled past. A body that draws no selectable list (the overview,
37
+ * the journal tail, an open detail, the help overlay, the decisions tab) owns
38
+ * no rows here.
39
+ */
40
+ export declare function drawnRunViewRowIds(data: RunCockpitData, interaction: RunInteractionState, bodyRows: number): readonly string[];
28
41
  /** What each drawn region draws: the resolved budgets its panels consume. */
29
42
  export type RegionContent = {
30
- readonly body: number;
43
+ /**
44
+ * Where the body band's item rows are drawn and how many there are: the
45
+ * plan's own `items` region, carried here unmodified. This file subtracts no
46
+ * chrome to obtain it — planFrame plans it, the paint draws its list at it,
47
+ * and hit resolution (pointer.ts) reads the same region off the same plan.
48
+ */
49
+ readonly items: FrameRegion;
31
50
  readonly journal: number;
32
- readonly bodyColumns: number;
33
51
  readonly sideRails: boolean;
34
52
  readonly stats: FrameCockpitLayout["stats"]["mode"] | "none";
35
53
  readonly progressBar: boolean;
@@ -53,20 +71,33 @@ export type PlannedRunCockpitFrame = {
53
71
  readonly keyEntries: readonly KeybarEntry[];
54
72
  };
55
73
  /** Resolve every draw-time input; the plan is planFrame's own return, consumed unmodified. */
56
- export declare function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection, }: {
74
+ export declare function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection, railColumns, }: {
57
75
  data: RunCockpitData;
58
76
  columns: number;
59
77
  rows?: number;
60
78
  interaction?: RunInteractionState;
61
79
  keyProjection?: RunKeyProjection;
80
+ /**
81
+ * The session's panel resize override, handed to planFrame as an input —
82
+ * never a bypass of it: the plan recomputes from the measured size plus this
83
+ * override and what is drawn is what the plan returns.
84
+ */
85
+ railColumns?: number;
62
86
  }): PlannedRunCockpitFrame;
63
87
  /** The public surface always enters paint through one measured plan. */
64
- export declare function RunCockpitFrame({ data, columns, rows, interaction, keyProjection, }: {
88
+ export declare function RunCockpitFrame({ data, columns, rows, interaction, keyProjection, onCommittedFrame, }: {
65
89
  data: RunCockpitData;
66
90
  columns: number;
67
91
  rows?: number;
68
92
  interaction?: RunInteractionState;
69
93
  keyProjection?: RunKeyProjection;
94
+ /**
95
+ * The renderer's commit seam: once this frame is committed, the exact plan
96
+ * and inputs it was drawn from are handed back, so an input surface can
97
+ * resolve its hits against the frame on the screen rather than against a
98
+ * plan it derived for itself. What nothing has painted yet has no geometry.
99
+ */
100
+ onCommittedFrame?: (planned: PlannedRunCockpitFrame, data: RunCockpitData) => void;
70
101
  }): ReactElement;
71
102
  /**
72
103
  * Assert the plan's tiled extents, the bytes production paints, and — given