@miadi/chronicle-tui 0.5.0 → 0.7.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
@@ -34,6 +34,9 @@ On Termux, install with `--ignore-scripts`. Node 22 or later.
34
34
  | `!` | the chronicle's attention board: every open item, newest asked first, filtered by `/`. `e` on an item opens its episode |
35
35
  | `c` | talking circles: the ceremonies held in the episode in focus, the circles gathered for it, yours, the others |
36
36
  | `v` | hold a Miadi review in the episode (writer token) |
37
+ | `i` | the episode's inquiries and, when you may write, the shelf: Enter attaches one, a dry run first, `y` confirms |
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
+ | `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 |
37
40
  | `?` | all keys · `q` quits |
38
41
 
39
42
  Under 90 columns (a phone, a narrow split) one pane shows at a time: the list
@@ -84,10 +87,38 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
84
87
  `POST /api/chronicle/attention`, and the wheel is reported, never assumed.
85
88
  - **A review** (`v`) posts to `POST /api/chronicle/episodes/{ref}/reviews`;
86
89
  with your person token its talking circle opens in your name.
90
+ - **Lineage** (`L`) is the room's lineage form
91
+ (`POST /api/chronicle/episodes/{ref}/lineage`): the door writes
92
+ `episode.yaml`, puts the edge and its words on the wheel, and lands the
93
+ commit. Offered when the episode door says it would take the write
94
+ (`capabilities.mint`). The episode card shows what each episode continues
95
+ from and relates to.
87
96
 
88
97
  Write actions declare `effect: "write"` and are offered only when the app said
89
98
  it would record them.
90
99
 
100
+ ## Inquiries and the companion seat
101
+
102
+ Both show what would happen before anything does, and act only on `y`:
103
+ Enter never confirms, so a double press cannot start a seat or land a commit.
104
+
105
+ - **Inquiries** (`i`) are the room's Inquiries card
106
+ (`GET /api/chronicle/episodes/{ref}/inquiry`): what the episode holds, and
107
+ for a writer the shelf, newest first, `/` to filter (`?q=`). Enter on a
108
+ shelf item asks for its thread (`owner/repo#number`, optional), posts a dry
109
+ run and shows it in the card's words, with the copy's size and what the door
110
+ noticed. `y` attaches: the door relates, copies, registers on the wheel and
111
+ commits. A refused dry run offers nothing to confirm.
112
+ - **The companion seat** (`t`) is the cockpit's "Open Mia for this episode"
113
+ (`POST /api/tide/companion`, jgwill/Miadi#714). The plan comes first: the
114
+ session, tide's state, the listener, the commands and the starter prompt.
115
+ `y` runs that plan, only when it would act (`create`, a re-arm that starts a
116
+ listener or relaunches, `archive then create`); a seat already listening, a
117
+ refusal or a pane running something else offer nothing. Archiving the
118
+ running session is the person's word, so `A` asks for a new plan with it.
119
+ The run takes up to a few minutes and ends with `tmux attach -t <session>`.
120
+ The browser never types into the seat's pane; tide does, once it is idle.
121
+
91
122
  ## Talking circles
92
123
 
93
124
  `c` opens the circles, from the episode in focus: the ceremonies held in it
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;AAmErB,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;AAoHD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBAi6ClC"}
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"}
package/dist/App.js CHANGED
@@ -19,6 +19,8 @@ import { DEFAULT_ACTIONS, actionFor, enterCeremony, enterCircle, } from "./actio
19
19
  import { TOKEN_ENV, answerAttention, connectApi } from "./api.js";
20
20
  import { BOARD_LABELS, ROOM_LABELS, answeredMessage, checkAnswer, filterBoard, loadBoard, loadEpisodePanel, panelView, readOnlyLabel, } from "./attention.js";
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
+ import { THREAD, attachInquiry, attachedMessage, previewLines, readInquiries } from "./inquiries.js";
23
+ import { companionLines, companionPlan, planActs, ranMessage, runCompanion } from "./tide.js";
22
24
  import { VISITOR, wheelDoors, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
23
25
  import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
24
26
  import { clean, cleanLine, link } from "./text.js";
@@ -108,6 +110,7 @@ export function App(props) {
108
110
  reader: null,
109
111
  access: VISITOR,
110
112
  mayAnswer: false,
113
+ mayMint: false,
111
114
  board: null,
112
115
  boardSel: 0,
113
116
  history: [],
@@ -203,7 +206,7 @@ export function App(props) {
203
206
  return;
204
207
  try {
205
208
  const reader = await api.me();
206
- const mayAnswer = await api.mayAnswer();
209
+ const [mayAnswer, mayMint] = await Promise.all([api.mayAnswer(), api.mayMint()]);
207
210
  if (!live)
208
211
  return;
209
212
  // Signing in can show more episodes (community ones), so the selection is kept by name, not by row.
@@ -211,7 +214,7 @@ export function App(props) {
211
214
  const keep = before.s.view === "attention" ? undefined : before.currentRow?.name;
212
215
  const keepItem = before.boardItem;
213
216
  details.current.clear();
214
- set({ reader, mayAnswer, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
217
+ set({ reader, mayAnswer, mayMint, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
215
218
  const after = derive();
216
219
  const listSel = keep ? after.filtered.findIndex((row) => row.name === keep) : -1;
217
220
  const boardSel = keepItem ? after.board.findIndex((item) => item.episode === keepItem.episode && item.id === keepItem.id) : -1;
@@ -329,8 +332,22 @@ export function App(props) {
329
332
  api: s.api,
330
333
  reader: s.reader,
331
334
  mayAnswer: s.mayAnswer,
335
+ mayMint: s.mayMint,
336
+ relateFrom: s.relateFrom,
337
+ setRelateFrom: (name) => set({ relateFrom: name }),
338
+ reload: reloadChronicle,
332
339
  };
333
340
  };
341
+ /** Read the chronicle again after a write landed in it, keeping the episode in focus. */
342
+ async function reloadChronicle() {
343
+ const keep = derive().currentRow?.name;
344
+ const chronicle = await loadChronicle(root);
345
+ details.current.clear();
346
+ set({ chronicle, status: ui.current.status });
347
+ const index = keep ? derive().filtered.findIndex((row) => row.name === keep) : -1;
348
+ if (index !== -1)
349
+ set({ listSel: index, status: ui.current.status });
350
+ }
334
351
  const openFile = (name, file, from, line) => {
335
352
  const s = ui.current;
336
353
  const text = loadText(s.chronicle.root, name, file, s.access);
@@ -583,6 +600,76 @@ export function App(props) {
583
600
  await reloadCeremony(handle);
584
601
  }, (error) => set({ status: cleanLine(error.message) }));
585
602
  };
603
+ // ── inquiries and the companion seat: a plan or a dry run, then `y` ───────
604
+ const reloadInquiries = async (handle) => {
605
+ const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
606
+ if (!entry || entry.overlay.type !== "inquiries" || !ui.current.api)
607
+ return;
608
+ const view = await readInquiries(ui.current.api, entry.overlay.episode, entry.overlay.query);
609
+ updateById(handle, (o) => (o.type === "inquiries" ? { ...o, view, error: undefined, selected: clamp(o.selected, view.shelf?.items.length ?? 0) } : o));
610
+ };
611
+ const filterInput = (handle, episode, query) => ({
612
+ type: "input",
613
+ title: "Filter the shelf by name",
614
+ hint: "Enter filters; an empty filter shows the newest.",
615
+ value: query,
616
+ allowEmpty: true,
617
+ submit: async (value) => {
618
+ const q = value.trim();
619
+ const view = await readInquiries(ui.current.api, episode, q);
620
+ updateById(handle, (o) => (o.type === "inquiries" ? { ...o, view, query: q, selected: 0 } : o));
621
+ return `${view.shelf?.items.length ?? 0} of ${view.shelf?.total ?? 0} on the shelf${q ? ` for “${cleanLine(q)}”` : ""}, newest first.`;
622
+ },
623
+ });
624
+ const attachInput = (handle, episode, label, item) => ({
625
+ type: "input",
626
+ title: `Attach ${cleanLine(item.artefact)} to ${label}`,
627
+ hint: "Its thread (optional, owner/repo#number). Enter shows what attaching does; nothing is written yet.",
628
+ value: item.issue ?? "",
629
+ allowEmpty: true,
630
+ submit: async (value) => {
631
+ const issue = value.trim();
632
+ if (issue && !THREAD.test(issue))
633
+ throw new Error("A thread is owner/repo#number, or nothing.");
634
+ const api = ui.current.api;
635
+ const preview = await attachInquiry(api, episode, { artefact: item.artefact, issue, dryRun: true });
636
+ return {
637
+ next: {
638
+ type: "confirm",
639
+ title: `Attach ${cleanLine(item.artefact)} to ${label}?${issue ? ` · ${issue}` : ""}`,
640
+ lines: previewLines(preview, item.artefact),
641
+ ...(preview.ok === false ? {} : { yes: "attach" }),
642
+ run: async () => attachedMessage(await attachInquiry(api, episode, { artefact: item.artefact, issue, dryRun: false }), item.artefact),
643
+ after: () => reloadInquiries(handle),
644
+ },
645
+ };
646
+ },
647
+ });
648
+ const askCompanion = (handle, archive) => {
649
+ const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
650
+ if (!entry || entry.overlay.type !== "companion" || !ui.current.api)
651
+ return;
652
+ const episode = entry.overlay.episode;
653
+ updateById(handle, (o) => (o.type === "companion" ? { ...o, archive, busy: "plan", answer: undefined, error: undefined } : o));
654
+ companionPlan(ui.current.api, episode, { archive }).then((answer) => updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, answer } : o)), (error) => updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, error: cleanLine(error.message) } : o)));
655
+ };
656
+ const runPlan = (handle) => {
657
+ const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
658
+ if (!entry || entry.overlay.type !== "companion" || !ui.current.api)
659
+ return;
660
+ const { episode, archive, answer } = entry.overlay;
661
+ if (!planActs(answer))
662
+ return set({ status: `Nothing to run: ${cleanLine(answer?.plan?.state ?? "no plan on screen")}.` });
663
+ updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: "run", error: undefined } : o));
664
+ set({ status: "Opening the seat: up to a few minutes. Esc leaves this open; the outcome comes here." });
665
+ runCompanion(ui.current.api, episode, { archive }).then((ran) => {
666
+ updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, ran } : o));
667
+ set({ status: ranMessage(ran) });
668
+ }, (error) => {
669
+ updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, error: cleanLine(error.message) } : o));
670
+ set({ status: cleanLine(`The seat did not open: ${error.message}`) });
671
+ });
672
+ };
586
673
  // ── keys ─────────────────────────────────────────────────────────────────
