@mnmal-ai/surface-core 0.15.0 → 0.17.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/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import { $ as maskDraft, A as completeDraft, B as EMPTY_HISTORY, C as reprintStart, D as MENU_NAME_WIDTH, E as unavailableReason, F as paletteQuery, G as walk, H as mayWalk, I as paletteRows, J as EMPTY_DRAFT, K as CTRL_C_WINDOW_MS, L as parseCommandLine, M as menuWindow, N as moveSelection, O as clampSelection, P as paletteMatches, Q as insertChunk, R as keySpecOf, S as reflowSequence, T as routeInput, U as remember, V as HISTORY_MAX, W as settle, X as editKey, Y as atEnd, Z as graphemeAt, _ as anchorPadding, a as answerEntry, at as RESERVED_GLOBAL_KEYS, b as prepareSequence, c as dropGaps, ct as normaliseKeySpec, d as markedFlights, et as pasteToLine, f as sendTurn, g as RESET_GROUND, h as REPRINT_LINES, i as addGap, it as CompositionError, j as enterRuns, k as commandUsage, l as emptyTranscript, lt as applySuggestion, m as DEFAULT_GROUND, n as setPanel, nt as wordRight, o as arriveTurn, ot as assertComposes, p as CLEAR_PANE, q as ctrlC, r as defineModule, rt as applyInput, s as commitEntry, st as composeModules, t as panelOf, tt as wordLeft, u as heldAsks, ut as mergeCompletions, v as groundSequence, w as sanitise, x as prepareTerminal, y as liveShrink, z as resolveKey } from "./overlay-DMjCbsaC.js";
2
- import { spinnerFrame } from "@mnmal-ai/surface-kit";
1
+ import { $ as parseCommandLine, A as RESET_GROUND, At as normaliseKeySpec, B as sanitise, C as emptyTranscript, Ct as wordLeft, D as CLEAR_PANE, Dt as RESERVED_GLOBAL_KEYS, E as sendTurn, Et as CompositionError, F as liveShrink, G as commandUsage, H as unavailableReason, I as prepareSequence, J as menuWindow, K as completeDraft, L as prepareTerminal, M as WHEEL_ARROWS_ON, Mt as mergeCompletions, N as anchorPadding, O as DEFAULT_GROUND, Ot as assertComposes, P as groundSequence, Q as paletteRows, R as reflowSequence, S as dropGaps, St as pasteToLine, T as markedFlights, Tt as applyInput, U as MENU_NAME_WIDTH, V as routeInput, W as clampSelection, X as paletteMatches, Y as moveSelection, Z as paletteQuery, _ as visibleRows, _t as atEnd, a as backspace, at as remember, b as arriveTurn, bt as insertChunk, c as listStep, ct as FOLLOWING, d as placement, dt as resolveFullscreen, et as keySpecOf, f as push, ft as scrollBy, g as typeChar, gt as EMPTY_DRAFT, h as turn, ht as ctrlC, i as back, it as mayWalk, j as WHEEL_ARROWS_OFF, jt as applySuggestion, k as REPRINT_LINES, kt as composeModules, l as move, lt as linesBelow, m as startFilter, mt as CTRL_C_WINDOW_MS, n as setPanel, nt as EMPTY_HISTORY, o as cursorRow, ot as settle, p as scrollDetail, pt as viewTop, q as enterRuns, r as defineModule, rt as HISTORY_MAX, s as endFilter, st as walk, t as panelOf, tt as resolveKey, u as pagerAt, ut as pageStep, v as addGap, vt as editKey, w as heldAsks, wt as wordRight, x as commitEntry, xt as maskDraft, y as answerEntry, yt as graphemeAt, z as reprintStart } from "./overlay-5s8slC38.js";
2
+ import { ListRows, spinnerFrame } from "@mnmal-ai/surface-kit";
3
3
  import { Box, Static, Text, measureElement, render, useApp, useInput, usePaste, useStdout } from "ink";
