@yaag/tui 0.1.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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@yaag/tui",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "src",
9
+ "!src/**/*.test.ts",
10
+ "!src/fixtures"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.ts"
15
+ },
16
+ "scripts": {
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "bun test src"
19
+ },
20
+ "dependencies": {
21
+ "@earendil-works/pi-tui": "^0.84.0",
22
+ "@yaag/runtime": "0.0.0"
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Formats a cost the way the Run tree mockup writes it: `$0.43`, with
3
+ * `+incomplete` appended when the cost is a floor (ADR-0012). A null cost is
4
+ * reported as unknown rather than as zero.
5
+ */
6
+ export function costText(value: number | null, incomplete = false): string {
7
+ if (value === null) return "cost unknown";
8
+ return `$${value.toFixed(2)}${incomplete ? " +incomplete" : ""}`;
9
+ }
10
+
11
+ /**
12
+ * Formats a token total the way the mockup writes it: `84K tok` above one
13
+ * thousand, the plain count below it. A null total is reported as unknown.
14
+ */
15
+ export function tokensText(total: number | null): string {
16
+ if (total === null) return "tokens unknown";
17
+ if (total < 1_000) return `${total} tok`;
18
+ return `${Math.round(total / 1_000)}K tok`;
19
+ }
@@ -0,0 +1,27 @@
1
+ import type { AgentActivity } from "@yaag/runtime";
2
+ import { sanitizeTerminalLine } from "./terminal-text.ts";
3
+
4
+ /**
5
+ * Formats the current Agent Activity as the one-line gist the tree draws.
6
+ *
7
+ * Every activity state of the Run Summary has a form: `thinking`, `writing`,
8
+ * `<tool>: <args>`, `compacting`, and `retrying 2/3`. The result is sanitized
9
+ * to one line, so a hostile tool name or argument gist cannot forge a row.
10
+ */
11
+ export function activityText(activity: AgentActivity): string {
12
+ switch (activity.type) {
13
+ case "thinking":
14
+ return "thinking";
15
+ case "writing":
16
+ return "writing";
17
+ case "compacting":
18
+ return "compacting";
19
+ case "retrying":
20
+ return `retrying ${activity.attempt}/${activity.max}`;
21
+ default: {
22
+ const args = sanitizeTerminalLine(activity.argsGist);
23
+ const name = sanitizeTerminalLine(activity.name);
24
+ return args === "" ? name : `${name}: ${args}`;
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,85 @@
1
+ import type { LifecycleEvent } from "@yaag/runtime";
2
+ import { sanitizeTerminalLine } from "./terminal-text.ts";
3
+
4
+ /** Maximum Ask rows one Agent's renderer-only ledger retains (mirrors the node table cap). */
5
+ export const AGENT_ASK_LEDGER_MAX = 64;
6
+
7
+ type AskStartEvent = Extract<LifecycleEvent, { readonly type: "ask_start" }>;
8
+ type AskEndEvent = Extract<LifecycleEvent, { readonly type: "ask_end" }>;
9
+
10
+ /** One Ask as the tree draws it: identity, gist, timing, and settlement. */
11
+ export interface AskRow {
12
+ readonly index: number;
13
+ readonly promptGist: string;
14
+ readonly startedAt: number | null;
15
+ readonly endedAt: number | null;
16
+ readonly ok: boolean | null;
17
+ readonly replayed: boolean;
18
+ }
19
+
20
+ /** One Agent's bounded Ask history, oldest settled Asks rolled into a counter. */
21
+ export interface AskLedger {
22
+ readonly rows: readonly AskRow[];
23
+ readonly settledPruned: number;
24
+ }
25
+
26
+ /** The ledger of an Agent that has started no Ask. */
27
+ export function emptyLedger(): AskLedger {
28
+ return { rows: [], settledPruned: 0 };
29
+ }
30
+
31
+ /**
32
+ * Records one Ask start. A repeated start for an index already present keeps
33
+ * the first-seen row, so a redraw cannot duplicate an Ask row.
34
+ */
35
+ export function applyAskStart(ledger: AskLedger, event: AskStartEvent): AskLedger {
36
+ if (ledger.rows.some((row) => row.index === event.index)) return ledger;
37
+ const row: AskRow = {
38
+ index: event.index,
39
+ promptGist: sanitizeTerminalLine(event.promptGist),
40
+ startedAt: event.at,
41
+ endedAt: null,
42
+ ok: null,
43
+ replayed: event.replayed === true,
44
+ };
45
+ return prune({ rows: [...ledger.rows, row], settledPruned: ledger.settledPruned });
46
+ }
47
+
48
+ /**
49
+ * Records one Ask settlement. A settlement for an index the ledger never saw
50
+ * start still records a row, so a late observer keeps the Ask count honest.
51
+ */
52
+ export function applyAskEnd(ledger: AskLedger, event: AskEndEvent): AskLedger {
53
+ const index = ledger.rows.findIndex((row) => row.index === event.index);
54
+ if (index === -1) {
55
+ const row: AskRow = {
56
+ index: event.index,
57
+ promptGist: "",
58
+ startedAt: null,
59
+ endedAt: event.at,
60
+ ok: event.ok,
61
+ replayed: false,
62
+ };
63
+ return prune({ rows: [...ledger.rows, row], settledPruned: ledger.settledPruned });
64
+ }
65
+ const rows = ledger.rows.map((row, position) =>
66
+ position === index ? { ...row, endedAt: event.at, ok: event.ok } : row,
67
+ );
68
+ return prune({ rows, settledPruned: ledger.settledPruned });
69
+ }
70
+
71
+ function prune(ledger: AskLedger): AskLedger {
72
+ if (ledger.rows.length <= AGENT_ASK_LEDGER_MAX) return ledger;
73
+ const excess = ledger.rows.length - AGENT_ASK_LEDGER_MAX;
74
+ const kept: AskRow[] = [];
75
+ let pruned = 0;
76
+ for (const row of ledger.rows) {
77
+ if (row.endedAt !== null && pruned < excess) {
78
+ pruned += 1;
79
+ continue;
80
+ }
81
+ kept.push(row);
82
+ }
83
+ if (pruned === 0) return ledger;
84
+ return { rows: kept, settledPruned: ledger.settledPruned + pruned };
85
+ }
@@ -0,0 +1,75 @@
1
+ import { costText } from "./accounting-text.ts";
2
+ import { durationText } from "./duration-text.ts";
3
+ import { sanitizeTerminalLine } from "./terminal-text.ts";
4
+ import { GLYPHS, stateGlyph } from "./tree-glyphs.ts";
5
+ import { buildTree } from "./tree-model.ts";
6
+ import type { TreeNode } from "./tree-node.ts";
7
+ import { clamp } from "./tree-rows.ts";
8
+ import type { TreeState } from "./tree-state.ts";
9
+
10
+ /** Everything the compact background view needs; `now` keeps it clock-free. */
11
+ export interface CompactRenderOptions {
12
+ readonly now: number;
13
+ readonly width: number;
14
+ /** Header text before the Program name; defaults to `yaag`. */
15
+ readonly label?: string;
16
+ }
17
+
18
+ /**
19
+ * Renders the same state folded to Agent level: one Run header line, then one
20
+ * line per Agent with a one-line nested-work gist for each live Agent.
21
+ *
22
+ * Pure over the state, and it never draws an Ask row: the full view owns that
23
+ * depth. Every untrusted string passes `sanitizeTerminalLine` first.
24
+ */
25
+ export function renderCompact(state: TreeState, options: CompactRenderOptions): readonly string[] {
26
+ return [compactHeaderLine(state, options), ...compactAgentLines(state, options)];
27
+ }
28
+
29
+ /** The one Run header line the compact view and the inline widget share. */
30
+ export function compactHeaderLine(state: TreeState, options: CompactRenderOptions): string {
31
+ const summary = state.summary;
32
+ const elapsed =
33
+ summary.runState === "ended"
34
+ ? summary.durationMs
35
+ : options.now - (summary.startedAt ?? options.now);
36
+ const status = summary.runState === "ended" ? summary.outcome : "running";
37
+ const asks = `${summary.asksSettled} ask${summary.asksSettled === 1 ? "" : "s"}`;
38
+ const label = sanitizeTerminalLine(options.label ?? "yaag");
39
+ const program = sanitizeTerminalLine(summary.program === "" ? "Run" : summary.program);
40
+ return clamp(
41
+ `${label} ▸ ${program} ${status} ${durationText(elapsed)} ${costText(summary.cost, summary.incomplete)} · ${asks}`,
42
+ options.width,
43
+ );
44
+ }
45
+
46
+ /** One line per Agent, with a one-line nested-work gist for each live Agent. */
47
+ export function compactAgentLines(
48
+ state: TreeState,
49
+ options: CompactRenderOptions,
50
+ ): readonly string[] {
51
+ return buildTree(state, { now: options.now }).map((agent) =>
52
+ compactAgentLine(agent, options.width),
53
+ );
54
+ }
55
+
56
+ /** The one Agent line of the compact view, drawn for one Agent node. */
57
+ export function compactAgentLine(agent: TreeNode, width: number): string {
58
+ return clamp(agentLine(agent), width);
59
+ }
60
+
61
+ function agentLine(agent: TreeNode): string {
62
+ const head = ` ${stateGlyph(agent.state)} ${agent.label} ${agent.facts[0] ?? ""}`;
63
+ const running = countRunningNested(agent.children);
64
+ if (agent.state !== "running" || running === 0) return head;
65
+ return `${head} ${GLYPHS.collapsed} ${running} subagent${running === 1 ? "" : "s"} running`;
66
+ }
67
+
68
+ function countRunningNested(nodes: readonly TreeNode[]): number {
69
+ let count = 0;
70
+ for (const node of nodes) {
71
+ if (node.kind === "nested" && node.state === "running") count += 1;
72
+ count += countRunningNested(node.children);
73
+ }
74
+ return count;
75
+ }
@@ -0,0 +1,33 @@
1
+ import { sanitizeTerminalLine } from "./terminal-text.ts";
2
+ import { stateGlyph } from "./tree-glyphs.ts";
3
+ import type { TreeNode } from "./tree-node.ts";
4
+ import { clamp } from "./tree-rows.ts";
5
+
6
+ /** The bounded last-output lines the pane draws under the selected node. */
7
+ export interface DetailsPaneOptions {
8
+ readonly width: number;
9
+ readonly outputTail: readonly string[];
10
+ }
11
+
12
+ /**
13
+ * Draws the details region for the selected node: its path as the header, its
14
+ * activity gist and facts, and the bounded output tail of the Agent that owns
15
+ * it. Returns no lines when nothing is selected.
16
+ */
17
+ export function renderDetailsPane(
18
+ node: TreeNode | undefined,
19
+ options: DetailsPaneOptions,
20
+ ): readonly string[] {
21
+ if (node === undefined) return [];
22
+ const lines = [
23
+ clamp(`${stateGlyph(node.state)} ${sanitizeTerminalLine(node.path)}`, options.width),
24
+ ];
25
+ if (node.activityGist !== null)
26
+ lines.push(clamp(` ${sanitizeTerminalLine(node.activityGist)}`, options.width));
27
+ if (node.facts.length > 0)
28
+ lines.push(clamp(` ${node.facts.map(sanitizeTerminalLine).join(" · ")}`, options.width));
29
+ const last = options.outputTail.at(-1);
30
+ if (last !== undefined && last !== "")
31
+ lines.push(clamp(` last: “${sanitizeTerminalLine(last)}”`, options.width));
32
+ return lines;
33
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The drill-in controller: it layers the actions menu and the read-only
3
+ * transcript overlay over the Run tree (spec §1).
4
+ *
5
+ * Read-only by construction: `DrillHost` exposes copy, editor, notify, and
6
+ * session reads and nothing else, so no code path here can prompt an Agent or
7
+ * stop a Run (ADR — a Peek observes, it never sends). It holds no clock either;
8
+ * a host that tails a live session file calls `refreshTranscript` on its own
9
+ * cadence.
10
+ */
11
+ import type { AgentInfo } from "@yaag/runtime";
12
+ import { routeDrillKey, routeMenuKey, routeOverlayKey } from "./drill-keys.ts";
13
+ import {
14
+ type AnyDrillAction,
15
+ applyDrillAction,
16
+ type DrillEffect,
17
+ type DrillState,
18
+ emptyDrill,
19
+ } from "./drill-state.ts";
20
+ import type { NamedKeybindings } from "./key-router.ts";
21
+ import { renderActionsMenu } from "./node-actions.ts";
22
+ import { agentOfPath } from "./node-path-parse.ts";
23
+ import { extractSystemPrompt } from "./session-transcript.ts";
24
+ import { nodeTranscriptLines } from "./transcript-content.ts";
25
+ import { renderTranscriptOverlay } from "./transcript-overlay.ts";
26
+ import { NO_TRANSCRIPT_STUB } from "./transcript-render.ts";
27
+ import type { TreeNavigator } from "./tree-navigator.ts";
28
+
29
+ /** Shown when a session file records no system prompt (spec §5). */
30
+ const NO_SYSTEM_PROMPT = "(no system prompt recorded in this session file)";
31
+
32
+ /** Every capability the drill-in needs from its host — all of them read-only. */
33
+ export interface DrillHost {
34
+ /** The tree's keyboard router; the drill layers sit above it. */
35
+ readonly navigator: TreeNavigator;
36
+ /** The keybinding matcher the drill key tables route through. */
37
+ readonly keybindings: NamedKeybindings;
38
+ /** The owning Agent's observer projection, absent for an unknown Agent. */
39
+ agentInfo(agent: string): AgentInfo | undefined;
40
+ /** Reads an Agent's session file body, or null when it is missing. */
41
+ readSession(agent: string): Promise<string | null>;
42
+ /** An Agent's session file path, or null when the spawn reported none. */
43
+ sessionPath(agent: string): string | null;
44
+ copyPath(text: string): Promise<void>;
45
+ openEditor(title: string, body: string): Promise<void>;
46
+ notify(message: string, level: "info" | "warning" | "error"): void;
47
+ requestRender(): void;
48
+ /** Content rows the overlay may draw. */
49
+ rows(): number;
50
+ /** Releases input focus when `esc` closes the last layer. */
51
+ dropFocus?(): void;
52
+ }
53
+
54
+ /**
55
+ * Routes input through the overlay, then the menu, then the tree, and performs
56
+ * each layer's read-only effects through the host.
57
+ *
58
+ * `handleInput` reports whether a layer consumed the byte; an unbound key is
59
+ * consumed by no layer. The tree's cursor and folds are never written, so
60
+ * closing every layer restores the tree exactly as the reader left it.
61
+ */
62
+ export class DrillController {
63
+ readonly #host: DrillHost;
64
+ #state: DrillState = emptyDrill();
65
+ /** Content of the currently open node, keyed by its path, or undefined. */
66
+ #content: { readonly path: string; readonly lines: readonly string[] } | undefined;
67
+ /** Bumped by every open, refresh, and close, to void in-flight reads. */
68
+ #generation = 0;
69
+
70
+ constructor(host: DrillHost) {
71
+ this.#host = host;
72
+ }
73
+
74
+ /** Which layer holds input focus. */
75
+ get layer(): DrillState {
76
+ return this.#state;
77
+ }
78
+
79
+ /** The framed overlay lines, or undefined while the tree holds focus. */
80
+ overlayLines(width: number): readonly string[] | undefined {
81
+ const state = this.#state;
82
+ switch (state.kind) {
83
+ case "tree":
84
+ return undefined;
85
+ case "menu":
86
+ return renderActionsMenu({ nodePath: state.path, cursor: state.cursor, width });
87
+ case "transcript":
88
+ return renderTranscriptOverlay({
89
+ nodePath: state.path,
90
+ content: this.#linesFor(state.path),
91
+ scroll: state.scroll,
92
+ rows: this.#host.rows(),
93
+ width,
94
+ });
95
+ }
96
+ }
97
+
98
+ /** Routes one input byte string; returns whether a layer consumed it. */
99
+ handleInput(data: string): boolean {
100
+ const action = this.#route(data);
101
+ if (action === undefined) return this.#state.kind === "tree" && this.#tree(data);
102
+ const before = this.#state;
103
+ const { state, effect } = applyDrillAction(before, action, {
104
+ selectedPath: this.#host.navigator.selectedPath,
105
+ total: before.kind === "transcript" ? this.#linesFor(before.path).length : 0,
106
+ rows: this.#host.rows(),
107
+ });
108
+ this.#state = state;
109
+ if (state.kind !== "transcript" || state.path !== openPath(before)) this.#invalidate();
110
+ if (effect !== undefined) void this.#perform(effect);
111
+ if (state !== before || effect !== undefined) this.#host.requestRender();
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Re-reads the open node's transcript, for a host that tails a live session
117
+ * file. Does nothing while no overlay is open, and never throws.
118
+ */
119
+ async refreshTranscript(): Promise<void> {
120
+ const state = this.#state;
121
+ if (state.kind !== "transcript") return;
122
+ await this.#loadTranscript(state.path, this.#nextGeneration());
123
+ }
124
+
125
+ #route(data: string): AnyDrillAction | undefined {
126
+ const keybindings = this.#host.keybindings;
127
+ switch (this.#state.kind) {
128
+ case "transcript":
129
+ return routeOverlayKey(data, keybindings);
130
+ case "menu":
131
+ return routeMenuKey(data, keybindings);
132
+ case "tree":
133
+ return routeDrillKey(data, keybindings);
134
+ }
135
+ }
136
+
137
+ #tree(data: string): boolean {
138
+ return this.#host.navigator.handleInput(data);
139
+ }
140
+
141
+ async #perform(effect: DrillEffect): Promise<void> {
142
+ switch (effect.kind) {
143
+ case "loadTranscript":
144
+ await this.#loadTranscript(effect.path, this.#nextGeneration());
145
+ return;
146
+ case "copySessionPath":
147
+ await this.#copySessionPath(effect.path);
148
+ return;
149
+ case "openSystemPrompt":
150
+ await this.#openSystemPrompt(effect.path);
151
+ return;
152
+ case "dropFocus":
153
+ this.#host.dropFocus?.();
154
+ return;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Reads one node's transcript and commits it only while that node is still
160
+ * the open overlay and no newer read has started, so a slow read for A can
161
+ * never land under B and a closed overlay drops its pending read.
162
+ */
163
+ async #loadTranscript(nodePath: string, generation: number): Promise<void> {
164
+ const agent = agentOfPath(nodePath);
165
+ const info = this.#host.agentInfo(agent);
166
+ const session = await this.#readSession(agent);
167
+ if (generation !== this.#generation) return;
168
+ const state = this.#state;
169
+ if (state.kind !== "transcript" || state.path !== nodePath) return;
170
+ this.#content = {
171
+ path: nodePath,
172
+ lines:
173
+ info === undefined
174
+ ? [NO_TRANSCRIPT_STUB]
175
+ : nodeTranscriptLines({ nodePath, agent: info, session }),
176
+ };
177
+ this.#host.requestRender();
178
+ }
179
+
180
+ /** Loaded lines for `nodePath`, or none while its own read is in flight. */
181
+ #linesFor(nodePath: string): readonly string[] {
182
+ const content = this.#content;
183
+ return content !== undefined && content.path === nodePath ? content.lines : [];
184
+ }
185
+
186
+ /** Drops retained content and voids every in-flight read. */
187
+ #invalidate(): void {
188
+ this.#content = undefined;
189
+ this.#generation += 1;
190
+ }
191
+
192
+ #nextGeneration(): number {
193
+ this.#generation += 1;
194
+ return this.#generation;
195
+ }
196
+
197
+ async #copySessionPath(nodePath: string): Promise<void> {
198
+ const path = this.#host.sessionPath(agentOfPath(nodePath));
199
+ if (path === null) {
200
+ this.#host.notify(`${nodePath} has no session file to copy.`, "warning");
201
+ return;
202
+ }
203
+ try {
204
+ await this.#host.copyPath(path);
205
+ this.#host.notify(`Copied session file path for ${nodePath}.`, "info");
206
+ } catch (error) {
207
+ this.#host.notify(`Could not copy path: ${message(error)}`, "error");
208
+ }
209
+ }
210
+
211
+ async #openSystemPrompt(nodePath: string): Promise<void> {
212
+ const session = await this.#readSession(agentOfPath(nodePath));
213
+ const prompt = session === null ? null : extractSystemPrompt(session);
214
+ await this.#host.openEditor(`System prompt — ${nodePath}`, prompt ?? NO_SYSTEM_PROMPT);
215
+ }
216
+
217
+ /** A session file that cannot be read degrades to the labelled stub. */
218
+ async #readSession(agent: string): Promise<string | null> {
219
+ try {
220
+ return await this.#host.readSession(agent);
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ }
226
+
227
+ /** The node path of an open transcript overlay, or undefined for other layers. */
228
+ function openPath(state: DrillState): string | undefined {
229
+ return state.kind === "transcript" ? state.path : undefined;
230
+ }
231
+
232
+ function message(error: unknown): string {
233
+ return error instanceof Error ? error.message : String(error);
234
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The three key tables of the drill-in (spec §2): the tree's drill gestures, the
3
+ * actions menu, and the transcript overlay.
4
+ *
5
+ * Every table routes through `routeKey`, so a user's named-binding override
6
+ * always beats a vim literal. `back`/`close` resolve `tui.select.cancel` before
7
+ * the bare `escape` literal, and both sit after the arrow entries, so an
8
+ * ESC-prefixed arrow sequence scrolls rather than closing the layer.
9
+ */
10
+ import { type KeyBinding, type NamedKeybindings, routeKey } from "./key-router.ts";
11
+
12
+ /** A drill gesture the tree layer consumes. */
13
+ export type DrillAction = "openMenu" | "openTranscript" | "back";
14
+
15
+ /** A gesture the actions menu consumes. */
16
+ export type MenuAction = "menuUp" | "menuDown" | "confirm" | "cancel";
17
+
18
+ /** A gesture the transcript overlay consumes. */
19
+ export type OverlayAction =
20
+ | "scrollUp"
21
+ | "scrollDown"
22
+ | "pageUp"
23
+ | "pageDown"
24
+ | "copyPath"
25
+ | "close";
26
+
27
+ /**
28
+ * The tree's drill keys. `openMenu` uses the literal `enter`, because the spec
29
+ * names no pi binding for it and the tree layer is consulted last.
30
+ */
31
+ export const DRILL_KEY_TABLE: readonly KeyBinding<DrillAction>[] = [
32
+ { action: "openMenu", keys: ["enter", "return"] },
33
+ { action: "openTranscript", keys: ["t"] },
34
+ { action: "back", named: "tui.select.cancel", keys: ["escape"] },
35
+ ];
36
+
37
+ /** The actions menu's keys. */
38
+ export const MENU_KEY_TABLE: readonly KeyBinding<MenuAction>[] = [
39
+ { action: "menuUp", named: "tui.select.up", vim: ["k"] },
40
+ { action: "menuDown", named: "tui.select.down", vim: ["j"] },
41
+ { action: "confirm", named: "tui.select.confirm", keys: ["enter", "return"] },
42
+ { action: "cancel", named: "tui.select.cancel", keys: ["escape"] },
43
+ ];
44
+
45
+ /** The transcript overlay's keys. */
46
+ export const OVERLAY_KEY_TABLE: readonly KeyBinding<OverlayAction>[] = [
47
+ { action: "scrollUp", named: "tui.select.up", vim: ["k"] },
48
+ { action: "scrollDown", named: "tui.select.down", vim: ["j"] },
49
+ { action: "pageUp", named: "tui.select.pageUp" },
50
+ { action: "pageDown", named: "tui.select.pageDown" },
51
+ { action: "copyPath", keys: ["c"] },
52
+ { action: "close", named: "tui.select.cancel", keys: ["escape"] },
53
+ ];
54
+
55
+ /** Resolves one input byte string to a tree drill action, or undefined. */
56
+ export function routeDrillKey(
57
+ data: string,
58
+ keybindings: NamedKeybindings,
59
+ ): DrillAction | undefined {
60
+ return routeKey(data, keybindings, DRILL_KEY_TABLE);
61
+ }
62
+
63
+ /** Resolves one input byte string to an actions-menu action, or undefined. */
64
+ export function routeMenuKey(data: string, keybindings: NamedKeybindings): MenuAction | undefined {
65
+ return routeKey(data, keybindings, MENU_KEY_TABLE);
66
+ }
67
+
68
+ /** Resolves one input byte string to a transcript-overlay action, or undefined. */
69
+ export function routeOverlayKey(
70
+ data: string,
71
+ keybindings: NamedKeybindings,
72
+ ): OverlayAction | undefined {
73
+ return routeKey(data, keybindings, OVERLAY_KEY_TABLE);
74
+ }