@miadi/chronicle-tui 0.7.0 → 0.8.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/README.md CHANGED
@@ -36,6 +36,8 @@ On Termux, install with `--ignore-scripts`. Node 22 or later.
36
36
  | `v` | hold a Miadi review in the episode (writer token) |
37
37
  | `i` | the episode's inquiries and, when you may write, the shelf: Enter attaches one, a dry run first, `y` confirms |
38
38
  | `t` | the episode's companion seat (Mia) through tide: the plan first, `y` runs it only when it would act, `A` asks again archiving the running session |
39
+ | `T` | this machine's tmux sessions, the episode in focus's first. Inside tmux, Enter opens one beside the chronicle; outside, it hands the terminal over until you detach |
40
+ | `F`, `X` | with a session beside: focus it (prefix+arrow comes back); close the split (the session goes on) |
39
41
  | `L` | relate episodes: `L` marks the one in focus, move to the other, `L` again, say why (relates to or continues from, one way or both). Esc forgets the mark |
40
42
  | `?` | all keys · `q` quits |
41
43
 
@@ -97,6 +99,27 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
97
99
  Write actions declare `effect: "write"` and are offered only when the app said
98
100
  it would record them.
99
101
 
102
+ ## A session beside the chronicle
103
+
104
+ Run the browser inside tmux and `T` lists the sessions on this machine, each
105
+ with the episode it works in (the vessel its pane is in, or the number its
106
+ name carries), the sessions of the episode in focus first. Enter on one:
107
+
108
+ - **inside tmux**, tmux splits the browser's window and shows that session in
109
+ the split, live, as a client of its own (`env -u TMUX tmux attach`). You type
110
+ in it directly. The prefix once moves between the panes; the session's own
111
+ tmux keys take the prefix twice. The browser jumps to the session's episode
112
+ and keeps it current: its files are read again every 5 s, and a file changed
113
+ in the last 15 minutes is marked ●, so you see what the agent is touching.
114
+ The header shows `⇆ <session> · <command> · <idle>`. `F` focuses the split,
115
+ `X` closes it, and the session goes on, as it does when you detach inside it.
116
+ - **outside tmux**, the browser steps aside and runs `tmux attach` in this
117
+ terminal; `prefix d` brings you back to the same screen.
118
+
119
+ While you type in it, the split is that session's most recent client, so its
120
+ window takes the split's size, as with any tmux client. The browser never types
121
+ into a session; only your keys reach it, and tmux never inherits a token.
122
+
100
123
  ## Inquiries and the companion seat
101
124
 
102
125
  Both show what would happen before anything does, and act only on `y`:
package/dist/App.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAiD,MAAM,OAAO,CAAA;AAIrE,OAAO,EAML,KAAK,eAAe,EAIrB,MAAM,cAAc,CAAA;AAqErB,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACpC,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAuHD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBAirDlC"}
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAiD,MAAM,OAAO,CAAA;AAIrE,OAAO,EAML,KAAK,eAAe,EAIrB,MAAM,cAAc,CAAA;AAmFrB,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACpC,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAyHD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBA8yDlC"}
package/dist/App.js CHANGED
@@ -21,6 +21,7 @@ import { BOARD_LABELS, ROOM_LABELS, answeredMessage, checkAnswer, filterBoard, l
21
21
  import { CEREMONY_TYPES, DIARY_PHASES, DIRECTIONS, ENTRY_TYPES, PHASE_LABELS, ceremonyHeadline, ceremonyLines, circleEntries, closeCeremony, mayWitness, openCeremony, readCeremony, readCircle, scrollTo, speak, splitLearnings, typeLabel, whenOf, witness, writeDiary, } from "./circles.js";
22
22
  import { THREAD, attachInquiry, attachedMessage, previewLines, readInquiries } from "./inquiries.js";
23
23
  import { companionLines, companionPlan, planActs, ranMessage, runCompanion } from "./tide.js";
24
+ import { attachHere, closePane, focusPane, idleFor, insideTmux, listSessions, openBeside, orderSessions, paneAlive, tmuxEnv, } from "./sessions.js";
24
25
  import { VISITOR, wheelDoors, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
25
26
  import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
26
27
  import { clean, cleanLine, link } from "./text.js";
@@ -88,7 +89,7 @@ const PLAIN_KEY = {
88
89
  };
89
90
  export function App(props) {
90
91
  const { root, actions: extra = [], url, withheld = [] } = props;
91
- const { exit } = useApp();
92
+ const { exit, suspendTerminal } = useApp();
92
93
  const { columns, rows } = useWindowSize();
93
94
  const [, render] = useReducer((n) => n + 1, 0);
94
95
  const ui = useRef({
@@ -670,6 +671,70 @@ export function App(props) {
670
671
  set({ status: cleanLine(`The seat did not open: ${error.message}`) });
671
672
  });
672
673
  };
674
+ // ── a tmux session beside the chronicle ───────────────────────────────────
675
+ const sessionRowsOf = (o) => {
676
+ const d = derive();
677
+ return o.sessions && d.s.chronicle ? orderSessions(o.sessions, d.visible, d.s.chronicle.root, o.episode, o.own) : [];
678
+ };
679
+ const closeBeside = async (message) => {
680
+ const beside = ui.current.beside;
681
+ if (!beside)
682
+ return;
683
+ set({ beside: undefined, status: message ?? `Closed the split; ${beside.session} goes on.` });
684
+ await closePane(beside.paneId, tmuxEnv(process.env, withheld));
685
+ };
686
+ const openSession = async (row) => {
687
+ const name = row.session.name;
688
+ if (row.own)
689
+ return set({ status: `${name} is this browser's own session.` });
690
+ if (!insideTmux()) {
691
+ // Outside tmux the terminal itself is handed over, and comes back on detach.
692
+ let outcome = { ok: false };
693
+ await suspendTerminal(() => {
694
+ outcome = attachHere(name, tmuxEnv(process.env, withheld));
695
+ });
696
+ return set({ status: outcome.ok ? `Back from ${name}.` : cleanLine(outcome.message ?? `Could not attach ${name}.`) });
697
+ }
698
+ if (ui.current.beside)
699
+ await closePane(ui.current.beside.paneId, tmuxEnv(process.env, withheld));
700
+ try {
701
+ const paneId = await openBeside(name, { env: tmuxEnv(process.env, withheld) });
702
+ set({ beside: { session: name, paneId, episode: row.episode, info: row.session }, overlays: [] });
703
+ // The chronicle side shows the episode the session works in.
704
+ if (row.episode && derive().rowByName(row.episode))
705
+ jumpTo(row.episode);
706
+ set({ status: `${name} is beside you · F focuses it, prefix+arrow comes back · X closes the split, the session goes on.` });
707
+ }
708
+ catch (error) {
709
+ set({ beside: undefined, status: cleanLine(`Could not open ${name}: ${error.message}`) });
710
+ }
711
+ };
712
+ // While a session is beside the chronicle: is the split still there, what does the session run, what changed in its episode.
713
+ useEffect(() => {
714
+ const timer = setInterval(async () => {
715
+ const beside = ui.current.beside;
716
+ if (!beside)
717
+ return;
718
+ const env = tmuxEnv(process.env, withheld);
719
+ if (!(await paneAlive(beside.paneId, env))) {
720
+ if (ui.current.beside?.paneId === beside.paneId)
721
+ set({ beside: undefined, status: `The split closed; ${beside.session} goes on.` });
722
+ return;
723
+ }
724
+ const info = (await listSessions(env)).find((session) => session.name === beside.session);
725
+ if (ui.current.beside?.paneId !== beside.paneId)
726
+ return;
727
+ if (beside.episode)
728
+ details.current.delete(`${ui.current.access.mayWrite}:${beside.episode}`);
729
+ set({ beside: { ...beside, info: info ?? beside.info }, status: ui.current.status });
730
+ }, 5_000);
731
+ return () => {
732
+ clearInterval(timer);
733
+ const beside = ui.current.beside;
734
+ if (beside)
735
+ void closePane(beside.paneId, tmuxEnv(process.env, withheld));
736
+ };
737
+ }, []);
673
738
  // ── keys ─────────────────────────────────────────────────────────────────
674
739
  // A paste into an input arrives whole, line breaks kept (bracketed paste).
675
740
  const pasteActive = ui.current.overlays[ui.current.overlays.length - 1]?.overlay.type === "input";
@@ -797,6 +862,17 @@ export function App(props) {
797
862
  }, (error) => updateById(handle, (o) => (o.type === "confirm" ? { ...o, busy: false, error: cleanLine(error.message) } : o)));
798
863
  return;
799
864
  }
865
+ if (top.type === "sessions") {
866
+ const rows = sessionRowsOf(top);
867
+ if (key.downArrow || input === "j")
868
+ return updateTop({ ...top, selected: clamp(top.selected + 1, rows.length) });
869
+ if (key.upArrow || input === "k")
870
+ return updateTop({ ...top, selected: clamp(top.selected - 1, rows.length) });
871
+ const row = rows[clamp(top.selected, rows.length)];
872
+ if (row && (key.return || input === "l" || key.rightArrow))
873
+ return void openSession(row);
874
+ return;
875
+ }
800
876
  if (top.type === "inquiries") {
801
877
  const handle = topHandle();
802
878
  const items = top.view?.shelf?.items ?? [];
@@ -1018,6 +1094,16 @@ export function App(props) {
1018
1094
  return forward();
1019
1095
  if (key.escape && s.relateFrom)
1020
1096
  return set({ relateFrom: undefined, status: "No longer relating." });
1097
+ if (s.beside && input === "F") {
1098
+ focusPane(s.beside.paneId, tmuxEnv(process.env, withheld)).catch((error) => set({ status: cleanLine(error.message) }));
1099
+ return;
1100
+ }
1101
+ if (s.beside && input === "X")
1102
+ return void closeBeside();
1103
+ if (input === "q" && s.beside) {
1104
+ void closeBeside();
1105
+ return exit();
1106
+ }
1021
1107
  if (key.escape || key.leftArrow || input === "h" || key.backspace)
1022
1108
  return back();
1023
1109
  if (input === "?")
@@ -1088,7 +1174,7 @@ export function App(props) {
1088
1174
  const fileWindow = windowOf(d.files, clamp(s.fileSel, d.files.length), Math.max(1, bodyHeight - fixedLines));
1089
1175
  const detailPane = (_jsx(Box, { flexDirection: "column", width: rightWidth, height: bodyHeight, overflow: "hidden", paddingLeft: compact ? 0 : 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: s.view === "attention" ? (d.boardItem ? itemBody(d.boardItem, rightWidth - 1) : _jsx(Text, { color: "gray", children: "Nothing selected." })) : d.currentRow ? (_jsxs(_Fragment, { children: [_jsx(Box, { height: titleLines, overflow: "hidden", flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: d.currentRow.number ?? "·" }), " ", d.currentRow.title] }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [[formatDate(d.currentRow.date), d.currentRow.status, d.currentRow.type].filter(Boolean).join(" · "), d.currentRow.structured ? "" : " · no episode.yaml", d.currentRow.audience === "community" ? " · community" : ""] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["reading: ", d.currentRow.readings.direction, " \u00B7 ", d.currentRow.readings.theme, " \u00B7 ", d.currentRow.readings.arc] }), openHere ? _jsxs(Text, { color: "yellow", children: ["? ", openHere, " open question", openHere === 1 ? "" : "s", " \u00B7 a"] }) : null, lineage ? _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: ["\u2197 ", lineage] }) : null, goal ? (_jsx(Box, { height: goalLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: goal }) })) : null, d.detail?.error ? _jsx(Text, { color: "red", children: cleanLine(d.detail.error) }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["── ", d.files.length, d.files.length >= MAX_FILES ? "+" : "", " file", d.files.length === 1 ? "" : "s", s.view === "files" ? "" : " · Enter", " ──"] }), fileWindow.items.map((file, index) => {
1090
1176
  const isSelected = s.view === "files" && fileWindow.start + index === clamp(s.fileSel, d.files.length);
1091
- return (_jsxs(Text, { wrap: "truncate-start", inverse: isSelected, color: file.kind === "text" ? "blue" : "gray", children: [cleanLine(file.relativePath), file.kind === "text" ? "" : ` · ${file.kind}`] }, file.relativePath));
1177
+ return (_jsxs(Text, { wrap: "truncate-start", inverse: isSelected, color: file.kind === "text" ? "blue" : "gray", children: [Date.now() - Date.parse(file.modifiedAt) < 15 * 60_000 ? _jsx(Text, { color: "green", children: "\u25CF " }) : null, cleanLine(file.relativePath), file.kind === "text" ? "" : ` · ${file.kind}`] }, file.relativePath));
1092
1178
  })] })) : (_jsx(Text, { color: "gray", children: "Nothing selected." })) }) }));
