@miadi/chronicle-tui 0.9.1 → 0.10.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,8 +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); detach it (the session goes on). Inside the split, `prefix prefix d` detaches too; its border says so |
39
+ | `T` | this machine's tmux sessions, the episode in focus's first. Inside tmux: Enter shows one on the right, `+` adds one below it (two at most), `e` goes to its episode; the list stays open. Outside tmux, Enter hands the terminal over until you detach |
40
+ | `F`, `X` | with sessions on the right: focus them in turn (Alt+s in one comes back); detach them all (they go on). Inside a split, `prefix prefix d` detaches that one; its border says so |
41
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 |
42
42
  | `?` | all keys · `q` quits from the main view (in a window it closes the window) · `Q` quits from any window, except while typing the filter · Ctrl+C quits too |
43
43
 
@@ -99,30 +99,40 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
99
99
  Write actions declare `effect: "write"` and are offered only when the app said
100
100
  it would record them.
101
101
 
102
- ## A session beside the chronicle
102
+ ## Sessions on the right
103
103
 
104
104
  Run the browser inside tmux and `T` lists the sessions on this machine, each
105
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>`, and the split's border
115
- says which session it is and how to come back. `F` focuses the split. To
116
- detach it, press `X` in the chronicle, or `prefix prefix d` inside the split
117
- (the prefix once goes to your tmux, twice to the session's client). Either
118
- way the session goes on, the border goes back as it was, and the chronicle
119
- returns to where you were before the session took it to its episode.
120
- - **outside tmux**, the browser steps aside and runs `tmux attach` in this
121
- terminal; `prefix d` brings you back to the same screen.
122
-
123
- While you type in it, the split is that session's most recent client, so its
124
- window takes the split's size, as with any tmux client. The browser never types
125
- into a session; only your keys reach it, and tmux never inherits a token.
106
+ name carries), the sessions of the episode in focus first. The list stays open
107
+ while you work with it:
108
+
109
+ - **Enter** shows the session on the right of the browser, live, as a client of
110
+ its own (`env -u TMUX tmux attach`). Whatever was on the right is detached
111
+ first, so Enter on the next session swaps it in.
112
+ - **`+`** adds the session below the one on the right, up to two (`MAX_BESIDE`).
113
+ - **`e`** closes the list and goes to the session's episode.
114
+ - **`F`** focuses the sessions on the right in turn; you type in them directly.
115
+ **Alt+s** in a split brings the focus back to the browser, where the list
116
+ is. The key is bound in tmux only while a split is open, acts only in a
117
+ split's pane (anywhere else it passes the key on), is left alone when you
118
+ bound it to something already, and is removed with the last split. Set
119
+ `MIADI_CHRONICLE_RETURN_KEY` to another tmux key, or `none`. `prefix ;`
120
+ (tmux's last pane) comes back too.
121
+ - **`X`** detaches every session on the right. `prefix prefix d` inside a split
122
+ detaches that one (the prefix once goes to your tmux, twice to the session's
123
+ client). Either way the sessions go on, and with the last split the window's
124
+ border goes back as it was.
125
+
126
+ Each split's border names its session and says how to come back. The header
127
+ shows `⇆ <session> · <command> · <idle>` for each. The episodes of the
128
+ sessions on the right are read every 5 s, and a file changed in the last 15
129
+ minutes is marked ●, so you see what an agent is touching.
130
+
131
+ While you type in a split it is that session's most recent client, so the
132
+ session's window takes the split's size, as with any tmux client, until its
133
+ own client is used again. The browser never types into a session; only your
134
+ keys reach it, and tmux never inherits a token. Outside tmux, Enter runs
135
+ `tmux attach` in this terminal and `prefix d` brings you back.
126
136
 
127
137
  ## Inquiries and the companion seat
128
138
 
package/dist/App.d.ts CHANGED
@@ -29,5 +29,7 @@ export interface AppProps {
29
29
  /** Environment keys `--env` loaded: an opener never inherits them. */
30
30
  withheld?: readonly string[];
31
31
  }
32
+ /** How many sessions the right side holds, one above the other. */
33
+ export declare const MAX_BESIDE = 2;
32
34
  export declare function App(props: AppProps): React.JSX.Element;
33
35
  //# sourceMappingURL=App.d.ts.map
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;AAuFrB,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;AAkID,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBA60DlC"}
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;AA2FrB,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;AAUD,mEAAmE;AACnE,eAAO,MAAM,UAAU,IAAI,CAAA;AAkI3B,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBAm4DlC"}
package/dist/App.js CHANGED
@@ -21,10 +21,12 @@ 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 { ISSUE_REF, weaveInquiry, wovenMessage, 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, markSplit, unmarkSplit, DETACH_HINT, orderSessions, paneAlive, tmuxEnv, } from "./sessions.js";
24
+ import { attachHere, closePane, focusPane, idleFor, insideTmux, listSessions, openBeside, markWindow, labelSplit, unmarkSplit, returnKey, keyLabel, bindReturnKey, unbindReturnKey, orderSessions, paneAlive, tmuxEnv, } from "./sessions.js";
25
25
  import { VISITOR, wheelDoors, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
26
26
  import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
27
27
  import { clean, cleanLine, link } from "./text.js";
28
+ /** How many sessions the right side holds, one above the other. */
29
+ export const MAX_BESIDE = 2;
28
30
  const DIRECTION_MARK = { east: "E", south: "S", west: "W", north: "N" };
29
31
  const DIRECTION_COLOR = { east: "yellow", south: "red", west: "blue", north: "white" };
30
32
  function Segments({ segments, selectedRef }) {
@@ -676,32 +678,40 @@ export function App(props) {
676
678
  const d = derive();
677
679
  return o.sessions && d.s.chronicle ? orderSessions(o.sessions, d.visible, d.s.chronicle.root, o.episode, o.own) : [];
678
680
  };
679
- /**
680
- * The split is gone, by `X` or by a detach inside it: the window's border goes
681
- * back as it was, and the chronicle returns to where it was before the session
682
- * took it to its episode, unless the person moved on since.
683
- */
681
+ const env = () => tmuxEnv(process.env, withheld);
682
+ /** Every split is gone: the window's border goes back as it was, and the return key is unbound unless another split uses it. */
684
683
  const endBeside = async (message) => {
685
684
  const beside = ui.current.beside;
686
685
  if (!beside)
687
686
  return;
688
- const d = derive();
689
- const stillThere = beside.jumped && d.s.view === "files" && d.currentRow?.name === beside.episode && d.s.history.length > 0;
690
- set({ beside: undefined });
691
- if (stillThere)
692
- back();
693
- set({ status: message });
687
+ set({ beside: undefined, status: message });
694
688
  if (beside.mark)
695
- await unmarkSplit(beside.mark, tmuxEnv(process.env, withheld));
689
+ await unmarkSplit(beside.mark, env());
690
+ if (beside.returnKey)
691
+ await unbindReturnKey(beside.returnKey, env());
696
692
  };
693
+ /** `X`: detach every session on the right; they go on. */
697
694
  const closeBeside = async () => {
698
695
  const beside = ui.current.beside;
699
696
  if (!beside)
700
697
  return;
701
- await closePane(beside.paneId, tmuxEnv(process.env, withheld));
702
- await endBeside(`Detached ${beside.session}; it goes on.`);
698
+ for (const slot of beside.slots)
699
+ await closePane(slot.paneId, env());
700
+ const names = beside.slots.map((slot) => slot.session).join(" and ");
701
+ await endBeside(`Detached ${names}; ${beside.slots.length > 1 ? "they go" : "it goes"} on.`);
703
702
  };
704
- const openSession = async (row) => {
703
+ /** The sessions list shows what is on the right: read tmux again after a change. */
704
+ const refreshSessionsList = async () => {
705
+ const sessions = await listSessions(env());
706
+ const stack = ui.current.overlays.map((entry) => (entry.overlay.type === "sessions" ? { id: entry.id, overlay: { ...entry.overlay, sessions } } : entry));
707
+ set({ overlays: stack, status: ui.current.status });
708
+ };
709
+ /**
710
+ * Show a session on the right. `below` adds it under the ones there (up to
711
+ * MAX_BESIDE); otherwise it takes the right side alone, detaching what was
712
+ * there. The sessions list stays open, so the next one is a key away.
713
+ */
714
+ const openSession = async (row, below = false) => {
705
715
  const name = row.session.name;
706
716
  if (row.own)
707
717
  return set({ status: `${name} is this browser's own session.` });