4
4
  import { Component, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { listLines, wrapCells } from "@mnmal-ai/surface-kit/list";
6
7
 
7
8
  //#region src/app.tsx
8
9
  /** Calls a module's view as a component, so a throw lands in the boundary and not in the shell. */
@@ -71,6 +72,15 @@ const OVERLAY_PAD_Y = 1;
71
72
  * not scanned; both come out of the size the panel view is handed (operator, 2026-10-09). */
72
73
  const PANEL_PAD_X = 4;
73
74
  const PANEL_PAD_Y = 1;
75
+ /** What a panel view may draw in: its insets, its own or the default, come out of the outer size. */
76
+ function panelInner(outer, def) {
77
+ const x = def?.inset?.x ?? PANEL_PAD_X;
78
+ const y = def?.inset?.y ?? PANEL_PAD_Y;
79
+ return {
80
+ columns: Math.max(1, outer.columns - 2 * x),
81
+ rows: Math.max(1, outer.rows - 2 * y)
82
+ };
83
+ }
74
84
  /**
75
85
  * 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
76
86
  * that follows the selection, and a hint line.
@@ -117,11 +127,12 @@ function defaultPanelRows(terminalRows, opts) {
117
127
  return Math.max(3, terminalRows - SHELL_ROWS - (opts?.band === true ? BAND_ROWS : 0));
118
128
  }
119
129
  /** What `run()` prepares the terminal with, or `null` when the host has prepared it already. */
120
- function runTerminalOptions(opts) {
130
+ function runTerminalOptions(opts, fullscreen) {
121
131
  if (opts.terminal === false) return null;
122
132
  return {
123
133
  anchor: opts.anchored === true,
124
- ...opts.terminal
134
+ ...opts.terminal,
135
+ ...fullscreen ? { fullscreen } : {}
125
136
  };
126
137
  }
127
138
  function createApp(opts) {
@@ -140,9 +151,13 @@ function createApp(opts) {
140
151
  const presenceModule = registry.presence?.module ?? "";
141
152
  const panelRows = opts.panelRows ?? ((r) => defaultPanelRows(r, { band }));
142
153
  const anchored = opts.anchored === true;
154
+ const fs = resolveFullscreen(opts.fullscreen, process.env.SURFACE_FULLSCREEN);
143
155
  function App({ columns, rows }) {
144
156
  const { exit } = useApp();
145
- const stdout = useStdout().stdout;
157
+ const ink = useStdout();
158
+ const stdout = ink.stdout;
159
+ const full = fs.on && stdout.isTTY === true;
160
+ const relays = anchored && !full;
146
161
  const [term, setTerm] = useState(() => ({
147
162
  columns: stdout.columns ?? 120,
148
163
  rows: stdout.rows ?? 30
@@ -157,12 +172,16 @@ function createApp(opts) {
157
172
  const heights = useRef(/* @__PURE__ */ new Map());
158
173
  const [reprintFrom, setReprintFrom] = useState(0);
159
174
  const relayout = useCallback(() => {
160
- if (stdout.isTTY) stdout.write(reflowSequence(stdout.rows));
175
+ if (stdout.isTTY) ink.write(reflowSequence(stdout.rows));
161
176
  putTranscript(dropGaps);
162
177
  const kept = transcriptRef.current.committed;
163
178
  setReprintFrom(reprintStart(kept.map((e) => heights.current.get(e.key) ?? 1), REPRINT_LINES));
164
179
  setEpoch((e) => e + 1);
165
- }, [stdout, putTranscript]);
180
+ }, [
181
+ stdout,
182
+ ink,
183
+ putTranscript
184
+ ]);
166
185
  useEffect(() => {
167
186
  const laidOut = {
168
187
  columns: stdout.columns ?? 120,
@@ -174,7 +193,7 @@ function createApp(opts) {
174
193
  columns: stdout.columns ?? 120,
175
194
  rows: stdout.rows ?? 30
176
195
  });
177
- if (!anchored) return;
196
+ if (!relays) return;
178
197
  clearTimeout(timer);
179
198
  timer = setTimeout(() => {
180
199
  const now = {
@@ -193,7 +212,11 @@ function createApp(opts) {
193
212
  clearTimeout(timer);
194
213
  stdout.off("resize", onResize);
195
214
  };
196
- }, [stdout, relayout]);
215
+ }, [
216
+ stdout,
217
+ relayout,
218
+ relays
219
+ ]);
197
220
  const width = Math.max(1, (columns ?? term.columns) - 2 * gutter);
198
221
  const height = rows ?? term.rows;
199
222
  const showRail = registry.rail.length > 0 && width >= railMin;
@@ -201,12 +224,8 @@ function createApp(opts) {
201
224
  columns: Math.max(1, width - (showRail ? RAIL_COLUMNS : 0)),
202
225
  rows: Math.max(1, panelRows(height))
203
226
  };
204
- const panelSize = {
205
- columns: Math.max(1, panelOuter.columns - 8),
206
- rows: Math.max(1, panelOuter.rows - 2)
207
- };
208
- const sizeRef = useRef(panelSize);
209
- sizeRef.current = panelSize;
227
+ const outerRef = useRef(panelOuter);
228
+ outerRef.current = panelOuter;
210
229
  const [live, setLive] = useState(/* @__PURE__ */ new Map());
211
230
  const [draft, setDraft] = useState(EMPTY_DRAFT);
212
231
  const [, setHistory] = useState(EMPTY_HISTORY);
@@ -238,6 +257,12 @@ function createApp(opts) {
238
257
  armedRef.current = a;
239
258
  setArmed(a);
240
259
  }, []);
260
+ const [scroll, setScroll] = useState(FOLLOWING);
261
+ const scrollRef = useRef(FOLLOWING);
262
+ const putScroll = useCallback((next) => {
263
+ scrollRef.current = next;
264
+ setScroll(next);
265
+ }, []);
241
266
  const defOf = useCallback((o) => o ? registry.panels.get(`${o.module}/${o.panel}`)?.def : void 0, []);
242
267
  const putHistory = useCallback((h) => {
243
268
  historyRef.current = h;
@@ -327,7 +352,7 @@ function createApp(opts) {
327
352
  const ctxFor = useCallback((moduleId) => ({
328
353
  theme,
329
354
  signal: appAbort.current.signal,
330
- size: () => sizeRef.current,
355
+ size: () => panelInner(outerRef.current, defOf(overlayRef.current)),
331
356
  open: (panel, state, opts) => {
332
357
  if (disposed.current) return;
333
358
  if (!registry.panels.has(`${moduleId}/${panel}`)) throw new Error(`module '${moduleId}' has no panel '${panel}'`);
@@ -338,6 +363,7 @@ function createApp(opts) {
338
363
  state
339
364
  });
340
365
  },
366
+ panelOpen: () => overlayRef.current !== null,
341
367
  update: (panel, fn) => {
342
368
  if (disposed.current) return;
343
369
  const o = overlayRef.current;
@@ -386,7 +412,8 @@ function createApp(opts) {
386
412
  say,
387
413
  guard,
388
414
  putOverlay,
389
- showToast
415
+ showToast,
416
+ defOf
390
417
  ]);
391
418
  const runTurn = useCallback(async (line, module, handler, sent) => {
392
419
  const id = nextId.current++;
@@ -438,6 +465,7 @@ function createApp(opts) {
438
465
  }
439
466
  }, [ctxFor, putTranscript]);
440
467
  const handleLine = useCallback((line, sent) => {
468
+ putScroll(FOLLOWING);
441
469
  const text = line.trim();
442
470
  const open = overlayRef.current;
443
471
  const owner = open ? registry.panels.get(`${open.module}/${open.panel}`)?.def : void 0;
@@ -472,13 +500,43 @@ function createApp(opts) {
472
500
  putHistory,
473
501
  runTurn,
474
502
  guard,
475
- putOverlay
503
+ putOverlay,
504
+ putScroll
476
505
  ]);
477
506
  handleLineRef.current = handleLine;
507
+ const viewportRef = useRef(null);
508
+ const flightsRef = useRef(null);
509
+ const [measured, setMeasured] = useState({
510
+ content: 0,
511
+ viewport: 1
512
+ });
513
+ const measuredRef = useRef(measured);
514
+ const contentRows = useCallback(() => {
515
+ let rows = 0;
516
+ for (const e of transcriptRef.current.committed) rows += heights.current.get(e.key) ?? 1;
517
+ return rows + (flightsRef.current === null ? 0 : measureElement(flightsRef.current).height);
518
+ }, []);
519
+ const viewportRows = useCallback(() => viewportRef.current === null ? measuredRef.current.viewport : Math.max(1, measureElement(viewportRef.current).height), []);
520
+ useEffect(() => {
521
+ if (!full || viewportRef.current === null) return;
522
+ const now = {
523
+ content: contentRows(),
524
+ viewport: viewportRows()
525
+ };
526
+ const held = scrollRef.current.held;
527
+ if (held !== null && held >= Math.max(0, now.content - now.viewport)) putScroll(FOLLOWING);
528
+ const was = measuredRef.current;
529
+ if (now.content === was.content && now.viewport === was.viewport) return;
530
+ measuredRef.current = now;
531
+ setMeasured(now);
532
+ });
533
+ useEffect(() => {
534
+ if (fs.invalid !== null) print(`SURFACE_FULLSCREEN=${fs.invalid} ignored: use on or off`, "warn");
535
+ }, [print]);
478
536
  const liveRef = useRef(null);
479
537
  const lastLive = useRef(null);
480
538
  useEffect(() => {
481
- if (!anchored || liveRef.current === null) return;
539
+ if (!relays || liveRef.current === null) return;
482
540
  const now = {
483
541
  height: measureElement(liveRef.current).height,
484
542
  committed: transcript.committed.filter((e) => e.kind !== "gap").length,
@@ -491,9 +549,13 @@ function createApp(opts) {
491
549
  const panelOpen = overlay !== null || menu !== null;
492
550
  const wasPanelOpen = useRef(panelOpen);
493
551
  useEffect(() => {
494
- if (anchored && wasPanelOpen.current && !panelOpen) relayout();
552
+ if (relays && wasPanelOpen.current && !panelOpen) relayout();
495
553
  wasPanelOpen.current = panelOpen;
496
- }, [panelOpen, relayout]);
554
+ }, [
555
+ panelOpen,
556
+ relayout,
557
+ relays
558
+ ]);
497
559
  const [pendingLine, setPendingLine] = useState(null);
498
560
  useEffect(() => {
499
561
  if (pendingLine === null) return;
@@ -501,7 +563,7 @@ function createApp(opts) {
501
563
  handleLine(pendingLine);
502
564
  }, [pendingLine, handleLine]);
503
565
  const sendAfterClose = (line) => {
504
- if (anchored) setPendingLine(line);
566
+ if (relays) setPendingLine(line);
505
567
  else handleLine(line);
506
568
  };
507
569
  const putDraft = useCallback((d) => {
@@ -580,7 +642,8 @@ function createApp(opts) {
580
642
  } : null,
581
643
  menu: menu?.kind ?? null,
582
644
  draftEmpty: draft.text === "",
583
- armed: armedOn !== null && overlay?.module === armedOn.module && overlay.panel === armedOn.panel && draft.text === armedOn.key ? armedOn.key : null
645
+ armed: armedOn !== null && overlay?.module === armedOn.module && overlay.panel === armedOn.panel && draft.text === armedOn.key ? armedOn.key : null,
646
+ ...full ? { viewport: scrollRef.current.held === null ? "following" : "held" } : {}
584
647
  };
585
648
  const route = resolveKey(state, key, registry);
586
649
  switch (route.kind) {
@@ -640,12 +703,16 @@ function createApp(opts) {
640
703
  }
641
704
  return;
642
705
  }
643
- case "panel": {
644
- const handler = (registry.panels.get(`${route.module}/${route.panel}`)?.def)?.on?.[route.act];
645
- if (!handler || !overlay) return;
706
+ case "panel":
707
+ case "panel-text": {
708
+ const def = registry.panels.get(`${route.module}/${route.panel}`)?.def;
709
+ const act = route.kind === "panel" ? def?.on?.[route.act] : void 0;
710
+ const typed = route.kind === "panel-text" ? def?.text : void 0;
711
+ if (!act && !typed || !overlay) return;
646
712
  let next;
647
713
  try {
648
- next = handler(overlay.state, ctxFor(route.module));
714
+ const c = ctxFor(route.module);
715
+ next = act ? act(overlay.state, c) : typed?.(overlay.state, route.text, c);
649
716
  } catch (err) {
650
717
  guard(route.module, () => {
651
718
  throw err;
@@ -731,6 +798,13 @@ function createApp(opts) {
731
798
  putDraft(atEnd(step.text));
732
799
  return;
733
800
  }
801
+ case "scroll": {
802
+ const content = contentRows();
803
+ const viewport = viewportRows();
804
+ const step = route.act === "page-up" ? -pageStep(viewport) : pageStep(viewport);
805
+ putScroll(route.act === "bottom" ? FOLLOWING : scrollBy(scrollRef.current, step, content, viewport));
806
+ return;
807
+ }
734
808
  case "submit": {
735
809
  putArmed(null);
736
810
  const line = draft.text;
@@ -941,6 +1015,230 @@ function createApp(opts) {
941
1015
  text: `… ${reprintFrom} earlier ${reprintFrom === 1 ? "entry" : "entries"} not reprinted`,
942
1016
  tone: "dim"
943
1017
  }, ...transcript.committed.slice(reprintFrom)];
1018
+ const flightRows = /* @__PURE__ */ jsxs(Fragment, { children: [markedFlights(transcript).map((f) => /* @__PURE__ */ jsx(Box, { children: waiting(f, false) }, `marked:${f.id}`)), heldAsks(transcript).map((f) => /* @__PURE__ */ jsxs(Box, {
1019
+ flexDirection: "column",
1020
+ children: [askView ? /* @__PURE__ */ jsx(Safe, {
1021
+ module: "app",
1022
+ theme,
1023
+ render: () => askView({
1024
+ entry: f,
1025
+ held: true,
1026
+ theme,
1027
+ size: { columns: width }
1028
+ })
1029
+ }) : /* @__PURE__ */ jsxs(Text, {
1030
+ color: theme.color.accent,
1031
+ children: [
1032
+ theme.glyph.prompt,
1033
+ " ",
1034
+ sanitise(f.question)
1035
+ ]
1036
+ }), waiting(f, true)]
1037
+ }, f.id))] });
1038
+ const overlayRows = /* @__PURE__ */ jsxs(Fragment, { children: [
1039
+ panel && overlay ? /* @__PURE__ */ jsx(Box, {
1040
+ backgroundColor: panel.ground ?? theme.color.raised,
1041
+ paddingX: panel.inset?.x ?? PANEL_PAD_X,
1042
+ paddingY: panel.inset?.y ?? PANEL_PAD_Y,
1043
+ flexDirection: "column",
1044
+ children: /* @__PURE__ */ jsx(Safe, {
1045
+ module: overlay.module,
1046
+ theme,
1047
+ render: () => panel.view({
1048
+ state: overlay.state,
1049
+ theme,
1050
+ size: panelInner(panelOuter, panel),
1051
+ ...armed !== null && armed.module === overlay.module && armed.panel === overlay.panel && draft.text === armed.key ? { armed: {
1052
+ key: armed.key,
1053
+ label: armed.label
1054
+ } } : {}
1055
+ })
1056
+ }, `${overlay.module}/${overlay.panel}`)
1057
+ }) : null,
1058
+ menu?.kind === "palette" ? /* @__PURE__ */ jsx(MenuList, {
1059
+ theme,
1060
+ rows: paletteItems.map((c) => ({
1061
+ key: c.name,
1062
+ name: commandUsage(c),
1063
+ detail: c.summary
1064
+ })),
1065
+ selected: menu.selected,
1066
+ height: menuRows,
1067
+ empty: `no command matches ${draft.text}`,
1068
+ hint: "tab complete · enter run · esc close"
1069
+ }) : null,
1070
+ menu?.kind === "completion" ? /* @__PURE__ */ jsx(MenuList, {
1071
+ theme,
1072
+ rows: menu.items.map((s) => ({
1073
+ key: s.insert,
1074
+ name: s.label,
1075
+ detail: s.detail ?? ""
1076
+ })),
1077
+ selected: menu.selected,
1078
+ height: menuRows,
1079
+ empty: "no completions",
1080
+ hint: "tab complete · esc close"
1081
+ }) : null
1082
+ ] });
1083
+ const pinnedRows = /* @__PURE__ */ jsxs(Fragment, { children: [
1084
+ band ? /* @__PURE__ */ jsxs(Box, {
1085
+ borderStyle: "bold",
1086
+ borderTop: false,
1087
+ borderRight: false,
1088
+ borderBottom: false,
1089
+ borderColor: theme.color.edge,
1090
+ backgroundColor: theme.color.surfaceAlt,
1091
+ paddingX: 1,
1092
+ paddingY: 1,
1093
+ flexDirection: "column",
1094
+ children: [/* @__PURE__ */ jsxs(Text, { children: [
1095
+ shownDraft.text.slice(0, shownDraft.cursor),
1096
+ under === "" ? /* @__PURE__ */ jsx(Text, {
1097
+ color: theme.color.cursor,
1098
+ children: "█"
1099
+ }) : /* @__PURE__ */ jsx(Text, {
1100
+ inverse: true,
1101
+ children: under
1102
+ }),
1103
+ under === "" ? "" : shownDraft.text.slice(shownDraft.cursor + under.length)
1104
+ ] }), /* @__PURE__ */ jsxs(Box, {
1105
+ marginTop: 1,
1106
+ children: [
1107
+ presence ? /* @__PURE__ */ jsx(Box, {
1108
+ flexShrink: 0,
1109
+ children: /* @__PURE__ */ jsxs(Text, {
1110
+ bold: true,
1111
+ children: [sanitise(presence.name), " "]
1112
+ })
1113
+ }) : null,
1114
+ /* @__PURE__ */ jsx(Box, {
1115
+ flexGrow: 1,
1116
+ minWidth: 0,
1117
+ children: /* @__PURE__ */ jsx(Text, {
1118
+ color: theme.color.dim,
1119
+ wrap: "truncate",
1120
+ children: presence?.view ? /* @__PURE__ */ jsx(Safe, {
1121
+ module: presenceModule,
1122
+ theme,
1123
+ render: () => presence.view?.({ theme })
1124
+ }) : null
1125
+ })
1126
+ }),
1127
+ /* @__PURE__ */ jsx(Box, {
1128
+ flexShrink: 0,
1129
+ paddingLeft: notice === null ? 0 : 2,
1130
+ children: /* @__PURE__ */ jsx(Text, {
1131
+ color: theme.color.warn,
1132
+ children: notice ?? " "
1133
+ })
1134
+ })
1135
+ ]
1136
+ })]
1137
+ }) : /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
1138
+ color: theme.color.accent,
1139
+ children: [theme.glyph.prompt, " "]
1140
+ }), shownDraft.text] }),
1141
+ toast ? /* @__PURE__ */ jsx(Text, {
1142
+ color: theme.color[toast.tone],
1143
+ children: toast.text
1144
+ }) : null,
1145
+ /* @__PURE__ */ jsxs(Box, {
1146
+ ...band ? { marginTop: 1 } : {},
1147
+ children: [registry.status.map((s) => /* @__PURE__ */ jsx(Box, {
1148
+ marginRight: 2,
1149
+ children: /* @__PURE__ */ jsx(Safe, {
1150
+ module: s.module,
1151
+ theme,
1152
+ render: () => s.item.view({ theme })
1153
+ })
1154
+ }, `${s.module}:${s.item.id}`)), [...status.entries()].map(([k, v]) => /* @__PURE__ */ jsxs(Text, {
1155
+ color: theme.color[v.tone],
1156
+ children: [v.text, " "]
1157
+ }, k))]
1158
+ })
1159
+ ] });
1160
+ const railColumn = /* @__PURE__ */ jsx(Fragment, { children: showRail ? /* @__PURE__ */ jsx(Box, {
1161
+ flexDirection: "column",
1162
+ width: 32,
1163
+ marginLeft: 1,
1164
+ children: registry.rail.map((r) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Safe, {
1165
+ module: r.module,
1166
+ theme,
1167
+ render: () => r.item.view({ theme })
1168
+ }) }, `${r.module}:${r.item.id}`))
1169
+ }) : null });
1170
+ if (full) {
1171
+ const { content, viewport } = measured;
1172
+ const top = viewTop(scroll, content, viewport);
1173
+ const below = linesBelow(scroll, content, viewport);
1174
+ const lo = top - viewport;
1175
+ const hi = top + 2 * viewport;
1176
+ const shown = [];
1177
+ let at = 0;
1178
+ let skipped = null;
1179
+ for (const e of transcript.committed) {
1180
+ const h = heights.current.get(e.key);
1181
+ const rows = h ?? 1;
1182
+ if (at + rows > lo && at < hi || h === void 0) {
1183
+ if (skipped === null) skipped = at;
1184
+ shown.push(e);
1185
+ }
1186
+ at += rows;
1187
+ }
1188
+ const rowsAbove = skipped ?? at;
1189
+ const markerText = `↓ ${below} lines below · End to jump back`;
1190
+ return /* @__PURE__ */ jsxs(Box, {
1191
+ flexDirection: "row",
1192
+ height,
1193
+ paddingX: gutter,
1194
+ children: [/* @__PURE__ */ jsxs(Box, {
1195
+ flexDirection: "column",
1196
+ flexGrow: 1,
1197
+ children: [/* @__PURE__ */ jsxs(Box, {
1198
+ ref: viewportRef,
1199
+ flexDirection: "column",
1200
+ flexGrow: 1,
1201
+ flexShrink: 1,
1202
+ minHeight: 1,
1203
+ overflow: "hidden",
1204
+ children: [/* @__PURE__ */ jsxs(Box, {
1205
+ flexDirection: "column",
1206
+ flexShrink: 0,
1207
+ marginTop: -(top - rowsAbove),
1208
+ children: [shown.map((e) => /* @__PURE__ */ jsx(Printed, {
1209
+ id: e.key,
1210
+ heights: heights.current,
1211
+ paddingX: 0,
1212
+ children: committed(e)
1213
+ }, e.key)), /* @__PURE__ */ jsx(Box, {
1214
+ ref: flightsRef,
1215
+ flexDirection: "column",
1216
+ flexShrink: 0,
1217
+ children: flightRows
1218
+ })]
1219
+ }), panelOpen ? /* @__PURE__ */ jsx(Box, {
1220
+ position: "absolute",
1221
+ bottom: 0,
1222
+ width: "100%",
1223
+ flexDirection: "column",
1224
+ children: overlayRows
1225
+ }) : below > 0 ? /* @__PURE__ */ jsx(Box, {
1226
+ position: "absolute",
1227
+ bottom: 0,
1228
+ children: /* @__PURE__ */ jsx(Text, {
1229
+ color: theme.color.dim,
1230
+ wrap: "truncate",
1231
+ children: markerText.padEnd(panelOuter.columns)
1232
+ })
1233
+ }) : null]
1234
+ }), /* @__PURE__ */ jsx(Box, {
1235
+ flexDirection: "column",
1236
+ flexShrink: 0,
1237
+ children: pinnedRows
1238
+ })]
1239
+ }), railColumn]
1240
+ });
1241
+ }
944
1242
  return /* @__PURE__ */ jsxs(Box, {
945
1243
  flexDirection: "column",
946
1244
  children: [/* @__PURE__ */ jsx(Static, {
@@ -959,170 +1257,475 @@ function createApp(opts) {
959
1257
  flexDirection: "column",
960
1258
  flexGrow: 1,
961
1259
  children: [
962
- markedFlights(transcript).map((f) => /* @__PURE__ */ jsx(Box, { children: waiting(f, false) }, `marked:${f.id}`)),
963
- heldAsks(transcript).map((f) => /* @__PURE__ */ jsxs(Box, {
964
- flexDirection: "column",
965
- children: [askView ? /* @__PURE__ */ jsx(Safe, {
966
- module: "app",
967
- theme,
968
- render: () => askView({
969
- entry: f,
970
- held: true,
971
- theme,
972
- size: { columns: width }
973
- })
974
- }) : /* @__PURE__ */ jsxs(Text, {
975
- color: theme.color.accent,
976
- children: [
977
- theme.glyph.prompt,
978
- " ",
979
- sanitise(f.question)
980
- ]
981
- }), waiting(f, true)]
982
- }, f.id)),
983
- panel && overlay ? /* @__PURE__ */ jsx(Box, {
984
- backgroundColor: theme.color.raised,
985
- paddingX: PANEL_PAD_X,
986
- paddingY: PANEL_PAD_Y,
987
- flexDirection: "column",
988
- children: /* @__PURE__ */ jsx(Safe, {
989
- module: overlay.module,
990
- theme,
991
- render: () => panel.view({
992
- state: overlay.state,
993
- theme,
994
- size: panelSize,
995
- ...armed !== null && armed.module === overlay.module && armed.panel === overlay.panel && draft.text === armed.key ? { armed: {
996
- key: armed.key,
997
- label: armed.label
998
- } } : {}
999
- })
1000
- }, `${overlay.module}/${overlay.panel}`)
1001
- }) : null,
1002
- menu?.kind === "palette" ? /* @__PURE__ */ jsx(MenuList, {
1003
- theme,
1004
- rows: paletteItems.map((c) => ({
1005
- key: c.name,
1006
- name: commandUsage(c),
1007
- detail: c.summary
1008
- })),
1009
- selected: menu.selected,
1010
- height: menuRows,
1011
- empty: `no command matches ${draft.text}`,
1012
- hint: "tab complete · enter run · esc close"
1013
- }) : null,
1014
- menu?.kind === "completion" ? /* @__PURE__ */ jsx(MenuList, {
1015
- theme,
1016
- rows: menu.items.map((s) => ({
1017
- key: s.insert,
1018
- name: s.label,
1019
- detail: s.detail ?? ""
1020
- })),
1021
- selected: menu.selected,
1022
- height: menuRows,
1023
- empty: "no completions",
1024
- hint: "tab complete · esc close"
1025
- }) : null,
1026
- band ? /* @__PURE__ */ jsxs(Box, {
1027
- borderStyle: "bold",
1028
- borderTop: false,
1029
- borderRight: false,
1030
- borderBottom: false,
1031
- borderColor: theme.color.edge,
1032
- backgroundColor: theme.color.surfaceAlt,
1033
- paddingX: 1,
1034
- paddingY: 1,
1035
- flexDirection: "column",
1036
- children: [/* @__PURE__ */ jsxs(Text, { children: [
1037
- shownDraft.text.slice(0, shownDraft.cursor),
1038
- under === "" ? /* @__PURE__ */ jsx(Text, {
1039
- color: theme.color.cursor,
1040
- children: "█"
1041
- }) : /* @__PURE__ */ jsx(Text, {
1042
- inverse: true,
1043
- children: under
1044
- }),
1045
- under === "" ? "" : shownDraft.text.slice(shownDraft.cursor + under.length)
1046
- ] }), /* @__PURE__ */ jsxs(Box, {
1047
- marginTop: 1,
1048
- children: [
1049
- presence ? /* @__PURE__ */ jsx(Box, {
1050
- flexShrink: 0,
1051
- children: /* @__PURE__ */ jsxs(Text, {
1052
- bold: true,
1053
- children: [sanitise(presence.name), " "]
1054
- })
1055
- }) : null,
1056
- /* @__PURE__ */ jsx(Box, {
1057
- flexGrow: 1,
1058
- minWidth: 0,
1059
- children: /* @__PURE__ */ jsx(Text, {
1060
- color: theme.color.dim,
1061
- wrap: "truncate",
1062
- children: presence?.view ? /* @__PURE__ */ jsx(Safe, {
1063
- module: presenceModule,
1064
- theme,
1065
- render: () => presence.view?.({ theme })
1066
- }) : null
1067
- })
1068
- }),
1069
- /* @__PURE__ */ jsx(Box, {
1070
- flexShrink: 0,
1071
- paddingLeft: notice === null ? 0 : 2,
1072
- children: /* @__PURE__ */ jsx(Text, {
1073
- color: theme.color.warn,
1074
- children: notice ?? " "
1075
- })
1076
- })
1077
- ]
1078
- })]
1079
- }) : /* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
1080
- color: theme.color.accent,
1081
- children: [theme.glyph.prompt, " "]
1082
- }), shownDraft.text] }),
1083
- toast ? /* @__PURE__ */ jsx(Text, {
1084
- color: theme.color[toast.tone],
1085
- children: toast.text
1086
- }) : null,
1087
- /* @__PURE__ */ jsxs(Box, {
1088
- ...band ? { marginTop: 1 } : {},
1089
- children: [registry.status.map((s) => /* @__PURE__ */ jsx(Box, {
1090
- marginRight: 2,
1091
- children: /* @__PURE__ */ jsx(Safe, {
1092
- module: s.module,
1093
- theme,
1094
- render: () => s.item.view({ theme })
1095
- })
1096
- }, `${s.module}:${s.item.id}`)), [...status.entries()].map(([k, v]) => /* @__PURE__ */ jsxs(Text, {
1097
- color: theme.color[v.tone],
1098
- children: [v.text, " "]
1099
- }, k))]
1100
- })
1260
+ flightRows,
1261
+ overlayRows,
1262
+ pinnedRows
1101
1263
  ]
1102
- }), showRail ? /* @__PURE__ */ jsx(Box, {
1103
- flexDirection: "column",
1104
- width: 32,
1105
- marginLeft: 1,
1106
- children: registry.rail.map((r) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Safe, {
1107
- module: r.module,
1108
- theme,
1109
- render: () => r.item.view({ theme })
1110
- }) }, `${r.module}:${r.item.id}`))
1111
- }) : null]
1264
+ }), railColumn]
1112
1265
  })]
1113
1266
  });
1114
1267
  }
1115
1268
  return {
1116
1269
  registry,
1117
1270
  App,
1271
+ fullscreen: fs.on,
1118
1272
  async run() {
1119
- const prepare = runTerminalOptions(opts);
1273
+ const full = fs.on && process.stdout.isTTY === true;
1274
+ const prepare = runTerminalOptions(opts, full);
1120
1275
  const restore = prepare === null ? () => {} : prepareTerminal(process.stdout, prepare);
1121
- await render(/* @__PURE__ */ jsx(App, {}), { exitOnCtrlC: false }).waitUntilExit();
1276
+ await render(/* @__PURE__ */ jsx(App, {}), {
1277
+ exitOnCtrlC: false,
1278
+ alternateScreen: full
1279
+ }).waitUntilExit();
1122
1280
  restore();
1123
1281
  }
1124
1282
  };
1125
1283
  }
1126
1284
 
1127
1285
  //#endregion
1128
- 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 };
1286
+ //#region src/list-pager.tsx
1287
+ /** A list longer than this is paged rather than printed. */
1288
+ const INLINE_LIST_ROWS = 10;
1289
+ const LIST_REPLY_KIND = "surface.list";
1290
+ /** Rows a page holds in a panel `rows` tall: the heading, filter and hint lines take the rest. */
1291
+ const pageSizeFor = (rows) => Math.max(5, rows - 6);
1292
+ const cleanSpan = (s) => s.em === true ? {
1293
+ text: sanitise(s.text),
1294
+ em: true
1295
+ } : { text: sanitise(s.text) };
1296
+ const cleanRow = (r) => ({
1297
+ key: r.key,
1298
+ name: typeof r.name === "string" ? sanitise(r.name) : r.name.map(cleanSpan),
1299
+ ...r.details ? { details: r.details.map(sanitise) } : {},
1300
+ ...r.value !== void 0 ? { value: sanitise(r.value) } : {}
1301
+ });
1302
+ /** Everything an app hands in reaches the terminal sanitised. */
1303
+ const cleanList = (l) => ({
1304
+ heading: sanitise(l.heading),
1305
+ rows: l.rows.map(cleanRow),
1306
+ ...l.total !== void 0 ? { total: l.total } : {},
1307
+ ...l.notes ? { notes: l.notes.map(sanitise) } : {}
1308
+ });
1309
+ const cleanDetail = (d) => ({
1310
+ title: sanitise(d.title),
1311
+ ...d.meta !== void 0 ? { meta: sanitise(d.meta) } : {},
1312
+ ...d.body !== void 0 ? { body: sanitise(d.body) } : {}
1313
+ });
1314
+ const inAll = (l) => l.total !== void 0 && l.total > l.rows.length ? `, ${l.total} in all` : "";
1315
+ const isPromise = (v) => typeof v?.then === "function";
1316
+ const errorText = (e) => sanitise(e instanceof Error ? e.message : String(e));
1317
+ /** The detail's body lines and the room the panel gives them. */
1318
+ function detailLayout(d, size) {
1319
+ const lines = d.body === void 0 ? [] : wrapCells(d.body, size.columns);
1320
+ const room = Math.max(1, size.rows - (d.meta === void 0 ? 3 : 4));
1321
+ return {
1322
+ lines,
1323
+ room,
1324
+ maxScroll: Math.max(0, lines.length - room)
1325
+ };
1326
+ }
1327
+ function Hint({ theme, parts }) {
1328
+ return /* @__PURE__ */ jsx(Box, {
1329
+ marginTop: 1,
1330
+ children: /* @__PURE__ */ jsx(Text, {
1331
+ color: theme.color.dim,
1332
+ wrap: "truncate",
1333
+ children: parts.join(" · ")
1334
+ })
1335
+ });
1336
+ }
1337
+ function ListPage({ step, depth, theme, size }) {
1338
+ const shown = visibleRows(step);
1339
+ const per = pageSizeFor(size.rows);
1340
+ const pages = Math.max(1, Math.ceil(shown.length / per));
1341
+ const at = placement(step, per);
1342
+ const page = Math.min(at.page, pages - 1);
1343
+ const slice = shown.slice(page * per, page * per + per);
1344
+ const total = step.list.total;
1345
+ const of = total !== void 0 && total > step.list.rows.length ? ` · ${step.list.rows.length} of ${total}` : "";
1346
+ const hint = [
1347
+ "↑↓ row",
1348
+ "←→ page",
1349
+ ...step.hooks.open ? ["enter open"] : [],
1350
+ ...step.hooks.related ? ["r related"] : [],
1351
+ "/ filter",
1352
+ depth > 1 || step.filter !== "" || step.filtering ? "esc back" : "esc close"
1353
+ ];
1354
+ return /* @__PURE__ */ jsxs(Box, {
1355
+ flexDirection: "column",
1356
+ children: [
1357
+ /* @__PURE__ */ jsxs(Text, {
1358
+ wrap: "truncate",
1359
+ children: [
1360
+ /* @__PURE__ */ jsx(Text, {
1361
+ color: theme.color.accent,
1362
+ children: "📋 List "
1363
+ }),
1364
+ /* @__PURE__ */ jsx(Text, {
1365
+ bold: true,
1366
+ children: step.list.heading
1367
+ }),
1368
+ /* @__PURE__ */ jsx(Text, {
1369
+ color: theme.color.dim,
1370
+ children: `${of} · page ${page + 1} of ${pages}`
1371
+ })
1372
+ ]
1373
+ }),
1374
+ step.filtering || step.filter !== "" ? /* @__PURE__ */ jsx(Text, {
1375
+ color: theme.color.dim,
1376
+ children: `filter: ${step.filter}${step.filtering ? "▌" : ""}`
1377
+ }) : null,
1378
+ /* @__PURE__ */ jsx(Box, {
1379
+ marginTop: 1,
1380
+ flexDirection: "column",
1381
+ children: slice.length === 0 ? /* @__PURE__ */ jsx(Text, {
1382
+ color: theme.color.dim,
1383
+ children: `no rows match ${step.filter}`
1384
+ }) : /* @__PURE__ */ jsx(ListRows, {
1385
+ lines: listLines(slice, size.columns, page * per + 1),
1386
+ theme,
1387
+ cursor: at.cursor
1388
+ })
1389
+ }),
1390
+ /* @__PURE__ */ jsx(Hint, {
1391
+ theme,
1392
+ parts: hint
1393
+ })
1394
+ ]
1395
+ });
1396
+ }
1397
+ function DetailPage({ step, theme, size }) {
1398
+ const hint = [
1399
+ "↑↓ scroll",
1400
+ ...step.hooks.related ? ["r related"] : [],
1401
+ "esc back"
1402
+ ];
1403
+ if (step.detail === "loading" || "error" in step.detail) return /* @__PURE__ */ jsxs(Box, {
1404
+ flexDirection: "column",
1405
+ children: [step.detail === "loading" ? /* @__PURE__ */ jsx(Text, {
1406
+ color: theme.color.dim,
1407
+ children: "loading…"
1408
+ }) : /* @__PURE__ */ jsx(Text, {
1409
+ color: theme.color.warn,
1410
+ children: step.detail.error
1411
+ }), /* @__PURE__ */ jsx(Hint, {
1412
+ theme,
1413
+ parts: ["esc back"]
1414
+ })]
1415
+ });
1416
+ const d = step.detail;
1417
+ const { lines, room } = detailLayout(d, size);
1418
+ return /* @__PURE__ */ jsxs(Box, {
1419
+ flexDirection: "column",
1420
+ children: [
1421
+ /* @__PURE__ */ jsxs(Text, {
1422
+ wrap: "truncate",
1423
+ children: [/* @__PURE__ */ jsx(Text, {
1424
+ color: theme.color.accent,
1425
+ children: "📋 "
1426
+ }), /* @__PURE__ */ jsx(Text, {
1427
+ bold: true,
1428
+ children: d.title
1429
+ })]
1430
+ }),
1431
+ d.meta !== void 0 ? /* @__PURE__ */ jsx(Text, {
1432
+ color: theme.color.dim,
1433
+ wrap: "truncate",
1434
+ children: d.meta
1435
+ }) : null,
1436
+ /* @__PURE__ */ jsx(Box, {
1437
+ marginTop: 1,
1438
+ flexDirection: "column",
1439
+ children: lines.slice(step.scroll, step.scroll + room).map((l, i) => /* @__PURE__ */ jsx(Text, { children: l }, i))
1440
+ }),
1441
+ /* @__PURE__ */ jsx(Hint, {
1442
+ theme,
1443
+ parts: hint
1444
+ })
1445
+ ]
1446
+ });
1447
+ }
1448
+ function PagerView({ state, theme, size }) {
1449
+ const step = state.steps[state.steps.length - 1];
1450
+ if (step === void 0) return null;
1451
+ if (step.kind === "list") return /* @__PURE__ */ jsx(ListPage, {
1452
+ step,
1453
+ depth: state.steps.length,
1454
+ theme,
1455
+ size
1456
+ });
1457
+ if (step.kind === "detail") return /* @__PURE__ */ jsx(DetailPage, {
1458
+ step,
1459
+ theme,
1460
+ size
1461
+ });
1462
+ return /* @__PURE__ */ jsxs(Box, {
1463
+ flexDirection: "column",
1464
+ children: [step.error === void 0 ? /* @__PURE__ */ jsx(Text, {
1465
+ color: theme.color.dim,
1466
+ children: "loading…"
1467
+ }) : /* @__PURE__ */ jsx(Text, {
1468
+ color: theme.color.warn,
1469
+ children: step.error
1470
+ }), /* @__PURE__ */ jsx(Hint, {
1471
+ theme,
1472
+ parts: ["esc back"]
1473
+ })]
1474
+ });
1475
+ }
1476
+ function ListReply({ data, theme, columns }) {
1477
+ const { list, paged } = data;
1478
+ const n = list.rows.length;
1479
+ const width = Math.max(1, columns - 2);
1480
+ let tail = null;
1481
+ if (paged) tail = `${n} rows — in the list (/list)${inAll(list)}`;
1482
+ else if (n > 10) tail = `…and ${n - 10} more — /list to page through them${inAll(list)}`;
1483
+ else if (list.total !== void 0 && list.total > n) tail = `… ${list.total} in all`;
1484
+ return /* @__PURE__ */ jsxs(Box, {
1485
+ flexDirection: "column",
1486
+ children: [
1487
+ /* @__PURE__ */ jsx(Text, {
1488
+ bold: true,
1489
+ wrap: "wrap",
1490
+ children: `${list.heading}:`
1491
+ }),
1492
+ paged ? null : /* @__PURE__ */ jsx(Box, {
1493
+ marginTop: 1,
1494
+ children: /* @__PURE__ */ jsx(ListRows, {
1495
+ lines: listLines(list.rows.slice(0, 10), width),
1496
+ theme
1497
+ })
1498
+ }),
1499
+ tail !== null ? /* @__PURE__ */ jsx(Text, {
1500
+ color: theme.color.dim,
1501
+ children: ` ${tail}`
1502
+ }) : null,
1503
+ (list.notes ?? []).length > 0 ? /* @__PURE__ */ jsx(Box, {
1504
+ marginTop: 1,
1505
+ flexDirection: "column",
1506
+ children: (list.notes ?? []).map((note, i) => /* @__PURE__ */ jsx(Text, {
1507
+ color: theme.color.dim,
1508
+ italic: true,
1509
+ wrap: "wrap",
1510
+ children: note
1511
+ }, i))
1512
+ }) : null
1513
+ ]
1514
+ });
1515
+ }
1516
+ /**
1517
+ * The pager module and `present`, which a turn handler yields as its reply:
1518
+ * `yield { phase: 'final', reply: pager.present(list, { open, related }) }`. A list of up to
1519
+ * `INLINE_LIST_ROWS` prints inline. A longer one opens the pager when no panel is open, and the
1520
+ * transcript then carries only its heading and a pointer; otherwise its first rows print inline.
1521
+ * `/list` reopens the last long list where it was left.
1522
+ */
1523
+ function listPager() {
1524
+ let own;
1525
+ let last = null;
1526
+ /**
1527
+ * Keeps the first step as the operator left it, and returns the state to show. Only for the
1528
+ * list `/list` would reopen: an older list paged while a newer one landed must not take its place.
1529
+ */
1530
+ const keep = (s) => {
1531
+ const first = s.steps[0];
1532
+ const was = last?.steps[0];
1533
+ if (first?.kind === "list" && was?.kind === "list" && first.list === was.list) last = {
1534
+ steps: [first],
1535
+ token: s.token
1536
+ };
1537
+ return s;
1538
+ };
1539
+ const top = (s) => s.steps[s.steps.length - 1];
1540
+ const replaceTop = (s, step) => ({
1541
+ ...s,
1542
+ steps: [...s.steps.slice(0, -1), step]
1543
+ });
1544
+ /** Settles an async step, only while the pager still shows the step it was started for. */
1545
+ const settle = (ctx, token, step) => ctx.update("pager", (cur) => {
1546
+ const t = top(cur);
1547
+ return cur.token === token && t !== void 0 ? replaceTop(cur, step(t)) : cur;
1548
+ });
1549
+ const open = (s, ctx) => {
1550
+ const t = top(s);
1551
+ if (t?.kind !== "list") return s;
1552
+ if (t.filtering) return keep(endFilter(s));
1553
+ const row = cursorRow(s);
1554
+ const hook = t.hooks.open;
1555
+ if (row === void 0 || hook === void 0) return s;
1556
+ const base = {
1557
+ kind: "detail",
1558
+ row,
1559
+ hooks: t.hooks,
1560
+ detail: "loading",
1561
+ scroll: 0
1562
+ };
1563
+ let got;
1564
+ try {
1565
+ got = hook(row);
1566
+ } catch (e) {
1567
+ return push(s, {
1568
+ ...base,
1569
+ detail: { error: errorText(e) }
1570
+ });
1571
+ }
1572
+ if (!isPromise(got)) return push(s, {
1573
+ ...base,
1574
+ detail: cleanDetail(got)
1575
+ });
1576
+ const next = push(s, base);
1577
+ got.then((d) => settle(ctx, next.token, (cur) => cur.kind === "detail" ? {
1578
+ ...cur,
1579
+ detail: cleanDetail(d)
1580
+ } : cur), (e) => settle(ctx, next.token, (cur) => cur.kind === "detail" ? {
1581
+ ...cur,
1582
+ detail: { error: errorText(e) }
1583
+ } : cur));
1584
+ return next;
1585
+ };
1586
+ const related = (s, ctx) => {
1587
+ const t = top(s);
1588
+ if (t?.kind === "list" && t.filtering) return keep(typeChar(s, "r"));
1589
+ if (t === void 0 || t.kind === "loading-related") return s;
1590
+ const row = cursorRow(s);
1591
+ const hook = t.hooks.related;
1592
+ if (row === void 0 || hook === void 0) return s;
1593
+ const hooks = t.hooks;
1594
+ let got;
1595
+ try {
1596
+ got = hook(row);
1597
+ } catch (e) {
1598
+ return push(s, {
1599
+ kind: "loading-related",
1600
+ row,
1601
+ error: errorText(e)
1602
+ });
1603
+ }
1604
+ if (!isPromise(got)) return push(s, listStep(cleanList(got), hooks));
1605
+ const next = push(s, {
1606
+ kind: "loading-related",
1607
+ row
1608
+ });
1609
+ got.then((l) => settle(ctx, next.token, () => listStep(cleanList(l), hooks)), (e) => settle(ctx, next.token, () => ({
1610
+ kind: "loading-related",
1611
+ row,
1612
+ error: errorText(e)
1613
+ })));
1614
+ return next;
1615
+ };
1616
+ const vertical = (delta) => (s, ctx) => {
1617
+ const t = top(s);
1618
+ if (t?.kind === "detail" && t.detail !== "loading" && !("error" in t.detail)) return scrollDetail(s, delta, detailLayout(t.detail, ctx.size()).maxScroll);
1619
+ return keep(move(s, delta, pageSizeFor(ctx.size().rows)));
1620
+ };
1621
+ const page = (delta) => (s, ctx) => keep(turn(s, delta, pageSizeFor(ctx.size().rows)));
1622
+ return {
1623
+ module: defineModule("surface.list", {
1624
+ init(ctx) {
1625
+ own = ctx;
1626
+ return () => {
1627
+ own = void 0;
1628
+ };
1629
+ },
1630
+ commands: [{
1631
+ name: "list",
1632
+ summary: "page through the last long list",
1633
+ args: [],
1634
+ run(_args, ctx) {
1635
+ if (last === null) {
1636
+ ctx.print("nothing to page through");
1637
+ return;
1638
+ }
1639
+ ctx.open("pager", last);
1640
+ }
1641
+ }],
1642
+ renderers: { [LIST_REPLY_KIND]: ({ data, theme, size }) => /* @__PURE__ */ jsx(ListReply, {
1643
+ data,
1644
+ theme,
1645
+ columns: size.columns
1646
+ }) },
1647
+ panels: { pager: {
1648
+ view: ({ state, theme, size }) => /* @__PURE__ */ jsx(PagerView, {
1649
+ state,
1650
+ theme,
1651
+ size
1652
+ }),
1653
+ keymap: [
1654
+ {
1655
+ key: "up",
1656
+ act: "up"
1657
+ },
1658
+ {
1659
+ key: "down",
1660
+ act: "down"
1661
+ },
1662
+ {
1663
+ key: "left",
1664
+ act: "left"
1665
+ },
1666
+ {
1667
+ key: "right",
1668
+ act: "right"
1669
+ },
1670
+ {
1671
+ key: "enter",
1672
+ act: "open"
1673
+ },
1674
+ {
1675
+ key: "r",
1676
+ act: "related"
1677
+ },
1678
+ {
1679
+ key: "/",
1680
+ act: "filter"
1681
+ },
1682
+ {
1683
+ key: "backspace",
1684
+ act: "backspace"
1685
+ }
1686
+ ],
1687
+ on: {
1688
+ up: vertical(-1),
1689
+ down: vertical(1),
1690
+ left: page(-1),
1691
+ right: page(1),
1692
+ open,
1693
+ related,
1694
+ filter: (s) => {
1695
+ const t = top(s);
1696
+ return keep(t?.kind === "list" && t.filtering ? typeChar(s, "/") : startFilter(s));
1697
+ },
1698
+ backspace: (s) => keep(backspace(s))
1699
+ },
1700
+ text: (s, text) => keep(typeChar(s, text)),
1701
+ escape: (s) => {
1702
+ const prev = back(s);
1703
+ return prev === void 0 ? void 0 : keep(prev);
1704
+ }
1705
+ } }
1706
+ }),
1707
+ present(list, hooks = {}) {
1708
+ const clean = cleanList(list);
1709
+ if (clean.rows.length <= 10) return {
1710
+ kind: LIST_REPLY_KIND,
1711
+ data: {
1712
+ list: clean,
1713
+ paged: false
1714
+ }
1715
+ };
1716
+ last = pagerAt(clean, hooks);
1717
+ const paged = own !== void 0 && !own.panelOpen();
1718
+ if (paged) own?.open("pager", last);
1719
+ return {
1720
+ kind: LIST_REPLY_KIND,
1721
+ data: {
1722
+ list: clean,
1723
+ paged
1724
+ }
1725
+ };
1726
+ }
1727
+ };
1728
+ }
1729
+
1730
+ //#endregion
1731
+ export { CLEAR_PANE, CTRL_C_WINDOW_MS, CompositionError, DEFAULT_GROUND, EMPTY_DRAFT, EMPTY_HISTORY, FOLLOWING, HISTORY_MAX, INLINE_LIST_ROWS, LIST_REPLY_KIND, MENU_NAME_WIDTH, RESERVED_GLOBAL_KEYS, RESET_GROUND, WHEEL_ARROWS_OFF, WHEEL_ARROWS_ON, addGap, anchorPadding, answerEntry, applyInput, applySuggestion, arriveTurn, assertComposes, atEnd, back, backspace, clampSelection, commandUsage, commitEntry, completeDraft, composeModules, createApp, ctrlC, cursorRow, defaultPanelRows, defineModule, editKey, emptyTranscript, endFilter, enterRuns, groundSequence, heldAsks, insertChunk, keySpecOf, linesBelow, listPager, listStep, liveShrink, markedFlights, maskDraft, mayWalk, menuWindow, mergeCompletions, move, moveSelection, normaliseKeySpec, pageSizeFor, pageStep, pagerAt, paletteMatches, paletteQuery, paletteRows, panelOf, parseCommandLine, pasteToLine, placement, prepareSequence, prepareTerminal, push, reflowSequence, remember, resolveFullscreen, resolveKey, routeInput, sanitise, scrollBy, scrollDetail, sendTurn, setPanel, settle, startFilter, turn, typeChar, viewTop, visibleRows, walk, wordLeft, wordRight };