@mnmal-ai/surface-core 0.0.0 → 0.14.6

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/dist/index.js ADDED
@@ -0,0 +1,1103 @@
1
+ import { $ as wordRight, A as moveSelection, B as remember, C as unavailableReason, D as completeDraft, E as commandUsage, F as keySpecOf, G as EMPTY_DRAFT, H as walk, I as resolveKey, J as graphemeAt, K as atEnd, L as EMPTY_HISTORY, M as paletteQuery, N as paletteRows, O as enterRuns, P as parseCommandLine, Q as wordLeft, R as HISTORY_MAX, S as routeInput, T as clampSelection, U as CTRL_C_WINDOW_MS, V as settle, W as ctrlC, X as maskDraft, Y as insertChunk, Z as pasteToLine, _ as liveShrink, a as answerEntry, at as normaliseKeySpec, b as reflowSequence, c as emptyTranscript, d as sendTurn, et as applyInput, f as CLEAR_PANE, g as groundSequence, h as anchorPadding, i as addGap, it as composeModules, j as paletteMatches, k as menuWindow, l as heldAsks, m as RESET_GROUND, n as setPanel, nt as RESERVED_GLOBAL_KEYS, o as arriveTurn, ot as applySuggestion, p as DEFAULT_GROUND, q as editKey, r as defineModule, rt as assertComposes, s as commitEntry, st as mergeCompletions, t as panelOf, tt as CompositionError, u as markedFlights, v as prepareSequence, w as MENU_NAME_WIDTH, x as sanitise, y as prepareTerminal, z as mayWalk } from "./overlay-DQUbfIKu.js";
2
+ import { spinnerFrame } from "@mnmal-ai/surface-kit";
3
+ import { Box, Static, Text, measureElement, render, useApp, useInput, usePaste, useStdout } from "ink";
4
+ import { Component, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/app.tsx
8
+ /** Calls a module's view as a component, so a throw lands in the boundary and not in the shell. */
9
+ function Run({ render }) {
10
+ const out = render();
11
+ return typeof out === "string" || typeof out === "number" ? /* @__PURE__ */ jsx(Text, { children: sanitise(String(out)) }) : out;
12
+ }
13
+ var Boundary = class extends Component {
14
+ state = { error: null };
15
+ static getDerivedStateFromError(err) {
16
+ return { error: err instanceof Error ? err.message : String(err) };
17
+ }
18
+ render() {
19
+ if (this.state.error === null) return this.props.children;
20
+ const reason = sanitise(this.state.error).split("\n")[0];
21
+ return /* @__PURE__ */ jsxs(Text, {
22
+ color: this.props.theme.color.error,
23
+ children: [
24
+ this.props.theme.glyph.fail,
25
+ " ",
26
+ sanitise(this.props.module),
27
+ " view failed: ",
28
+ reason
29
+ ]
30
+ });
31
+ }
32
+ };
33
+ function Safe(props) {
34
+ return /* @__PURE__ */ jsx(Boundary, {
35
+ module: props.module,
36
+ theme: props.theme,
37
+ children: /* @__PURE__ */ jsx(Run, { render: props.render })
38
+ });
39
+ }
40
+ const ERROR_KIND = "core.error";
41
+ const CANCELLED_KIND = "core.cancelled";
42
+ const TOAST_MS = 3e3;
43
+ /** A window drag resizes per step; the pane is re-laid once, when it settles. */
44
+ const REFLOW_SETTLE_MS = 150;
45
+ /** Columns the rail takes when it shows: its width and the margin before it. */
46
+ const RAIL_COLUMNS = 33;
47
+ /** Rows the shell keeps for itself: the input, the toast and the status row, and a little
48
+ * conversation above the panel. */
49
+ const SHELL_ROWS = 6;
50
+ /** What the band adds: its top and bottom padding, the presence row with a blank row above it,
51
+ * and the blank row between the band and the status row. */
52
+ const BAND_ROWS = 5;
53
+ /** Insets on raised menus (the / palette, completions): two columns each side and a row top and
54
+ * bottom (operator, 2026-10-09). The rows come out of the menu's row budget, so it never grows. */
55
+ const OVERLAY_PAD_X = 2;
56
+ const OVERLAY_PAD_Y = 1;
57
+ /** A panel's insets. Wider than a menu's and with a row top and bottom, because a panel is read,
58
+ * not scanned; both come out of the size the panel view is handed (operator, 2026-10-09). */
59
+ const PANEL_PAD_X = 4;
60
+ const PANEL_PAD_Y = 1;
61
+ /**
62
+ * The `/` palette and the completion menu, directly above the input on the band's ground: a fixed name column, the detail dimmed, the selection on the `selection` tone, a window
63
+ * that follows the selection, and a hint line.
64
+ */
65
+ function MenuList({ theme, rows, selected, height, empty, hint }) {
66
+ const { start, end, position } = menuWindow(rows.length, selected, height);
67
+ return /* @__PURE__ */ jsxs(Box, {
68
+ flexDirection: "column",
69
+ paddingX: OVERLAY_PAD_X,
70
+ paddingY: OVERLAY_PAD_Y,
71
+ backgroundColor: theme.color.raised,
72
+ children: [rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
73
+ color: theme.color.dim,
74
+ wrap: "truncate",
75
+ children: sanitise(empty)
76
+ }) : rows.slice(start, end).map((r, i) => {
77
+ const on = start + i === selected;
78
+ return /* @__PURE__ */ jsxs(Box, {
79
+ ...on ? { backgroundColor: theme.color.selection } : {},
80
+ children: [/* @__PURE__ */ jsx(Box, {
81
+ width: 22,
82
+ flexShrink: 0,
83
+ children: /* @__PURE__ */ jsx(Text, {
84
+ bold: on,
85
+ wrap: "truncate",
86
+ ...on ? { color: theme.color.surface } : {},
87
+ children: sanitise(r.name)
88
+ })
89
+ }), /* @__PURE__ */ jsx(Text, {
90
+ color: on ? theme.color.surface : theme.color.dim,
91
+ wrap: "truncate",
92
+ children: sanitise(r.detail)
93
+ })]
94
+ }, r.key);
95
+ }), /* @__PURE__ */ jsx(Text, {
96
+ color: theme.color.dim,
97
+ wrap: "truncate",
98
+ children: `↑↓ select${position} · ${hint}`
99
+ })]
100
+ });
101
+ }
102
+ /** A panel's rows by default: the terminal less the shell's own, and never under three. */
103
+ function defaultPanelRows(terminalRows, opts) {
104
+ return Math.max(3, terminalRows - SHELL_ROWS - (opts?.band === true ? BAND_ROWS : 0));
105
+ }
106
+ /** What `run()` prepares the terminal with, or `null` when the host has prepared it already. */
107
+ function runTerminalOptions(opts) {
108
+ if (opts.terminal === false) return null;
109
+ return {
110
+ anchor: opts.anchored === true,
111
+ ...opts.terminal
112
+ };
113
+ }
114
+ function createApp(opts) {
115
+ const registry = composeModules(opts.modules);
116
+ const theme = opts.theme;
117
+ const railMin = opts.railMinColumns ?? 80;
118
+ const askView = opts.entries?.ask;
119
+ const answerView = opts.entries?.answer;
120
+ const waitingView = opts.entries?.waiting;
121
+ const noteView = opts.entries?.note;
122
+ const exitHint = sanitise(opts.exitHint ?? "press ctrl+c again to exit");
123
+ const g = opts.gutter ?? 0;
124
+ const gutter = Number.isInteger(g) && g >= 0 && g <= 8 ? g : 0;
125
+ const band = opts.band === true;
126
+ const presence = registry.presence?.spec;
127
+ const presenceModule = registry.presence?.module ?? "";
128
+ const panelRows = opts.panelRows ?? ((r) => defaultPanelRows(r, { band }));
129
+ const anchored = opts.anchored === true;
130
+ function App({ columns, rows }) {
131
+ const { exit } = useApp();
132
+ const { stdout } = useStdout();
133
+ const [term, setTerm] = useState(() => ({
134
+ columns: stdout.columns ?? 120,
135
+ rows: stdout.rows ?? 30
136
+ }));
137
+ const [epoch, setEpoch] = useState(0);
138
+ const relayout = useCallback(() => {
139
+ if (stdout.isTTY) stdout.write(reflowSequence(stdout.rows));
140
+ setEpoch((e) => e + 1);
141
+ }, [stdout]);
142
+ useEffect(() => {
143
+ const laidOut = {
144
+ columns: stdout.columns ?? 120,
145
+ rows: stdout.rows ?? 30
146
+ };
147
+ let timer;
148
+ const onResize = () => {
149
+ setTerm({
150
+ columns: stdout.columns ?? 120,
151
+ rows: stdout.rows ?? 30
152
+ });
153
+ if (!anchored) return;
154
+ clearTimeout(timer);
155
+ timer = setTimeout(() => {
156
+ const now = {
157
+ columns: stdout.columns ?? 120,
158
+ rows: stdout.rows ?? 30
159
+ };
160
+ const grew = now.rows > laidOut.rows;
161
+ laidOut.rows = now.rows;
162
+ if (now.columns === laidOut.columns && !grew) return;
163
+ laidOut.columns = now.columns;
164
+ relayout();
165
+ }, REFLOW_SETTLE_MS);
166
+ };
167
+ stdout.on("resize", onResize);
168
+ return () => {
169
+ clearTimeout(timer);
170
+ stdout.off("resize", onResize);
171
+ };
172
+ }, [stdout, relayout]);
173
+ const width = Math.max(1, (columns ?? term.columns) - 2 * gutter);
174
+ const height = rows ?? term.rows;
175
+ const showRail = registry.rail.length > 0 && width >= railMin;
176
+ const panelOuter = {
177
+ columns: Math.max(1, width - (showRail ? RAIL_COLUMNS : 0)),
178
+ rows: Math.max(1, panelRows(height))
179
+ };
180
+ const panelSize = {
181
+ columns: Math.max(1, panelOuter.columns - 8),
182
+ rows: Math.max(1, panelOuter.rows - 2)
183
+ };
184
+ const sizeRef = useRef(panelSize);
185
+ sizeRef.current = panelSize;
186
+ const [transcript, setTranscript] = useState(() => emptyTranscript());
187
+ const transcriptRef = useRef(transcript);
188
+ const putTranscript = useCallback((fn) => {
189
+ transcriptRef.current = fn(transcriptRef.current);
190
+ setTranscript(transcriptRef.current);
191
+ }, []);
192
+ const [live, setLive] = useState(/* @__PURE__ */ new Map());
193
+ const [draft, setDraft] = useState(EMPTY_DRAFT);
194
+ const [, setHistory] = useState(EMPTY_HISTORY);
195
+ const [overlay, setOverlay] = useState(null);
196
+ const [menu, setMenu] = useState(null);
197
+ const [status, setStatus] = useState(/* @__PURE__ */ new Map());
198
+ const [toast, setToast] = useState(null);
199
+ const draftRef = useRef(EMPTY_DRAFT);
200
+ const requests = useRef(0);
201
+ const nextId = useRef(1);
202
+ const notes = useRef(0);
203
+ const armedAt = useRef(null);
204
+ const flights = useRef(/* @__PURE__ */ new Map());
205
+ const disposed = useRef(false);
206
+ const toastTimer = useRef(null);
207
+ const [notice, setNotice] = useState(null);
208
+ const noticeTimer = useRef(null);
209
+ const menuRef = useRef(null);
210
+ const overlayRef = useRef(null);
211
+ const historyRef = useRef(EMPTY_HISTORY);
212
+ const putMenu = useCallback((m) => {
213
+ menuRef.current = m;
214
+ setMenu(m);
215
+ }, []);
216
+ const [armed, setArmed] = useState(null);
217
+ const armedRef = useRef(null);
218
+ const putArmed = useCallback((a) => {
219
+ if (armedRef.current === a) return;
220
+ armedRef.current = a;
221
+ setArmed(a);
222
+ }, []);
223
+ const defOf = useCallback((o) => o ? registry.panels.get(`${o.module}/${o.panel}`)?.def : void 0, []);
224
+ const putHistory = useCallback((h) => {
225
+ historyRef.current = h;
226
+ setHistory(h);
227
+ }, []);
228
+ const putOverlay = useCallback((o) => {
229
+ const was = overlayRef.current;
230
+ if ((o === null || was === null || o.module !== was.module || o.panel !== was.panel) && (defOf(was)?.mask === true || defOf(o)?.mask === true)) {
231
+ draftRef.current = EMPTY_DRAFT;
232
+ requests.current += 1;
233
+ setDraft(EMPTY_DRAFT);
234
+ if (defOf(o)?.mask === true) {
235
+ if (menuRef.current !== null) putMenu(null);
236
+ putHistory(settle(historyRef.current));
237
+ putArmed(null);
238
+ }
239
+ }
240
+ overlayRef.current = o;
241
+ setOverlay(o);
242
+ const a = armedRef.current;
243
+ if (a !== null && (o === null || o.module !== a.module || o.panel !== a.panel || o.state !== a.state)) putArmed(null);
244
+ }, [
245
+ putArmed,
246
+ defOf,
247
+ putMenu,
248
+ putHistory
249
+ ]);
250
+ const showToast = useCallback((text, tone, ms) => {
251
+ if (disposed.current) return;
252
+ if (toastTimer.current !== null) clearTimeout(toastTimer.current);
253
+ setToast({
254
+ text: sanitise(text),
255
+ tone
256
+ });
257
+ toastTimer.current = setTimeout(() => {
258
+ toastTimer.current = null;
259
+ setToast(null);
260
+ }, ms);
261
+ }, []);
262
+ const showNotice = useCallback((text, ms) => {
263
+ if (disposed.current) return;
264
+ if (noticeTimer.current !== null) clearTimeout(noticeTimer.current);
265
+ setNotice(sanitise(text));
266
+ noticeTimer.current = setTimeout(() => {
267
+ noticeTimer.current = null;
268
+ setNotice(null);
269
+ }, ms);
270
+ }, []);
271
+ const bus = useRef(/* @__PURE__ */ new Map());
272
+ const appAbort = useRef(new AbortController());
273
+ const handleLineRef = useRef(() => {});
274
+ const setDraftRef = useRef(() => {});
275
+ const print = useCallback((text, tone) => {
276
+ if (disposed.current) return;
277
+ notes.current += 1;
278
+ const entry = {
279
+ key: `note:${notes.current}`,
280
+ kind: "note",
281
+ text: sanitise(text),
282
+ ...tone ? { tone } : {}
283
+ };
284
+ putTranscript((t) => commitEntry(t, entry));
285
+ }, [putTranscript]);
286
+ const say = useCallback((reply) => {
287
+ if (disposed.current) return;
288
+ notes.current += 1;
289
+ const entry = {
290
+ key: `said:${notes.current}`,
291
+ kind: "answer",
292
+ question: "",
293
+ reply,
294
+ mark: null,
295
+ unprompted: true,
296
+ at: Date.now()
297
+ };
298
+ putTranscript((t) => commitEntry(t, entry));
299
+ }, [putTranscript]);
300
+ const guard = useCallback((moduleId, fn) => {
301
+ const fail = (err) => print(`${moduleId}: ${err instanceof Error ? err.message : String(err)}`, "error");
302
+ try {
303
+ const r = fn();
304
+ if (r instanceof Promise) r.catch(fail);
305
+ } catch (err) {
306
+ fail(err);
307
+ }
308
+ }, [print]);
309
+ const ctxFor = useCallback((moduleId) => ({
310
+ theme,
311
+ signal: appAbort.current.signal,
312
+ size: () => sizeRef.current,
313
+ open: (panel, state, opts) => {
314
+ if (disposed.current) return;
315
+ if (!registry.panels.has(`${moduleId}/${panel}`)) throw new Error(`module '${moduleId}' has no panel '${panel}'`);
316
+ if (opts?.ifNoneOpen === true && overlayRef.current !== null) return;
317
+ putOverlay({
318
+ module: moduleId,
319
+ panel,
320
+ state
321
+ });
322
+ },
323
+ update: (panel, fn) => {
324
+ if (disposed.current) return;
325
+ const o = overlayRef.current;
326
+ if (o && o.module === moduleId && o.panel === panel) putOverlay({
327
+ ...o,
328
+ state: fn(o.state)
329
+ });
330
+ },
331
+ close: () => {
332
+ if (!disposed.current && overlayRef.current?.module === moduleId) putOverlay(null);
333
+ },
334
+ print,
335
+ say,
336
+ transcript: () => transcriptRef.current,
337
+ submit: (text, opts) => {
338
+ if (!disposed.current) handleLineRef.current(text, opts ?? {});
339
+ },
340
+ setDraft: (text) => {
341
+ if (!disposed.current) setDraftRef.current(text);
342
+ },
343
+ status: (id, text, tone) => {
344
+ if (disposed.current) return;
345
+ setStatus((s) => {
346
+ const next = new Map(s);
347
+ if (text === null) next.delete(`${moduleId}:${id}`);
348
+ else next.set(`${moduleId}:${id}`, {
349
+ text: sanitise(text),
350
+ tone: tone ?? "dim"
351
+ });
352
+ return next;
353
+ });
354
+ },
355
+ toast: (text, tone) => showToast(text, tone ?? "info", TOAST_MS),
356
+ emit: (event, payload) => {
357
+ for (const fn of [...bus.current.get(event) ?? []]) fn(payload);
358
+ },
359
+ on: (event, fn) => {
360
+ const set = bus.current.get(event) ?? /* @__PURE__ */ new Set();
361
+ const safe = (p) => guard(moduleId, () => fn(p));
362
+ set.add(safe);
363
+ bus.current.set(event, set);
364
+ return () => set.delete(safe);
365
+ }
366
+ }), [
367
+ print,
368
+ say,
369
+ guard,
370
+ putOverlay,
371
+ showToast
372
+ ]);
373
+ const runTurn = useCallback(async (line, module, handler, sent) => {
374
+ const id = nextId.current++;
375
+ const turn = {
376
+ id,
377
+ shown: sent?.shown ?? line,
378
+ ...sent?.emphasis === void 0 ? {} : { emphasis: sent.emphasis },
379
+ ...sent?.context === void 0 ? {} : { context: sent.context }
380
+ };
381
+ const ac = new AbortController();
382
+ let landed = false;
383
+ const land = (reply) => {
384
+ if (landed) return false;
385
+ landed = true;
386
+ flights.current.delete(id);
387
+ if (disposed.current) return true;
388
+ setLive((m) => {
389
+ const next = new Map(m);
390
+ next.delete(id);
391
+ return next;
392
+ });
393
+ putTranscript((t) => arriveTurn(t, id, reply));
394
+ return true;
395
+ };
396
+ flights.current.set(id, {
397
+ ac,
398
+ land
399
+ });
400
+ putTranscript((t) => sendTurn(t, id, turn.shown, turn.emphasis));
401
+ try {
402
+ for await (const u of handler.answer(line, ctxFor(module), ac.signal, turn)) {
403
+ if (landed) return;
404
+ if (u.phase === "final" || u.phase === "failed") {
405
+ land(u.reply);
406
+ return;
407
+ }
408
+ if (disposed.current) return;
409
+ setLive((m) => new Map(m).set(id, u.reply));
410
+ }
411
+ land({
412
+ kind: ERROR_KIND,
413
+ data: "no answer"
414
+ });
415
+ } catch (err) {
416
+ land({
417
+ kind: ERROR_KIND,
418
+ data: err instanceof Error ? err.message : String(err)
419
+ });
420
+ }
421
+ }, [ctxFor, putTranscript]);
422
+ const handleLine = useCallback((line, sent) => {
423
+ const text = line.trim();
424
+ const open = overlayRef.current;
425
+ const owner = open ? registry.panels.get(`${open.module}/${open.panel}`)?.def : void 0;
426
+ if (open && owner?.submit && (owner.mask === true && sent === void 0 || parseCommandLine(text) === null)) {
427
+ let next;
428
+ try {
429
+ next = owner.submit(open.state, line, ctxFor(open.module));
430
+ } catch (err) {
431
+ guard(open.module, () => {
432
+ throw err;
433
+ });
434
+ return;
435
+ }
436
+ const cur = overlayRef.current;
437
+ if (next !== void 0 && cur?.module === open.module && cur.panel === open.panel) putOverlay({
438
+ ...cur,
439
+ state: next
440
+ });
441
+ return;
442
+ }
443
+ if (text === "") return;
444
+ if (sent === void 0) putHistory(remember(historyRef.current, text));
445
+ const route = routeInput(text, registry, opts.defaultTurn);
446
+ if (route.kind === "command") Promise.resolve().then(() => route.spec.run(route.args, ctxFor(route.module))).catch((err) => print(`/${route.spec.name} failed: ${err instanceof Error ? err.message : String(err)}`, "error"));
447
+ else if (route.kind === "unknown-command") print(`unknown command /${route.name}`, "warn");
448
+ else if (route.kind === "unavailable-command") print(`/${route.name}: ${route.reason}`, "warn");
449
+ else if (route.kind === "turn") runTurn(text, route.module, route.handler, sent);
450
+ else print("nothing here answers that", "warn");
451
+ }, [
452
+ ctxFor,
453
+ print,
454
+ putHistory,
455
+ runTurn,
456
+ guard,
457
+ putOverlay
458
+ ]);
459
+ handleLineRef.current = handleLine;
460
+ const liveRef = useRef(null);
461
+ const lastLive = useRef(null);
462
+ useEffect(() => {
463
+ if (!anchored || liveRef.current === null) return;
464
+ const now = {
465
+ height: measureElement(liveRef.current).height,
466
+ committed: transcript.committed.filter((e) => e.kind !== "gap").length,
467
+ epoch
468
+ };
469
+ const shrink = liveShrink(lastLive.current, now);
470
+ lastLive.current = now;
471
+ if (shrink > 0) putTranscript((t) => addGap(t, shrink, epoch));
472
+ });
473
+ const panelOpen = overlay !== null || menu !== null;
474
+ const wasPanelOpen = useRef(panelOpen);
475
+ useEffect(() => {
476
+ if (anchored && wasPanelOpen.current && !panelOpen) relayout();
477
+ wasPanelOpen.current = panelOpen;
478
+ }, [panelOpen, relayout]);
479
+ const [pendingLine, setPendingLine] = useState(null);
480
+ useEffect(() => {
481
+ if (pendingLine === null) return;
482
+ setPendingLine(null);
483
+ handleLine(pendingLine);
484
+ }, [pendingLine, handleLine]);
485
+ const sendAfterClose = (line) => {
486
+ if (anchored) setPendingLine(line);
487
+ else handleLine(line);
488
+ };
489
+ const putDraft = useCallback((d) => {
490
+ draftRef.current = d;
491
+ requests.current += 1;
492
+ setDraft(d);
493
+ }, []);
494
+ const setDraftAndMenu = useCallback((d) => {
495
+ if (defOf(overlayRef.current)?.mask === true) {
496
+ putDraft(d);
497
+ if (menuRef.current !== null) putMenu(null);
498
+ return;
499
+ }
500
+ const before = paletteQuery(draftRef.current.text);
501
+ putDraft(d);
502
+ putHistory(settle(historyRef.current));
503
+ const q = paletteQuery(d.text);
504
+ const m = menuRef.current;
505
+ if (q !== null) {
506
+ const same = m?.kind === "palette" && before === q;
507
+ putMenu(same ? m : {
508
+ kind: "palette",
509
+ selected: 0,
510
+ moved: false
511
+ });
512
+ } else if (m !== null) putMenu(null);
513
+ }, [
514
+ putDraft,
515
+ putHistory,
516
+ putMenu,
517
+ defOf
518
+ ]);
519
+ setDraftRef.current = (text) => {
520
+ putArmed(null);
521
+ setDraftAndMenu(atEnd(text));
522
+ };
523
+ useEffect(() => {
524
+ disposed.current = false;
525
+ appAbort.current = new AbortController();
526
+ const disposers = [];
527
+ for (const m of registry.modules) guard(m.id, () => {
528
+ const dispose = m.init?.(ctxFor(m.id));
529
+ if (dispose) disposers.push({
530
+ module: m.id,
531
+ fn: dispose
532
+ });
533
+ });
534
+ return () => {
535
+ disposed.current = true;
536
+ if (toastTimer.current !== null) clearTimeout(toastTimer.current);
537
+ if (noticeTimer.current !== null) clearTimeout(noticeTimer.current);
538
+ appAbort.current.abort();
539
+ for (const f of flights.current.values()) f.ac.abort();
540
+ for (const d of disposers) guard(d.module, d.fn);
541
+ };
542
+ }, [ctxFor, guard]);
543
+ const commands = useMemo(() => [...registry.commands.values()].map((c) => c.spec), []);
544
+ const rowsFor = (text) => paletteRows(commands.filter((c) => unavailableReason(c) === null), paletteQuery(text) ?? "");
545
+ const paletteFor = (text) => rowsFor(text).map((r) => r.cmd);
546
+ const paletteItems = menu?.kind === "palette" ? paletteFor(draft.text) : [];
547
+ const menuRows = Math.max(1, panelOuter.rows - 1 - 2);
548
+ useInput((input, inkKey) => {
549
+ const key = {
550
+ ...inkKey,
551
+ input
552
+ };
553
+ const draft = draftRef.current;
554
+ const menu = menuRef.current;
555
+ const overlay = overlayRef.current;
556
+ const history = historyRef.current;
557
+ const armedOn = armedRef.current;
558
+ const state = {
559
+ overlay: overlay ? {
560
+ module: overlay.module,
561
+ panel: overlay.panel
562
+ } : null,
563
+ menu: menu?.kind ?? null,
564
+ draftEmpty: draft.text === "",
565
+ armed: armedOn !== null && overlay?.module === armedOn.module && overlay.panel === armedOn.panel && draft.text === armedOn.key ? armedOn.key : null
566
+ };
567
+ const route = resolveKey(state, key, registry);
568
+ switch (route.kind) {
569
+ case "exit-chord":
570
+ if (ctrlC(armedAt.current, Date.now()) === "exit") {
571
+ appAbort.current.abort();
572
+ exit();
573
+ } else {
574
+ armedAt.current = Date.now();
575
+ putArmed(null);
576
+ if (band) showNotice(exitHint, CTRL_C_WINDOW_MS);
577
+ else showToast(exitHint, "dim", CTRL_C_WINDOW_MS);
578
+ }
579
+ return;
580
+ case "menu": {
581
+ if (menu === null) return;
582
+ const count = (menu.kind === "palette" ? paletteFor(draft.text) : menu.items).length;
583
+ if (count === 0 && route.act !== "move") {
584
+ putMenu(null);
585
+ if (route.act === "run") {
586
+ putDraft(EMPTY_DRAFT);
587
+ sendAfterClose(draft.text);
588
+ }
589
+ return;
590
+ }
591
+ if (route.act === "move") putMenu({
592
+ ...menu,
593
+ selected: moveSelection(menu.selected, route.delta, count),
594
+ ...menu.kind === "palette" ? { moved: true } : {}
595
+ });
596
+ else if (route.act === "close") putMenu(null);
597
+ else if (menu.kind === "palette") {
598
+ const rows = rowsFor(draft.text);
599
+ const c = route.act === "run" ? enterRuns(rows, menu.selected, menu.moved) : rows[clampSelection(menu.selected, count)]?.cmd;
600
+ if (c === void 0) return;
601
+ if (c === null) {
602
+ putMenu(null);
603
+ putDraft(EMPTY_DRAFT);
604
+ sendAfterClose(draft.text);
605
+ } else if (route.act === "run" && c.args.every((a) => a.optional === true)) {
606
+ putMenu(null);
607
+ putDraft(EMPTY_DRAFT);
608
+ sendAfterClose(`/${c.name}`);
609
+ } else {
610
+ putMenu(null);
611
+ putDraft(atEnd(completeDraft(c)));
612
+ }
613
+ } else {
614
+ const s = menu.items[clampSelection(menu.selected, count)];
615
+ if (s === void 0) return;
616
+ const next = applySuggestion(draft.text, draft.cursor, s);
617
+ putMenu(null);
618
+ putDraft({
619
+ text: next.text,
620
+ cursor: next.cursor
621
+ });
622
+ }
623
+ return;
624
+ }
625
+ case "panel": {
626
+ const handler = (registry.panels.get(`${route.module}/${route.panel}`)?.def)?.on?.[route.act];
627
+ if (!handler || !overlay) return;
628
+ let next;
629
+ try {
630
+ next = handler(overlay.state, ctxFor(route.module));
631
+ } catch (err) {
632
+ guard(route.module, () => {
633
+ throw err;
634
+ });
635
+ return;
636
+ }
637
+ const cur = overlayRef.current;
638
+ if (next !== void 0 && cur?.module === overlay.module && cur.panel === overlay.panel) putOverlay({
639
+ ...cur,
640
+ state: next
641
+ });
642
+ return;
643
+ }
644
+ case "panel-close": {
645
+ putArmed(null);
646
+ const def = overlay ? registry.panels.get(`${overlay.module}/${overlay.panel}`)?.def : void 0;
647
+ if (overlay && def?.escape) {
648
+ let next;
649
+ try {
650
+ next = def.escape(overlay.state, ctxFor(overlay.module));
651
+ } catch (err) {
652
+ guard(overlay.module, () => {
653
+ throw err;
654
+ });
655
+ return;
656
+ }
657
+ const cur = overlayRef.current;
658
+ const same = cur?.module === overlay.module && cur.panel === overlay.panel;
659
+ if (next !== void 0 && same) {
660
+ putOverlay({
661
+ ...cur,
662
+ state: next
663
+ });
664
+ return;
665
+ }
666
+ if (!same) return;
667
+ }
668
+ putOverlay(null);
669
+ return;
670
+ }
671
+ case "confirm": {
672
+ const handler = (registry.panels.get(`${route.module}/${route.panel}`)?.def)?.on?.[route.act];
673
+ if (!handler || !overlay) return;
674
+ let next;
675
+ try {
676
+ next = handler(overlay.state, ctxFor(route.module));
677
+ } catch (err) {
678
+ guard(route.module, () => {
679
+ throw err;
680
+ });
681
+ return;
682
+ }
683
+ putArmed(null);
684
+ putDraft(EMPTY_DRAFT);
685
+ const cur = overlayRef.current;
686
+ if (next !== void 0 && cur?.module === overlay.module && cur.panel === overlay.panel) putOverlay({
687
+ ...cur,
688
+ state: next
689
+ });
690
+ return;
691
+ }
692
+ case "global":
693
+ guard(route.module, () => registry.globals.get(route.key)?.binding.run(ctxFor(route.module)));
694
+ return;
695
+ case "cancel-turn": {
696
+ const newest = [...flights.current.keys()].pop();
697
+ const f = newest === void 0 ? void 0 : flights.current.get(newest);
698
+ if (f) {
699
+ f.ac.abort();
700
+ f.land({
701
+ kind: CANCELLED_KIND,
702
+ data: "cancelled"
703
+ });
704
+ }
705
+ return;
706
+ }
707
+ case "history": {
708
+ if (defOf(overlay)?.mask === true) return;
709
+ putArmed(null);
710
+ const step = walk(history, route.dir, draft.text);
711
+ if (step === null) return;
712
+ putHistory(step.state);
713
+ putDraft(atEnd(step.text));
714
+ return;
715
+ }
716
+ case "submit": {
717
+ putArmed(null);
718
+ const line = draft.text;
719
+ putDraft(EMPTY_DRAFT);
720
+ putMenu(null);
721
+ handleLine(line);
722
+ return;
723
+ }
724
+ case "complete": {
725
+ if (defOf(overlay)?.mask === true) return;
726
+ const completers = registry.completers.map((c) => c.completer);
727
+ requests.current += 1;
728
+ const ticket = requests.current;
729
+ mergeCompletions(completers, draft.text, draft.cursor).then((items) => {
730
+ if (requests.current !== ticket) return;
731
+ if (items.length === 1 && items[0]) {
732
+ const next = applySuggestion(draft.text, draft.cursor, items[0]);
733
+ putDraft({
734
+ text: next.text,
735
+ cursor: next.cursor
736
+ });
737
+ } else if (items.length > 1) putMenu({
738
+ kind: "completion",
739
+ items,
740
+ selected: 0
741
+ });
742
+ });
743
+ return;
744
+ }
745
+ case "edit": {
746
+ const focused = overlay ? registry.panels.get(`${overlay.module}/${overlay.panel}`)?.def : void 0;
747
+ const hit = overlay && draft.text === "" && !key.ctrl && !key.meta ? focused?.confirm?.find((c) => c.key === input) : void 0;
748
+ putArmed(overlay && hit ? {
749
+ module: overlay.module,
750
+ panel: overlay.panel,
751
+ key: hit.key,
752
+ act: hit.act,
753
+ label: hit.label,
754
+ state: overlay.state
755
+ } : null);
756
+ if ([...input].length <= 1) {
757
+ const edited = editKey(draft, input, key);
758
+ if (edited !== null) {
759
+ setDraftAndMenu(edited);
760
+ return;
761
+ }
762
+ }
763
+ if (input === "") return;
764
+ const r = insertChunk(draft, input);
765
+ setDraftAndMenu(r.draft);
766
+ const masked = defOf(overlay)?.mask === true ? overlay : null;
767
+ for (const line of r.submit) {
768
+ const cur = overlayRef.current;
769
+ if (masked !== null && (cur === null || cur.module !== masked.module || cur.panel !== masked.panel)) break;
770
+ handleLine(line);
771
+ }
772
+ return;
773
+ }
774
+ case "ignore": return;
775
+ }
776
+ });
777
+ usePaste((text) => {
778
+ putArmed(null);
779
+ const open = overlayRef.current;
780
+ const def = open === null ? void 0 : registry.panels.get(`${open.module}/${open.panel}`)?.def;
781
+ if (open !== null && !((def?.confirm?.length ?? 0) > 0 || def?.submit !== void 0)) return;
782
+ const line = pasteToLine(text);
783
+ if (line === "") return;
784
+ setDraftAndMenu(insertChunk(draftRef.current, line).draft);
785
+ });
786
+ const renderReply = (reply) => {
787
+ if (reply.kind === ERROR_KIND) return /* @__PURE__ */ jsxs(Text, {
788
+ color: theme.color.error,
789
+ children: [
790
+ theme.glyph.fail,
791
+ " ",
792
+ sanitise(String(reply.data))
793
+ ]
794
+ });
795
+ if (reply.kind === CANCELLED_KIND) return /* @__PURE__ */ jsx(Text, {
796
+ color: theme.color.dim,
797
+ children: "cancelled"
798
+ });
799
+ const r = registry.renderers.get(reply.kind);
800
+ return r ? /* @__PURE__ */ jsx(Safe, {
801
+ module: r.module,
802
+ theme,
803
+ render: () => r.render({
804
+ data: reply.data,
805
+ theme,
806
+ size: { columns: width }
807
+ })
808
+ }) : /* @__PURE__ */ jsx(Text, { children: sanitise(String(reply.data)) });
809
+ };
810
+ const chips = (reply) => {
811
+ if (!reply.chips || reply.chips.length === 0) return null;
812
+ const seen = /* @__PURE__ */ new Map();
813
+ const kids = [];
814
+ for (const c of reply.chips) {
815
+ const tone = c.tone ?? "dim";
816
+ const n = (seen.get(`${tone}:${c.label}`) ?? 0) + 1;
817
+ seen.set(`${tone}:${c.label}`, n);
818
+ if (kids.length > 0) kids.push(" · ");
819
+ kids.push(/* @__PURE__ */ jsx(Text, {
820
+ color: theme.color[tone],
821
+ children: sanitise(c.label)
822
+ }, `${tone}:${c.label}#${n}`));
823
+ }
824
+ return /* @__PURE__ */ jsx(Text, { children: kids });
825
+ };
826
+ const panel = overlay ? registry.panels.get(`${overlay.module}/${overlay.panel}`)?.def : void 0;
827
+ const shownDraft = panel?.mask === true ? maskDraft(draft) : draft;
828
+ const under = graphemeAt(shownDraft.text, shownDraft.cursor);
829
+ const waiting = (f, held) => {
830
+ const reply = live.get(f.id);
831
+ const body = reply ? renderReply(reply) : /* @__PURE__ */ jsxs(Text, {
832
+ color: theme.color.dim,
833
+ children: [spinnerFrame(Math.floor(Date.now() / 120)), " Thinking"]
834
+ });
835
+ if (waitingView) return /* @__PURE__ */ jsx(Safe, {
836
+ module: "app",
837
+ theme,
838
+ render: () => waitingView({
839
+ flight: f,
840
+ held,
841
+ ...reply === void 0 ? {} : { reply },
842
+ body,
843
+ theme,
844
+ size: { columns: width }
845
+ })
846
+ });
847
+ if (held) return body;
848
+ return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
849
+ color: theme.color.accent,
850
+ children: [
851
+ "⟨",
852
+ f.mark,
853
+ "⟩ "
854
+ ]
855
+ }), body] });
856
+ };
857
+ const committed = (e) => {
858
+ if (e.kind === "ask") {
859
+ if (askView) return /* @__PURE__ */ jsx(Safe, {
860
+ module: "app",
861
+ theme,
862
+ render: () => askView({
863
+ entry: e,
864
+ held: false,
865
+ theme,
866
+ size: { columns: width }
867
+ })
868
+ }, e.key);
869
+ return /* @__PURE__ */ jsxs(Text, {
870
+ color: theme.color.accent,
871
+ children: [
872
+ e.mark !== null ? `⟨${e.mark}⟩ ` : "",
873
+ theme.glyph.prompt,
874
+ " ",
875
+ sanitise(e.question)
876
+ ]
877
+ }, e.key);
878
+ }
879
+ if (e.kind === "answer") {
880
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [renderReply(e.reply), chips(e.reply)] });
881
+ if (answerView) return /* @__PURE__ */ jsx(Safe, {
882
+ module: "app",
883
+ theme,
884
+ render: () => answerView({
885
+ entry: e,
886
+ body,
887
+ theme,
888
+ size: { columns: width }
889
+ })
890
+ }, e.key);
891
+ return /* @__PURE__ */ jsxs(Box, {
892
+ flexDirection: "column",
893
+ children: [e.mark !== null ? /* @__PURE__ */ jsxs(Text, {
894
+ color: theme.color.accent,
895
+ children: [
896
+ "↩ ⟨",
897
+ e.mark,
898
+ "⟩"
899
+ ]
900
+ }) : null, body]
901
+ }, e.key);
902
+ }
903
+ if (e.kind === "note") {
904
+ if (noteView) return /* @__PURE__ */ jsx(Safe, {
905
+ module: "app",
906
+ theme,
907
+ render: () => noteView({
908
+ entry: e,
909
+ theme,
910
+ size: { columns: width }
911
+ })
912
+ }, e.key);
913
+ return /* @__PURE__ */ jsx(Text, {
914
+ color: theme.color[e.tone ?? "dim"],
915
+ children: sanitise(e.text)
916
+ }, e.key);
917
+ }
918
+ return /* @__PURE__ */ jsx(Text, { children: "\n".repeat(Math.max(0, e.lines - 1)) }, e.key);
919
+ };
920
+ return /* @__PURE__ */ jsxs(Box, {
921
+ flexDirection: "column",
922
+ children: [/* @__PURE__ */ jsx(Static, {
923
+ items: transcript.committed.filter((e) => e.kind !== "gap" || e.epoch === epoch),
924
+ children: (e) => /* @__PURE__ */ jsx(Box, {
925
+ paddingX: gutter,
926
+ flexDirection: "column",
927
+ children: committed(e)
928
+ }, e.key)
929
+ }, `static:${epoch}`), /* @__PURE__ */ jsxs(Box, {
930
+ ref: liveRef,
931
+ flexDirection: "row",
932
+ paddingX: gutter,
933
+ children: [/* @__PURE__ */ jsxs(Box, {
934
+ flexDirection: "column",
935
+ flexGrow: 1,
936
+ children: [
937
+ markedFlights(transcript).map((f) => /* @__PURE__ */ jsx(Box, { children: waiting(f, false) }, `marked:${f.id}`)),
938
+ heldAsks(transcript).map((f) => /* @__PURE__ */ jsxs(Box, {
939
+ flexDirection: "column",
940
+ children: [askView ? /* @__PURE__ */ jsx(Safe, {
941
+ module: "app",
942
+ theme,
943
+ render: () => askView({
944
+ entry: f,
945
+ held: true,
946
+ theme,
947
+ size: { columns: width }
948
+ })
949
+ }) : /* @__PURE__ */ jsxs(Text, {
950
+ color: theme.color.accent,
951
+ children: [
952
+ theme.glyph.prompt,
953
+ " ",
954
+ sanitise(f.question)
955
+ ]
956
+ }), waiting(f, true)]
957
+ }, f.id)),
958
+ panel && overlay ? /* @__PURE__ */ jsx(Box, {
959
+ backgroundColor: theme.color.raised,
960
+ paddingX: PANEL_PAD_X,
961
+ paddingY: PANEL_PAD_Y,
962
+ flexDirection: "column",
963
+ children: /* @__PURE__ */ jsx(Safe, {
964
+ module: overlay.module,
965
+ theme,
966
+ render: () => panel.view({
967
+ state: overlay.state,
968
+ theme,
969
+ size: panelSize,
970
+ ...armed !== null && armed.module === overlay.module && armed.panel === overlay.panel && draft.text === armed.key ? { armed: {
971
+ key: armed.key,
972
+ label: armed.label
973
+ } } : {}
974
+ })
975
+ }, `${overlay.module}/${overlay.panel}`)
976
+ }) : null,
977
+ menu?.kind === "palette" ? /* @__PURE__ */ jsx(MenuList, {
978
+ theme,
979
+ rows: paletteItems.map((c) => ({
980
+ key: c.name,
981
+ name: commandUsage(c),
982
+ detail: c.summary
983
+ })),
984
+ selected: menu.selected,
985
+ height: menuRows,
986
+ empty: `no command matches ${draft.text}`,
987
+ hint: "tab complete · enter run · esc close"
988
+ }) : null,
989
+ menu?.kind === "completion" ? /* @__PURE__ */ jsx(MenuList, {
990
+ theme,
991
+ rows: menu.items.map((s) => ({
992
+ key: s.insert,
993
+ name: s.label,
994
+ detail: s.detail ?? ""
995
+ })),
996
+ selected: menu.selected,
997
+ height: menuRows,
998
+ empty: "no completions",
999
+ hint: "tab complete · esc close"
1000
+ }) : null,
1001
+ band ? /* @__PURE__ */ jsxs(Box, {
1002
+ borderStyle: "bold",
1003
+ borderTop: false,
1004
+ borderRight: false,
1005
+ borderBottom: false,
1006
+ borderColor: theme.color.edge,
1007
+ backgroundColor: theme.color.surfaceAlt,
1008
+ paddingX: 1,
1009
+ paddingY: 1,
1010
+ flexDirection: "column",
1011
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
1012
+ shownDraft.text.slice(0, shownDraft.cursor),
1013
+ under === "" ? /* @__PURE__ */ jsx(Text, {
1014
+ color: theme.color.cursor,
1015
+ children: "█"
1016
+ }) : /* @__PURE__ */ jsx(Text, {
1017
+ inverse: true,
1018
+ children: under
1019
+ }),
1020
+ under === "" ? "" : shownDraft.text.slice(shownDraft.cursor + under.length)
1021
+ ] }), /* @__PURE__ */ jsxs(Box, {
1022
+ marginTop: 1,
1023
+ children: [
1024
+ presence ? /* @__PURE__ */ jsx(Box, {
1025
+ flexShrink: 0,
1026
+ children: /* @__PURE__ */ jsxs(Text, {
1027
+ bold: true,
1028
+ children: [sanitise(presence.name), " "]
1029
+ })
1030
+ }) : null,
1031
+ /* @__PURE__ */ jsx(Box, {
1032
+ flexGrow: 1,
1033
+ minWidth: 0,
1034
+ children: /* @__PURE__ */ jsx(Text, {
1035
+ color: theme.color.dim,
1036
+ wrap: "truncate",
1037
+ children: presence?.view ? /* @__PURE__ */ jsx(Safe, {
1038
+ module: presenceModule,
1039
+ theme,
1040
+ render: () => presence.view?.({ theme })
1041
+ }) : null
1042
+ })
1043
+ }),
1044
+ /* @__PURE__ */ jsx(Box, {
1045
+ flexShrink: 0,
1046
+ paddingLeft: notice === null ? 0 : 2,
1047
+ children: /* @__PURE__ */ jsx(Text, {
1048
+ color: theme.color.warn,
1049
+ children: notice ?? " "
1050
+ })
1051
+ })
1052
+ ]
1053
+ })]
1054
+ }) : /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
1055
+ color: theme.color.accent,
1056
+ children: [theme.glyph.prompt, " "]
1057
+ }), shownDraft.text] }),
1058
+ toast ? /* @__PURE__ */ jsx(Text, {
1059
+ color: theme.color[toast.tone],
1060
+ children: toast.text
1061
+ }) : null,
1062
+ /* @__PURE__ */ jsxs(Box, {
1063
+ ...band ? { marginTop: 1 } : {},
1064
+ children: [registry.status.map((s) => /* @__PURE__ */ jsx(Box, {
1065
+ marginRight: 2,
1066
+ children: /* @__PURE__ */ jsx(Safe, {
1067
+ module: s.module,
1068
+ theme,
1069
+ render: () => s.item.view({ theme })
1070
+ })
1071
+ }, `${s.module}:${s.item.id}`)), [...status.entries()].map(([k, v]) => /* @__PURE__ */ jsxs(Text, {
1072
+ color: theme.color[v.tone],
1073
+ children: [v.text, " "]
1074
+ }, k))]
1075
+ })
1076
+ ]
1077
+ }), showRail ? /* @__PURE__ */ jsx(Box, {
1078
+ flexDirection: "column",
1079
+ width: 32,
1080
+ marginLeft: 1,
1081
+ children: registry.rail.map((r) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Safe, {
1082
+ module: r.module,
1083
+ theme,
1084
+ render: () => r.item.view({ theme })
1085
+ }) }, `${r.module}:${r.item.id}`))
1086
+ }) : null]
1087
+ })]
1088
+ });
1089
+ }
1090
+ return {
1091
+ registry,
1092
+ App,
1093
+ async run() {
1094
+ const prepare = runTerminalOptions(opts);
1095
+ const restore = prepare === null ? () => {} : prepareTerminal(process.stdout, prepare);
1096
+ await render(/* @__PURE__ */ jsx(App, {}), { exitOnCtrlC: false }).waitUntilExit();
1097
+ restore();
1098
+ }
1099
+ };
1100
+ }
1101
+
1102
+ //#endregion
1103
+ export { CLEAR_PANE, CTRL_C_WINDOW_MS, CompositionError, DEFAULT_GROUND, EMPTY_DRAFT, EMPTY_HISTORY, HISTORY_MAX, MENU_NAME_WIDTH, RESERVED_GLOBAL_KEYS, RESET_GROUND, addGap, anchorPadding, answerEntry, applyInput, applySuggestion, arriveTurn, assertComposes, atEnd, clampSelection, commandUsage, commitEntry, completeDraft, composeModules, createApp, ctrlC, defaultPanelRows, defineModule, editKey, emptyTranscript, enterRuns, groundSequence, heldAsks, insertChunk, keySpecOf, liveShrink, markedFlights, maskDraft, mayWalk, menuWindow, mergeCompletions, moveSelection, normaliseKeySpec, paletteMatches, paletteQuery, paletteRows, panelOf, parseCommandLine, pasteToLine, prepareSequence, prepareTerminal, reflowSequence, remember, resolveKey, routeInput, sanitise, sendTurn, setPanel, settle, walk, wordLeft, wordRight };