@@ -709,58 +719,92 @@ export function App(props) {
709
719
  // Outside tmux the terminal itself is handed over, and comes back on detach.
710
720
  let outcome = { ok: false };
711
721
  await suspendTerminal(() => {
712
- outcome = attachHere(name, tmuxEnv(process.env, withheld));
722
+ outcome = attachHere(name, env());
713
723
  });
714
724
  return set({ status: outcome.ok ? `Back from ${name}.` : cleanLine(outcome.message ?? `Could not attach ${name}.`) });
715
725
  }
716
- if (ui.current.beside)
717
- await closeBeside();
726
+ const current = ui.current.beside;
727
+ if (current?.slots.some((slot) => slot.session === name))
728
+ return set({ status: `${name} is already on the right.` });
729
+ if (below && current && current.slots.length >= MAX_BESIDE) {
730
+ return set({ status: `${MAX_BESIDE} sessions are on the right already: Enter shows ${name} alone, X detaches them.` });
731
+ }
732
+ const adding = below && current && current.slots.length > 0;
718
733
  try {
719
- const env = tmuxEnv(process.env, withheld);
720
- const paneId = await openBeside(name, { env });
721
- // The border says which session it is and how to come back; a window that refuses it still works.
722
- const mark = await markSplit(paneId, name, env).catch(() => undefined);
723
- // The chronicle side shows the episode the session works in.
724
- const jumped = Boolean(row.episode && derive().rowByName(row.episode));
725
- set({ beside: { session: name, paneId, episode: row.episode, info: row.session, mark, jumped }, overlays: [] });
726
- if (jumped)
727
- jumpTo(row.episode);
728
- set({ status: `${name} is beside you · F focuses it, prefix+arrow comes back · X detaches it here, ${DETACH_HINT.replace(": ", " in it: ")}.` });
734
+ let slots = current?.slots ?? [];
735
+ if (!adding) {
736
+ for (const slot of slots)
737
+ await closePane(slot.paneId, env());
738
+ slots = [];
739
+ }
740
+ const paneId = adding ? await openBeside(name, { below: true, pane: slots[slots.length - 1].paneId, env: env() }) : await openBeside(name, { env: env() });
741
+ // The border names the session and says how to come back; a window that refuses it still works.
742
+ const mark = current?.mark ?? (await markWindow(paneId, env()).catch(() => undefined));
743
+ const wanted = returnKey();
744
+ const key = current?.returnKey ?? (wanted && (await bindReturnKey(wanted, env()).catch(() => false)) ? wanted : undefined);
745
+ await labelSplit(paneId, name, { returnTo: process.env.TMUX_PANE, returnKey: key, env: env() }).catch(() => { });
746
+ const slot = { session: name, paneId, episode: row.episode, info: row.session };
747
+ set({ beside: { slots: [...slots, slot], mark, returnKey: key, focus: 0 } });
748
+ const back = key ? `${keyLabel(key)} in it comes back here` : "prefix ; comes back here";
749
+ set({ status: `${name} is on the right${adding ? ", below" : ""} · F focuses it, ${back} · X detaches ${adding ? "both" : "it"}.` });
750
+ await refreshSessionsList();
729
751
  }
