tickmarkr 1.79.0 → 1.81.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.
@@ -0,0 +1,390 @@
1
+ export const SPARKLINE_BUCKET_WINDOW = 12;
2
+ const MINUTE_MS = 60_000;
3
+ const SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES = [
4
+ 1,
5
+ 5,
6
+ 10,
7
+ 30,
8
+ 60,
9
+ 120,
10
+ 360,
11
+ 720,
12
+ 1_440,
13
+ ];
14
+ export function selectSparklineBucketWidthMs(elapsedMs) {
15
+ const safeElapsedMs = Math.max(0, Number.isFinite(elapsedMs) ? elapsedMs : 0);
16
+ const requiredMinutes = safeElapsedMs / SPARKLINE_BUCKET_WINDOW / MINUTE_MS;
17
+ const selected = SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES.find((minutes) => minutes >= requiredMinutes);
18
+ if (selected !== undefined)
19
+ return selected * MINUTE_MS;
20
+ let coarsened = SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES.at(-1);
21
+ while (coarsened < requiredMinutes)
22
+ coarsened *= 2;
23
+ return coarsened * MINUTE_MS;
24
+ }
25
+ function recordValue(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value)
27
+ ? value
28
+ : undefined;
29
+ }
30
+ function parseSource(raw) {
31
+ const events = [];
32
+ const defects = [];
33
+ for (const [index, line] of raw.split("\n").entries()) {
34
+ if (!line.trim())
35
+ continue;
36
+ try {
37
+ const parsed = recordValue(JSON.parse(line));
38
+ const data = recordValue(parsed?.data);
39
+ if (!parsed
40
+ || typeof parsed.ts !== "string"
41
+ || typeof parsed.event !== "string"
42
+ || !data
43
+ || (parsed.taskId !== undefined && typeof parsed.taskId !== "string")) {
44
+ defects.push({ line: index + 1, raw: line });
45
+ continue;
46
+ }
47
+ events.push({
48
+ line: index + 1,
49
+ ts: parsed.ts,
50
+ event: parsed.event,
51
+ ...(typeof parsed.taskId === "string" ? { taskId: parsed.taskId } : {}),
52
+ data,
53
+ });
54
+ }
55
+ catch {
56
+ defects.push({ line: index + 1, raw: line });
57
+ }
58
+ }
59
+ return { events, defects };
60
+ }
61
+ function assignmentFrom(value) {
62
+ const assignment = recordValue(value);
63
+ return assignment
64
+ && typeof assignment.adapter === "string"
65
+ && typeof assignment.model === "string"
66
+ ? { adapter: assignment.adapter, model: assignment.model }
67
+ : undefined;
68
+ }
69
+ function elapsedReading(first, last) {
70
+ const seconds = Math.max(0, Math.floor((Date.parse(last) - Date.parse(first)) / 1_000));
71
+ const hours = Math.floor(seconds / 3_600);
72
+ const minutes = Math.floor((seconds % 3_600) / 60);
73
+ const remainder = seconds % 60;
74
+ const padded = (part) => String(part).padStart(2, "0");
75
+ return `${padded(hours)}:${padded(minutes)}:${padded(remainder)}`;
76
+ }
77
+ function defaultDaemonLiveness(pid) {
78
+ try {
79
+ process.kill(pid, 0);
80
+ return true;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ function latestLifecycle(events) {
87
+ for (let index = events.length - 1; index >= 0; index -= 1) {
88
+ const event = events[index].event;
89
+ if (event === "superseded")
90
+ return "superseded";
91
+ if (event === "run-end")
92
+ return "completed";
93
+ if (event === "run-start" || event === "run-resume")
94
+ return "active";
95
+ }
96
+ return "active";
97
+ }
98
+ function daemonPid(events) {
99
+ for (let index = events.length - 1; index >= 0; index -= 1) {
100
+ const event = events[index];
101
+ if ((event.event === "run-start" || event.event === "run-resume")
102
+ && typeof event.data.pid === "number") {
103
+ return event.data.pid;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+ function deriveTasks(events, interrupted) {
109
+ const tasks = new Map();
110
+ const task = (taskId, index, ts) => {
111
+ const existing = tasks.get(taskId);
112
+ if (existing)
113
+ return existing;
114
+ const created = {
115
+ id: taskId,
116
+ dispatches: [],
117
+ state: "pending",
118
+ lastIndex: index,
119
+ lastTs: ts,
120
+ phase: "pending",
121
+ };
122
+ tasks.set(taskId, created);
123
+ return created;
124
+ };
125
+ for (const [index, event] of events.entries()) {
126
+ if (!event.taskId)
127
+ continue;
128
+ if (event.event === "task-dispatch") {
129
+ const fact = task(event.taskId, index, event.ts);
130
+ const recordedAttempt = event.data.attempt;
131
+ fact.dispatches.push({
132
+ index,
133
+ ts: event.ts,
134
+ attempt: typeof recordedAttempt === "number"
135
+ ? Math.max(1, Math.floor(recordedAttempt) + 1)
136
+ : fact.dispatches.length + 1,
137
+ assignment: assignmentFrom(event.data.assignment),
138
+ });
139
+ fact.state = "running";
140
+ fact.phase = "worker";
141
+ fact.lastIndex = index;
142
+ fact.lastTs = event.ts;
143
+ continue;
144
+ }
145
+ if (event.event === "phase-start") {
146
+ const fact = task(event.taskId, index, event.ts);
147
+ fact.phase = typeof event.data.phase === "string" ? event.data.phase : fact.phase;
148
+ fact.lastIndex = index;
149
+ fact.lastTs = event.ts;
150
+ continue;
151
+ }
152
+ const state = {
153
+ "task-done": "done",
154
+ "task-failed": "failed",
155
+ "task-human": "human",
156
+ "task-approved": "pending",
157
+ }[event.event];
158
+ if (state) {
159
+ const fact = task(event.taskId, index, event.ts);
160
+ fact.state = state;
161
+ fact.lastIndex = index;
162
+ fact.lastTs = event.ts;
163
+ }
164
+ }
165
+ if (interrupted) {
166
+ for (const fact of tasks.values()) {
167
+ if (fact.state === "running" || fact.state === "pending") {
168
+ fact.state = "interrupted";
169
+ }
170
+ }
171
+ }
172
+ return tasks;
173
+ }
174
+ function taskAttempt(fact) {
175
+ return fact.dispatches.at(-1)?.attempt ?? Math.max(1, fact.dispatches.length);
176
+ }
177
+ function taskActor(fact) {
178
+ const assignment = fact.dispatches.at(-1)?.assignment;
179
+ return assignment ? `${assignment.adapter}:${assignment.model}` : "unassigned";
180
+ }
181
+ function changedActingAdapter(fact) {
182
+ const first = fact.dispatches[0]?.assignment?.adapter;
183
+ const last = fact.dispatches.at(-1)?.assignment?.adapter;
184
+ return first !== undefined && last !== undefined && first !== last;
185
+ }
186
+ function spotlightTask(tasks) {
187
+ return [...tasks.values()]
188
+ .filter((fact) => fact.dispatches.length > 0)
189
+ .sort((left, right) => Number(changedActingAdapter(right)) - Number(changedActingAdapter(left))
190
+ || taskAttempt(right) - taskAttempt(left)
191
+ || right.lastIndex - left.lastIndex)[0];
192
+ }
193
+ function defectRow(defect) {
194
+ const excerpt = defect.raw.trim().replace(/\s+/g, " ").slice(0, 64);
195
+ return {
196
+ id: `defect:${defect.line}`,
197
+ time: `L${defect.line}`,
198
+ state: "fail",
199
+ text: `fail · DEFECT · source line ${defect.line} unparseable${excerpt ? ` · ${excerpt}` : ""}`,
200
+ };
201
+ }
202
+ function eventTime(ts) {
203
+ const parsed = Date.parse(ts);
204
+ return Number.isFinite(parsed)
205
+ ? new Date(parsed).toISOString().slice(11, 19)
206
+ : ts.slice(0, 8);
207
+ }
208
+ /**
209
+ * The outcome an event records about itself, in every form the committed
210
+ * sources use: an unsuccessful result flag, an unsuccessful outcome flag, an
211
+ * event name reporting a failure, a non-zero exit status, or a run-end whose
212
+ * tip verification reads failed.
213
+ */
214
+ function recordsFailure(event) {
215
+ return event.event.includes("failed")
216
+ || event.data.pass === false
217
+ || event.data.ok === false
218
+ || (typeof event.data.exitCode === "number" && event.data.exitCode !== 0)
219
+ || (event.event === "run-end" && event.data.tipVerify === "failed");
220
+ }
221
+ function recordsSuccess(event) {
222
+ return event.event === "task-done"
223
+ || event.event === "merge"
224
+ || event.data.pass === true
225
+ || event.data.ok === true
226
+ || (event.event === "run-end" && event.data.tipVerify !== "failed");
227
+ }
228
+ /**
229
+ * A row depicts the event it names: the event's own recorded outcome decides
230
+ * the state, never the state its task reached later. Only a task with no
231
+ * terminal event in a run whose daemon is gone lends its interruption, because
232
+ * no event records that ending — the interruption is the run's present truth.
233
+ * Every state carries its word, so meaning never rides on hue alone.
234
+ */
235
+ function eventPresentation(event, task) {
236
+ if (event.event === "escalation" || event.event === "task-human") {
237
+ return { state: "warn", word: "warn" };
238
+ }
239
+ if (recordsFailure(event))
240
+ return { state: "fail", word: "fail" };
241
+ if (recordsSuccess(event))
242
+ return { state: "pass", word: "pass" };
243
+ if (task?.state === "interrupted") {
244
+ return { state: "warn", word: "interrupted" };
245
+ }
246
+ return { state: "neutral", word: "neutral" };
247
+ }
248
+ function historyRow(event, tasks, spotlight, newest, runStatus) {
249
+ const task = event.taskId === undefined ? undefined : tasks.get(event.taskId);
250
+ const { state, word } = eventPresentation(event, task);
251
+ // The spotlight entry must keep the task, its attempt and its acting adapter
252
+ // inside the narrowest band's width budget — that budget cannot pay for the
253
+ // separators and the event name, so the state word and the run's status
254
+ // (what the newest event recorded) ride after the adapter instead.
255
+ const text = newest && spotlight !== undefined
256
+ ? `${spotlight.id} attempt ${taskAttempt(spotlight)} ${taskActor(spotlight)} ${word}${word === runStatus ? "" : ` ${runStatus}`}`
257
+ : task !== undefined
258
+ ? `${task.id} ${word} · ${event.event}`
259
+ : `${word} · ${event.event}`;
260
+ return {
261
+ id: `event:${event.line}`,
262
+ time: eventTime(event.ts),
263
+ state,
264
+ text,
265
+ };
266
+ }
267
+ function journalRows(events, tasks, defects, runStatus) {
268
+ const spotlight = spotlightTask(tasks);
269
+ const history = [...events].reverse().map((event, index) => historyRow(event, tasks, spotlight, index === 0, runStatus));
270
+ return [...[...defects].reverse().map(defectRow), ...history];
271
+ }
272
+ function bucketedMetricSamples(events) {
273
+ const taskSamples = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => null);
274
+ const gateTotals = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => 0);
275
+ const gatePasses = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => 0);
276
+ const firstMs = Date.parse(events[0].ts);
277
+ const lastMs = Date.parse(events.at(-1).ts);
278
+ const bucketWidthMs = selectSparklineBucketWidthMs(lastMs - firstMs);
279
+ const windowStartMs = lastMs - bucketWidthMs * SPARKLINE_BUCKET_WINDOW;
280
+ const completedTasks = new Set();
281
+ for (const event of events) {
282
+ const eventMs = Date.parse(event.ts);
283
+ if (!Number.isFinite(eventMs))
284
+ continue;
285
+ const bucket = Math.max(0, Math.min(SPARKLINE_BUCKET_WINDOW - 1, Math.floor((eventMs - windowStartMs) / bucketWidthMs)));
286
+ if (event.event === "task-done"
287
+ && event.taskId
288
+ && !completedTasks.has(event.taskId)) {
289
+ completedTasks.add(event.taskId);
290
+ taskSamples[bucket] = (taskSamples[bucket] ?? 0) + 1;
291
+ }
292
+ if (event.event === "gate-result") {
293
+ gateTotals[bucket] = gateTotals[bucket] + 1;
294
+ if (event.data.pass === true) {
295
+ gatePasses[bucket] = gatePasses[bucket] + 1;
296
+ }
297
+ }
298
+ }
299
+ return {
300
+ tasks: taskSamples,
301
+ gates: gateTotals.map((total, index) => total === 0 ? null : gatePasses[index]),
302
+ passRate: gateTotals.map((total, index) => total === 0 ? null : Math.round((gatePasses[index] / total) * 100)),
303
+ };
304
+ }
305
+ function tipVerificationPassed(events) {
306
+ const eventVerdicts = events.flatMap((event) => {
307
+ if (event.event === "tip-verify-failed")
308
+ return [false];
309
+ if (event.event === "tip-verify")
310
+ return [event.data.pass === true];
311
+ return [];
312
+ });
313
+ const endVerdicts = events.flatMap((event) => event.event === "run-end" && typeof event.data.tipVerify === "string"
314
+ ? [event.data.tipVerify === "passed"]
315
+ : []);
316
+ return eventVerdicts.length > 0
317
+ && endVerdicts.length > 0
318
+ && eventVerdicts.every(Boolean)
319
+ && endVerdicts.every(Boolean);
320
+ }
321
+ export function deriveRunCockpitData(source, binaryVersion, options = {}) {
322
+ const { events, defects } = parseSource(source.raw);
323
+ if (events.length === 0)
324
+ throw new Error(`empty cockpit capture: ${source.fileName}`);
325
+ const start = events.find((event) => event.event === "run-start");
326
+ if (!start)
327
+ throw new Error(`capture has no run-start: ${source.fileName}`);
328
+ const lifecycle = latestLifecycle(events);
329
+ const pid = daemonPid(events);
330
+ const alive = lifecycle !== "active"
331
+ ? false
332
+ : pid !== undefined && (options.isDaemonAlive ?? defaultDaemonLiveness)(pid);
333
+ const interrupted = !alive;
334
+ const tasks = deriveTasks(events, interrupted);
335
+ const taskFacts = [...tasks.values()];
336
+ const done = taskFacts.filter((task) => task.state === "done").length;
337
+ const failed = taskFacts.filter((task) => task.state === "failed").length;
338
+ const human = taskFacts.filter((task) => task.state === "human").length;
339
+ const hasInterruptedTask = taskFacts.some((task) => task.state === "interrupted");
340
+ const gateEvents = events.filter((event) => event.event === "gate-result");
341
+ const passed = gateEvents.filter((event) => event.data.pass === true).length;
342
+ const passRate = gateEvents.length === 0 ? 0 : Math.round((passed / gateEvents.length) * 100);
343
+ const progress = tasks.size === 0 ? 0 : Math.round((done / tasks.size) * 100);
344
+ const escalated = events.filter((event) => event.event === "escalation").length;
345
+ const tipPassed = tipVerificationPassed(events);
346
+ const spotlight = spotlightTask(tasks);
347
+ const samples = bucketedMetricSamples(events);
348
+ const runStatus = failed > 0
349
+ ? "failed"
350
+ : lifecycle === "superseded" || hasInterruptedTask
351
+ ? "interrupted"
352
+ : lifecycle === "completed"
353
+ ? "done"
354
+ : interrupted
355
+ ? "interrupted"
356
+ : "running";
357
+ return {
358
+ binaryVersion,
359
+ runId: source.fileName.replace(/(?:\.interrupted)?\.journal\.jsonl$/, ""),
360
+ branch: typeof start.data.branch === "string" ? start.data.branch : "unknown",
361
+ status: runStatus,
362
+ elapsed: elapsedReading(events[0].ts, events.at(-1).ts),
363
+ tasks: {
364
+ done,
365
+ total: tasks.size,
366
+ samples: samples.tasks,
367
+ },
368
+ gates: {
369
+ passed,
370
+ total: gateEvents.length,
371
+ samples: samples.gates,
372
+ },
373
+ passRate: {
374
+ value: passRate,
375
+ samples: samples.passRate,
376
+ },
377
+ progress,
378
+ progressCaption: spotlight
379
+ ? `${spotlight.id} · ${spotlight.phase} · attempt ${taskAttempt(spotlight)} · ${taskActor(spotlight)}`
380
+ : "- · pending · attempt 1 · unassigned",
381
+ journalRows: journalRows(events, tasks, defects, runStatus),
382
+ statusItems: [
383
+ { state: tipPassed ? "pass" : "fail", text: `tip-verify ${tipPassed ? "passed" : "FAILED"}` },
384
+ { state: "pass", text: `done ${done}` },
385
+ { state: failed > 0 ? "fail" : "neutral", text: `failed ${failed}` },
386
+ { state: human > 0 ? "warn" : "neutral", text: `human ${human}` },
387
+ { state: escalated > 0 ? "warn" : "neutral", text: `escalated ${escalated}` },
388
+ ],
389
+ };
390
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * THE key contract for the run cockpit. This registry is the single source of
3
+ * truth for both halves of a key: what the keybar advertises and what the key
4
+ * does. The keybar's entries are derived from these bindings (`keybarEntries`),
5
+ * so a key with no registered handler cannot be drawn — an advertised key that
6
+ * does nothing is not expressible.
7
+ */
8
+ /** The subset of an ink key event the run bindings discriminate on. */
9
+ export type RunKeyEvent = {
10
+ readonly input: string;
11
+ readonly key: {
12
+ readonly upArrow?: boolean;
13
+ readonly downArrow?: boolean;
14
+ readonly leftArrow?: boolean;
15
+ readonly return?: boolean;
16
+ readonly tab?: boolean;
17
+ readonly escape?: boolean;
18
+ readonly backspace?: boolean;
19
+ readonly delete?: boolean;
20
+ readonly ctrl?: boolean;
21
+ };
22
+ };
23
+ /**
24
+ * The observable interaction state of the run surface. RunCockpitFrame renders
25
+ * every field of it — selection pointer, panel focus, opened event, follow
26
+ * indicator, filter prompt, help overlay — so the initial state below renders
27
+ * byte-identical to a frame that carries no interaction state at all, and every
28
+ * transition away from it is a change an operator can see.
29
+ */
30
+ export type RunInteractionState = {
31
+ /** The journal row the pointer marks — moved by ↑↓, -1 while nothing is selected. */
32
+ readonly selection: number;
33
+ /** The focused panel, index into RUN_PANEL_FOCUS_ORDER — cycled by Tab. */
34
+ readonly panel: number;
35
+ /** The selection opened by ⏎, or null once ← returns from it. */
36
+ readonly opened: number | null;
37
+ /** Follow mode — toggled by f; drawn as a status-strip indicator while on. */
38
+ readonly follow: boolean;
39
+ /** The / prompt is open and taking filter text. */
40
+ readonly filterPrompt: boolean;
41
+ /** The text the journal is narrowed to — empty shows every row. */
42
+ readonly filterQuery: string;
43
+ /** Help overlay — toggled by ?. */
44
+ readonly help: boolean;
45
+ /** Quit requested by q. */
46
+ readonly quit: boolean;
47
+ };
48
+ export declare function initialRunInteractionState(): RunInteractionState;
49
+ /**
50
+ * The panels Tab cycles focus through, in cycle order; index 0 is the focus
51
+ * the surface opens with, so the initial frame is the one the anchors pin.
52
+ */
53
+ export declare const RUN_PANEL_FOCUS_ORDER: readonly ["RUN", "VIEWS", "KEYS"];
54
+ export type RunKeyBinding = {
55
+ /** The glyph the keybar draws, e.g. "↑↓". */
56
+ readonly key: string;
57
+ /** The promise the keybar draws next to the glyph, e.g. "Move". */
58
+ readonly label: string;
59
+ /** Whether a key event activates this binding. */
60
+ readonly matches: (event: RunKeyEvent) => boolean;
61
+ /** The state transition the label promises. */
62
+ readonly apply: (state: RunInteractionState, event: RunKeyEvent) => RunInteractionState;
63
+ /**
64
+ * How a binding that carries on/off state reports it — the brand's reserved
65
+ * toggle glyphs, never bracket toggles. Present exactly on the bindings
66
+ * whose state is on/off.
67
+ */
68
+ readonly report?: (state: RunInteractionState) => string;
69
+ };
70
+ /** A toggle's on/off report: the brand's reserved glyphs, never brackets. */
71
+ export declare function toggleReport(label: string, on: boolean): string;
72
+ /**
73
+ * Every key the run surface advertises, in keybar order. Each entry carries
74
+ * its handler, so advertisement and behaviour cannot drift apart.
75
+ */
76
+ export declare const RUN_KEY_BINDINGS: readonly RunKeyBinding[];
77
+ /** The panels the setup surface cycles through, in their painted order. */
78
+ export declare const SETUP_PANEL_FOCUS_ORDER: readonly ["SETUP", "HARNESSES", "KEYS"];
79
+ /**
80
+ * The observable state owned by setup's bindings. Its initial value is the
81
+ * already-pinned setup frame: step two, the harness body focused, nothing
82
+ * selected, no modal action open. Every handler moves away from that state in
83
+ * a way SetupCockpitFrame paints.
84
+ */
85
+ export type SetupInteractionState = {
86
+ readonly selection: number;
87
+ readonly panel: number;
88
+ readonly step: number;
89
+ readonly selected: readonly number[];
90
+ readonly all: boolean;
91
+ readonly probeRevision: number;
92
+ readonly savePrompt: boolean;
93
+ readonly saved: boolean;
94
+ readonly help: boolean;
95
+ readonly quit: boolean;
96
+ };
97
+ export declare function initialSetupInteractionState(): SetupInteractionState;
98
+ export type SetupKeyBinding = {
99
+ readonly key: string;
100
+ readonly label: string;
101
+ readonly matches: (event: RunKeyEvent) => boolean;
102
+ readonly apply: (state: SetupInteractionState, event: RunKeyEvent) => SetupInteractionState;
103
+ readonly report?: (state: SetupInteractionState) => string;
104
+ };
105
+ /**
106
+ * Every setup advertisement and handler, in the exact order the surface has
107
+ * always drawn. The global six are registered here with setup semantics; the
108
+ * six setup-only bindings follow them on the same surface registry.
109
+ */
110
+ export declare const SETUP_KEY_BINDINGS: readonly SetupKeyBinding[];
111
+ /** One surface-aware registry serves both cockpit renderers and dispatchers. */
112
+ export declare const SURFACE_KEY_BINDINGS: {
113
+ readonly run: readonly RunKeyBinding[];
114
+ readonly setup: readonly SetupKeyBinding[];
115
+ };
116
+ /** Save confirmation owns input until it is accepted or dismissed. */
117
+ export declare function applySetupPromptInput(event: RunKeyEvent, state: SetupInteractionState): SetupInteractionState | undefined;
118
+ /**
119
+ * The transitions of the open / prompt. The prompt is not an advertised key,
120
+ * so it has no binding — but its transitions live here beside the registry
121
+ * because they rewrite the same interaction state: characters grow the query,
122
+ * backspace shrinks it, ⏎ applies it (the journal stays narrowed), escape
123
+ * cancels it. Anything else the prompt swallows unchanged.
124
+ */
125
+ export declare function applyFilterInput(event: RunKeyEvent, state: RunInteractionState): RunInteractionState | undefined;
126
+ /** What an advertisement looks like once the handler is stripped away. */
127
+ export type KeybarEntry = {
128
+ readonly key: string;
129
+ readonly label: string;
130
+ };
131
+ /**
132
+ * The keybar's entries, derived from a binding registry. This is the only way
133
+ * a keybar list is produced: remove a handler and its entry disappears.
134
+ */
135
+ export declare function keybarEntries(bindings: readonly {
136
+ readonly key: string;
137
+ readonly label: string;
138
+ }[]): readonly KeybarEntry[];
139
+ /** The binding a key event activates, or undefined when no handler owns it. */
140
+ export declare function resolveRunKeyBinding(bindings: readonly RunKeyBinding[], event: RunKeyEvent): RunKeyBinding | undefined;
141
+ /**
142
+ * Send a key event to the registry: the matching binding's handler produces
143
+ * the next interaction state. An event no handler owns changes nothing.
144
+ */
145
+ export declare function dispatchRunKey(event: RunKeyEvent, state: RunInteractionState, bindings?: readonly RunKeyBinding[]): RunInteractionState | undefined;
146
+ /** The setup binding a key event activates, or undefined when none owns it. */
147
+ export declare function resolveSetupKeyBinding(bindings: readonly SetupKeyBinding[], event: RunKeyEvent): SetupKeyBinding | undefined;
148
+ /** Send a key event through the setup half of the shared surface registry. */
149
+ export declare function dispatchSetupKey(event: RunKeyEvent, state: SetupInteractionState, bindings?: readonly SetupKeyBinding[]): SetupInteractionState | undefined;