587
674
  // A paste into an input arrives whole, line breaks kept (bracketed paste).
588
675
  const pasteActive = ui.current.overlays[ui.current.overlays.length - 1]?.overlay.type === "input";
@@ -645,7 +732,11 @@ export function App(props) {
645
732
  if (!value.trim() && !top.allowEmpty)
646
733
  return updateTop({ ...top, error: "Nothing to send yet." });
647
734
  updateTop({ ...top, busy: true, error: undefined });
648
- top.submit(value, choices.map((c) => c.values[c.index])).then(async (message) => {
735
+ top.submit(value, choices.map((c) => c.values[c.index])).then(async (result) => {
736
+ // A two-part form: the next input takes this one's place.
737
+ if (typeof result !== "string")
738
+ return updateTop(result.next);
739
+ const message = result;
649
740
  set({ status: cleanLine(message), overlays: ui.current.overlays.slice(0, -1) });
650
741
  try {
651
742
  await top.after?.();
@@ -666,6 +757,9 @@ export function App(props) {
666
757
  if (top) {
667
758
  if (top.type === "help")
668
759
  return closeTop();
760
+ // An act under way is not left half-seen.
761
+ if (top.type === "confirm" && top.busy)
762
+ return;
669
763
  if (key.escape || input === "q" || input === "h" || key.backspace || key.leftArrow)
670
764
  return closeTop();
671
765
  if (top.type === "doors") {
@@ -685,6 +779,51 @@ export function App(props) {
685
779
  }
686
780
  if (top.type === "qr" && input === "i")
687
781
  return updateTop({ ...top, invert: !top.invert });
782
+ if (top.type === "confirm") {
783
+ // Only an act the dry run allowed: a refusal offers nothing to confirm.
784
+ if (input !== "y" || !top.run || !top.yes)
785
+ return;
786
+ const handle = topHandle();
787
+ const run = top.run;
788
+ updateTop({ ...top, busy: true, error: undefined });
789
+ run().then(async (message) => {
790
+ set({ status: cleanLine(message), overlays: ui.current.overlays.filter((entry) => entry.id !== handle) });
791
+ try {
792
+ await top.after?.();
793
+ }
794
+ catch (error) {
795
+ set({ status: cleanLine(`Done, but the refresh failed: ${error.message}`) });
796
+ }
797
+ }, (error) => updateById(handle, (o) => (o.type === "confirm" ? { ...o, busy: false, error: cleanLine(error.message) } : o)));
798
+ return;
799
+ }
800
+ if (top.type === "inquiries") {
801
+ const handle = topHandle();
802
+ const items = top.view?.shelf?.items ?? [];
803
+ if (key.downArrow || input === "j")
804
+ return updateTop({ ...top, selected: clamp(top.selected + 1, items.length) });
805
+ if (key.upArrow || input === "k")
806
+ return updateTop({ ...top, selected: clamp(top.selected - 1, items.length) });
807
+ if (input === "/" && top.view?.attach)
808
+ return void openOverlay(filterInput(handle, top.episode, top.query));
809
+ const item = items[clamp(top.selected, items.length)];
810
+ if (item && (key.return || input === "l" || key.rightArrow)) {
811
+ if (item.wovenHere)
812
+ return set({ status: `${cleanLine(item.artefact)} is already woven into this episode.` });
813
+ return void openOverlay(attachInput(handle, top.episode, top.label, item));
814
+ }
815
+ return;
816
+ }
817
+ if (top.type === "companion") {
818
+ const handle = topHandle();
819
+ if (top.busy || top.ran)
820
+ return;
821
+ if (input === "A")
822
+ return askCompanion(handle, !top.archive);
823
+ if (input === "y")
824
+ return runPlan(handle);
825
+ return;
826
+ }
688
827
  if (top.type === "circles" && top.index) {
689
828
  const entries = circleEntries(top.index);
690
829
  const step = (from, by) => {
@@ -877,6 +1016,8 @@ export function App(props) {
877
1016
  return move(1_000_000, PLAIN_KEY);
878
1017
  if (key.return || key.rightArrow || input === "l")
879
1018
  return forward();
1019
+ if (key.escape && s.relateFrom)
1020
+ return set({ relateFrom: undefined, status: "No longer relating." });
880
1021
  if (key.escape || key.leftArrow || input === "h" || key.backspace)
881
1022
  return back();
882
1023
  if (input === "?")
@@ -933,13 +1074,25 @@ export function App(props) {
933
1074
  const linesOf = (text, cap) => Math.min(cap, Math.max(1, wrapSegments([{ text, style: "text" }], textWidth).length));
934
1075
  const titleLines = d.currentRow ? linesOf(`${d.currentRow.number ?? "·"} ${d.currentRow.title}`, 3) : 0;
935
1076
  const goalLines = goal ? linesOf(goal, compact ? 6 : 8) : 0;
936
- const fixedLines = titleLines + 2 + (openHere ? 1 : 0) + goalLines + (d.detail?.error ? 1 : 0) + 1;
1077
+ // The episodes this one continues from or relates to, by number where it has one.
1078
+ const lineage = d.currentRow?.lineage.length
1079
+ ? ["continues from", "relates to", "branch of"]
1080
+ .map((kind) => {
1081
+ const targets = d.currentRow.lineage.filter((link) => link.kind === kind).map((link) => (link.number !== undefined ? String(link.number) : link.target));
1082
+ return targets.length ? `${kind} ${targets.join(", ")}` : "";
1083
+ })
1084
+ .filter(Boolean)
1085
+ .join(" · ")
1086
+ : "";
1087
+ const fixedLines = titleLines + 2 + (openHere ? 1 : 0) + (lineage ? 1 : 0) + goalLines + (d.detail?.error ? 1 : 0) + 1;
937
1088
  const fileWindow = windowOf(d.files, clamp(s.fileSel, d.files.length), Math.max(1, bodyHeight - fixedLines));
938
- 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, 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) => {
1089
+ 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) => {
939
1090
  const isSelected = s.view === "files" && fileWindow.start + index === clamp(s.fileSel, d.files.length);
940
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));
941
1092
  })] })) : (_jsx(Text, { color: "gray", children: "Nothing selected." })) }) }));