1093
1179
  const fileBody = s.open ? (_jsxs(Box, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [s.open.empty ? _jsx(Text, { color: "gray", children: "(empty file)" }) : null, s.open.view.lines.slice(s.open.scroll, s.open.scroll + bodyHeight).map((line, index) => (_jsx(Segments, { segments: line.segments, selectedRef: s.open.refSel }, s.open.scroll + index)))] })) : null;
1094
1180
  /** How many screen rows these lines take inside an overlay. */
@@ -1149,6 +1235,17 @@ export function App(props) {
1149
1235
  const room = Math.max(1, bodyHeight - 2 - (2 + (top.busy ? 1 : 0) + (top.error ? 1 : 0)));
1150
1236
  return box("red", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: cleanLine(top.title) }), _jsx(Box, { flexDirection: "column", height: Math.min(room, rowsOf(top.lines.map((line) => cleanLine(line.text)))), overflow: "hidden", flexShrink: 0, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: top.lines.map((line, index) => (_jsx(Text, { wrap: "wrap", color: line.tone === "warn" ? "yellow" : line.tone === "meta" ? "gray" : undefined, children: cleanLine(line.text) }, index))) }) }), top.busy ? _jsx(Text, { color: "gray", children: "Working\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, _jsxs(Text, { wrap: "truncate-end", children: [top.yes && !top.busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " ", top.yes, " \u00B7", " "] })) : null, _jsx(Text, { color: "gray", children: top.busy ? "" : "Esc cancels" })] })] }));
1151
1237
  }
1238
+ if (top.type === "sessions") {
1239
+ const rows = sessionRowsOf(top);
1240
+ const listWindow = windowOf(rows, clamp(top.selected, rows.length), Math.max(1, bodyHeight - 5));
1241
+ const focusLabel = top.episode ? episodeLabel(top.episode) : "";
1242
+ const now = Date.now() / 1000;
1243
+ return box("green", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Sessions", " ", _jsxs(Text, { color: "gray", children: ["\u00B7 ", insideTmux() ? "Enter opens one beside the chronicle" : "Enter hands this terminal to it; prefix d comes back"] })] }), !top.sessions && !top.error ? _jsx(Text, { color: "gray", children: "Asking tmux\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, rows.length && focusLabel ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [rows.some((row) => row.here) ? `${focusLabel} first` : `none works in ${focusLabel}`, " \u00B7 \u25CF has a client \u00B7 \u25CB has none"] })) : null, listWindow.items.map((row, index) => {
1244
+ const isSelected = listWindow.start + index === clamp(top.selected, rows.length);
1245
+ const where = row.episode ? episodeLabel(row.episode) : "";
1246
+ return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [_jsx(Text, { color: row.session.attached ? "green" : "gray", children: row.session.attached ? "●" : "○" }), " ", _jsx(Text, { bold: row.here, children: cleanLine(row.session.name) }), _jsx(Text, { color: "gray", children: [where, cleanLine(row.session.command), idleFor(row.session.activity, now), row.own ? "this browser" : ""].filter(Boolean).map((part) => ` · ${part}`).join("") })] }, row.session.name));
1247
+ }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "\u2191\u2193 \u00B7 \u23CE open \u00B7 Esc" })] }));
1248
+ }
1152
1249
  if (top.type === "inquiries") {
1153
1250
  const view = top.view;
1154
1251
  const items = view?.shelf?.items ?? [];
@@ -1248,6 +1345,10 @@ export function App(props) {
1248
1345
  const view = s.view;
1249
1346
  if (s.overlays.length)
1250
1347
  return "Esc back";
1348
+ if (s.beside && !compact)
1349
+ return "F focus beside X close split ? keys q quit";
1350
+ if (s.beside)
1351
+ return "F X ? q";
1251
1352
  if (view === "file")
1252
1353
  return compact ? "Tab ref · Esc · q" : "Tab next reference ⇧Tab previous Esc back ? keys q quit";
1253
1354
  if (view === "attention")
@@ -1281,6 +1382,6 @@ export function App(props) {
1281
1382
  ? "…"
1282
1383
  : "read-only"
1283
1384
  : "";
1284
- return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: "miadi-chronicle" }), " ", _jsx(Text, { color: "gray", children: where }), readerLabel ? _jsx(Text, { color: s.reader ? "green" : "gray", children: ` · ${readerLabel}` }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: s.inputMode ? "yellow" : "gray", children: "/ " }), cleanLine(s.query), s.inputMode ? _jsx(Text, { inverse: true, children: " " }) : null, " ", s.view === "attention" ? (_jsx(Text, { color: "gray", children: "attention" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: s.searchMode === "title" ? "yellow" : "gray", children: "title" }), _jsx(Text, { color: "gray", children: compact ? "|" : " | " }), _jsx(Text, { color: s.searchMode === "full" ? "yellow" : "gray", children: compact ? "full" : "full text" })] })), s.view === "list" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.filtered.length] }) : null, s.relateFrom ? _jsxs(Text, { color: "red", children: [" \u00B7 relating ", episodeLabel(s.relateFrom), " \u2192 L"] }) : null, s.view === "attention" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.board.length] }) : null] }), _jsx(Box, { flexDirection: "row", height: bodyHeight, overflow: "hidden", children: body }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: s.status }), _jsxs(Text, { wrap: "truncate-end", children: [(s.overlays.length ? [] : available).map((action) => (_jsxs(Text, { children: [_jsx(Text, { color: action.effect === "write" ? "red" : "yellow", children: action.key === "Enter" ? "⏎" : action.key }), " ", action.label, " "] }, action.id))), _jsx(Text, { color: "gray", children: hints })] })] }));
1385
+ return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: "miadi-chronicle" }), " ", _jsx(Text, { color: "gray", children: where }), s.beside ? (_jsxs(Text, { color: "cyan", children: [` · ⇆ ${cleanLine(s.beside.session)}`, s.beside.info ? ` · ${cleanLine(s.beside.info.command)} · ${idleFor(s.beside.info.activity)}` : ""] })) : null, readerLabel ? _jsx(Text, { color: s.reader ? "green" : "gray", children: ` · ${readerLabel}` }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: s.inputMode ? "yellow" : "gray", children: "/ " }), cleanLine(s.query), s.inputMode ? _jsx(Text, { inverse: true, children: " " }) : null, " ", s.view === "attention" ? (_jsx(Text, { color: "gray", children: "attention" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: s.searchMode === "title" ? "yellow" : "gray", children: "title" }), _jsx(Text, { color: "gray", children: compact ? "|" : " | " }), _jsx(Text, { color: s.searchMode === "full" ? "yellow" : "gray", children: compact ? "full" : "full text" })] })), s.view === "list" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.filtered.length] }) : null, s.relateFrom ? _jsxs(Text, { color: "red", children: [" \u00B7 relating ", episodeLabel(s.relateFrom), " \u2192 L"] }) : null, s.view === "attention" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.board.length] }) : null] }), _jsx(Box, { flexDirection: "row", height: bodyHeight, overflow: "hidden", children: body }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: s.status }), _jsxs(Text, { wrap: "truncate-end", children: [(s.overlays.length ? [] : available).map((action) => (_jsxs(Text, { children: [_jsx(Text, { color: action.effect === "write" ? "red" : "yellow", children: action.key === "Enter" ? "⏎" : action.key }), " ", action.label, " "] }, action.id))), _jsx(Text, { color: "gray", children: hints })] })] }));
1285
1386
  }
1286
1387
  //# sourceMappingURL=App.js.map