730
752
  catch (error) {
731
- set({ beside: undefined, status: cleanLine(`Could not open ${name}: ${error.message}`) });
753
+ set({ status: cleanLine(`Could not open ${name}: ${error.message}`) });
732
754
  }
733
755
  };
734
- // While a session is beside the chronicle: every second, is the split still there (a detach inside it closes it);
735
- // every five, what the session runs and what changed in its episode.
756
+ /** `F`: focus the sessions on the right in turn. */
757
+ const focusBeside = () => {
758
+ const beside = ui.current.beside;
759
+ if (!beside?.slots.length)
760
+ return;
761
+ const slot = beside.slots[beside.focus % beside.slots.length];
762
+ set({ beside: { ...beside, focus: beside.focus + 1 } });
763
+ focusPane(slot.paneId, env()).catch((error) => set({ status: cleanLine(error.message) }));
764
+ };
765
+ // While sessions are on the right: every second, which splits are still there (a detach inside one closes it);
766
+ // every five, what each session runs and what changed in its episode.
736
767
  const besideTick = useRef(0);
737
768
  useEffect(() => {
738
769
  const timer = setInterval(async () => {
739
770
  const beside = ui.current.beside;
740
771
  if (!beside)
741
772
  return;
742
- const env = tmuxEnv(process.env, withheld);
743
- if (!(await paneAlive(beside.paneId, env))) {
744
- if (ui.current.beside?.paneId === beside.paneId)
745
- await endBeside(`Detached from ${beside.session}; back in the chronicle. It goes on.`);
773
+ const alive = await Promise.all(beside.slots.map((slot) => paneAlive(slot.paneId, env())));
774
+ if (ui.current.beside !== beside)
775
+ return;
776
+ const gone = beside.slots.filter((_, index) => !alive[index]);
777
+ if (gone.length) {
778
+ const left = beside.slots.filter((_, index) => alive[index]);
779
+ const names = gone.map((slot) => slot.session).join(" and ");
780
+ if (left.length === 0)
781
+ await endBeside(`Detached from ${names}; back in the chronicle. ${gone.length > 1 ? "They go" : "It goes"} on.`);
782
+ else
783
+ set({ beside: { ...beside, slots: left }, status: `Detached from ${names}; ${left.map((slot) => slot.session).join(" and ")} stays on the right.` });
784
+ void refreshSessionsList();
746
785
  return;
747
786
  }
748
787
  if (++besideTick.current % 5 !== 0)
749
788
  return;
750
- const info = (await listSessions(env)).find((session) => session.name === beside.session);
751
- if (ui.current.beside?.paneId !== beside.paneId)
789
+ const sessions = await listSessions(env());
790
+ if (ui.current.beside !== beside)
752
791
  return;
753
- if (beside.episode)
754
- details.current.delete(`${ui.current.access.mayWrite}:${beside.episode}`);
755
- set({ beside: { ...beside, info: info ?? beside.info }, status: ui.current.status });
792
+ for (const slot of beside.slots)
793
+ if (slot.episode)
794
+ details.current.delete(`${ui.current.access.mayWrite}:${slot.episode}`);
795
+ const slots = beside.slots.map((slot) => ({ ...slot, info: sessions.find((session) => session.name === slot.session) ?? slot.info }));
796
+ set({ beside: { ...beside, slots }, status: ui.current.status });
756
797
  }, 1_000);
757
798
  return () => {
758
799
  clearInterval(timer);
759
800
  const beside = ui.current.beside;
760
801
  if (beside) {
761
- void closePane(beside.paneId, tmuxEnv(process.env, withheld));
802
+ for (const slot of beside.slots)
803
+ void closePane(slot.paneId, env());
762
804
  if (beside.mark)
763
- void unmarkSplit(beside.mark, tmuxEnv(process.env, withheld));
805
+ void unmarkSplit(beside.mark, env());
806
+ if (beside.returnKey)
807
+ void unbindReturnKey(beside.returnKey, env());
764
808
  }
765
809
  };
766
810
  }, []);
@@ -901,8 +945,21 @@ export function App(props) {
901
945
  if (key.upArrow || input === "k")
902
946
  return updateTop({ ...top, selected: clamp(top.selected - 1, rows.length) });
903
947
  const row = rows[clamp(top.selected, rows.length)];
948
+ // Enter: this session alone on the right. +: below the one there. The list stays, for the next one.
904
949
  if (row && (key.return || input === "l" || key.rightArrow))
905
950
  return void openSession(row);
951
+ if (row && input === "+")
952
+ return void openSession(row, true);
953
+ if (input === "X")
954
+ return void closeBeside();
955
+ if (input === "F")
956
+ return focusBeside();
957
+ if (row && input === "e") {
958
+ if (!row.episode || !derive().rowByName(row.episode))
959
+ return set({ status: `${row.session.name} works in no episode you can see.` });
960
+ set({ overlays: [] });
961
+ return jumpTo(row.episode);
962
+ }
906
963
  return;
907
964
  }
908
965
  if (top.type === "inquiries") {
@@ -1126,10 +1183,8 @@ export function App(props) {
1126
1183
  return forward();
1127
1184
  if (key.escape && s.relateFrom)
1128
1185
  return set({ relateFrom: undefined, status: "No longer relating." });
1129
- if (s.beside && input === "F") {
1130
- focusPane(s.beside.paneId, tmuxEnv(process.env, withheld)).catch((error) => set({ status: cleanLine(error.message) }));
1131
- return;
1132
- }
1186
+ if (s.beside && input === "F")
1187
+ return focusBeside();
1133
1188
  if (s.beside && input === "X")
1134
1189
  return void closeBeside();
1135
1190
  if (key.escape || key.leftArrow || input === "h" || key.backspace)
@@ -1277,8 +1332,13 @@ export function App(props) {
1277
1332
  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) => {
1278
1333
  const isSelected = listWindow.start + index === clamp(top.selected, rows.length);
1279
1334
  const where = row.episode ? episodeLabel(row.episode) : "";
1280
- 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));
1281
- }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "\u2191\u2193 \u00B7 \u23CE open \u00B7 Esc" })] }));
1335
+ const slot = s.beside?.slots.findIndex((candidate) => candidate.session === row.session.name) ?? -1;
1336
+ return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [slot >= 0 ? _jsx(Text, { color: "cyan", children: slot === 0 ? "⇆ " : "⇣ " }) : null, _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));
1337
+ }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: insideTmux()
1338
+ ? compact
1339
+ ? `⏎ right · + below · e · ${s.beside ? "F X · " : ""}Esc`
1340
+ : `↑↓ · ⏎ on the right · + below it (up to ${MAX_BESIDE}) · e its episode${s.beside ? " · F focus · X detach" : ""} · Esc`
1341
+ : "↑↓ · ⏎ open · e its episode · Esc" })] }));
1282
1342
  }
1283
1343
  if (top.type === "inquiries") {
1284
1344
  const view = top.view;
@@ -1380,7 +1440,7 @@ export function App(props) {
1380
1440
  if (s.overlays.length)
1381
1441
  return compact ? "Esc · Q" : "Esc back Q quit";
1382
1442
  if (s.beside && !compact)
1383
- return "F focus beside X detach it ? keys q quit";
1443
+ return "T sessions F focus right X detach all ? keys q quit";
1384
1444
  if (s.beside)
1385
1445
  return "F X ? q";
1386
1446
  if (view === "file")
@@ -1416,6 +1476,8 @@ export function App(props) {
1416
1476
  ? "…"
1417
1477
  : "read-only"
1418
1478
  : "";
1419
- 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 })] })] }));
1479
+ 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: [` · ⇆ `, s.beside.slots
1480
+ .map((slot) => `${cleanLine(slot.session)}${slot.info ? ` · ${cleanLine(slot.info.command)} · ${idleFor(slot.info.activity)}` : ""}`)
1481
+ .join(" | ")] })) : 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 })] })] }));
1420
1482
  }
1421
1483
  //# sourceMappingURL=App.js.map