942
1093
  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
+ /** How many screen rows these lines take inside an overlay. */
1095
+ const rowsOf = (texts) => Math.max(1, texts.reduce((sum, text) => sum + wrapSegments([{ text, style: "text" }], Math.max(10, columns - 4)).length, 0));
943
1096
  // Children never shrink: a fixed-height box clips what does not fit instead
944
1097
  // of squeezing rows to zero height, where Ink draws them over each other.
945
1098
  const box = (color, children) => (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingLeft: 1, paddingRight: 1, flexGrow: 1, height: bodyHeight, overflow: "hidden", children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: children }) }));
@@ -992,6 +1145,32 @@ export function App(props) {
992
1145
  if (top.type === "item") {
993
1146
  return box("yellow", _jsxs(_Fragment, { children: [itemBody(top.item, columns - 4), _jsxs(Text, { color: "gray", wrap: "wrap", children: [top.item.state === "open" ? (s.mayAnswer ? "Enter answers · " : `${readOnlyLabel(TOKEN_ENV)} · `) : "", "e opens its episode \u00B7 Esc"] })] }));
994
1147
  }
1148
+ if (top.type === "confirm") {
1149
+ const room = Math.max(1, bodyHeight - 2 - (2 + (top.busy ? 1 : 0) + (top.error ? 1 : 0)));
1150
+ 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
+ }
1152
+ if (top.type === "inquiries") {
1153
+ const view = top.view;
1154
+ const items = view?.shelf?.items ?? [];
1155
+ const wovenRows = view ? Math.max(1, view.woven.length) : 0;
1156
+ const room = Math.max(1, bodyHeight - 2 - (1 + Math.min(wovenRows, 6) + (view?.attach ? 1 : 0) + 1));
1157
+ const shelfWindow = windowOf(items, clamp(top.selected, items.length), room);
1158
+ return box("blue", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Inquiries \u00B7 ", top.label] }), !view && !top.error ? _jsx(Text, { color: "gray", children: "Asking the episode's inquiry door\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, view && view.woven.length === 0 ? _jsx(Text, { color: "gray", italic: true, children: "No inquiry is woven into this episode." }) : null, view?.woven.slice(0, 6).map((w) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "blue", children: "\u25C7" }), " ", w.artefact, _jsxs(Text, { color: "gray", children: [w.issue ? ` · ${w.issue}` : "", w.freshness ? ` · ${w.freshness}` : "", w.exists ? "" : " · missing from the shelf"] })] }, w.artefact))), view?.attach ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u2500\u2500 the shelf \u00B7 ", items.length, " of ", view.shelf?.total ?? 0, ", newest first", top.query ? ` · “${cleanLine(top.query)}”` : "", view.shelf?.unavailable ? " · unavailable on the app's host" : "", " \u2500\u2500"] })) : null, shelfWindow.items.map((item, index) => {
1159
+ const isSelected = shelfWindow.start + index === clamp(top.selected, items.length);
1160
+ return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, color: item.wovenHere ? "gray" : undefined, children: [cleanLine(item.artefact), _jsxs(Text, { color: "gray", children: [" · ", item.modified.slice(0, 10), item.issue ? ` · ${item.issue}` : "", item.episodes.length ? ` · in episode ${item.episodes.join(", ")}` : "", item.wovenHere ? " · already here" : ""] })] }, item.artefact));
1161
+ }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: view?.attach ? "↑↓ · ⏎ attach · / filter · Esc" : view ? "Attaching needs a writer the app accepts · Esc" : "Esc" })] }));
1162
+ }
1163
+ if (top.type === "companion") {
1164
+ const shown = top.ran ?? top.answer;
1165
+ const lines = shown ? companionLines(shown) : [];
1166
+ const acts = !top.ran && planActs(top.answer);
1167
+ const toneColor = { state: "yellow", meta: "gray", warn: "yellow", error: "red", step: "green" };
1168
+ const nothing = !acts && !top.busy && !top.ran && Boolean(shown?.plan?.state);
1169
+ // The plan is clipped above the keys, so `y` and Esc stay on screen however long the starter prompt is.
1170
+ const room = Math.max(1, bodyHeight - 2 - (2 + (top.busy ? 1 : 0) + (top.error ? 1 : 0) + (nothing ? 1 : 0)));
1171
+ const planRows = Math.min(room, rowsOf(lines.map((line) => line.text)));
1172
+ return box("red", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Companion \u00B7 ", top.label, " \u00B7 mia ", _jsxs(Text, { color: "gray", children: ["\u00B7 archive the running session: ", top.archive ? "yes" : "no"] })] }), top.busy === "plan" ? _jsx(Text, { color: "gray", children: "Asking tide for the plan\u2026" }) : null, top.busy === "run" ? _jsx(Text, { color: "yellow", children: "Opening the seat\u2026 (up to a few minutes)" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, _jsx(Box, { flexDirection: "column", height: planRows, overflow: "hidden", flexShrink: 0, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: lines.map((line, index) => (_jsx(Text, { wrap: "wrap", color: line.tone ? toneColor[line.tone] : undefined, children: line.text }, index))) }) }), _jsxs(Text, { wrap: "truncate-end", children: [acts && !top.busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " runs this plan \u00B7", " "] })) : null, !top.ran && !top.busy ? _jsxs(Text, { color: "gray", children: ["A ", top.archive ? "keeps" : "archives", " the running session and asks again \u00B7 "] }) : null, _jsx(Text, { color: "gray", children: "Esc" })] }), nothing ? _jsx(Text, { color: "gray", children: "Nothing to run for this plan." }) : null] }));
1173
+ }
995
1174
  if (top.type === "circles") {
996
1175
  const entries = top.index ? circleEntries(top.index) : [];
997
1176
  const listWindow = windowOf(entries, clamp(top.selected, entries.length), Math.max(1, bodyHeight - 4));
@@ -1102,6 +1281,6 @@ export function App(props) {
1102
1281
  ? "…"
1103
1282
  : "read-only"
1104
1283
  : "";
1105
- 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.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 })] })] }));
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 })] })] }));
1106
1285
  }
1107
1286
  //# sourceMappingURL=App.js.map