@miadi/chronicle-tui 0.6.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 +47 -0
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +252 -3
- package/dist/App.js.map +1 -1
- package/dist/actions.d.ts +54 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +76 -1
- package/dist/actions.js.map +1 -1
- package/dist/api.d.ts +10 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +19 -10
- package/dist/api.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/inquiries.d.ts +76 -0
- package/dist/inquiries.d.ts.map +1 -0
- package/dist/inquiries.js +77 -0
- package/dist/inquiries.js.map +1 -0
- package/dist/sessions.d.ts +76 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +148 -0
- package/dist/sessions.js.map +1 -0
- package/dist/tide.d.ts +79 -0
- package/dist/tide.d.ts.map +1 -0
- package/dist/tide.js +102 -0
- package/dist/tide.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,10 @@ 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
|
+
| `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) |
|
|
37
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 |
|
|
38
42
|
| `?` | all keys · `q` quits |
|
|
39
43
|
|
|
@@ -95,6 +99,49 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
|
|
|
95
99
|
Write actions declare `effect: "write"` and are offered only when the app said
|
|
96
100
|
it would record them.
|
|
97
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
|
+
|
|
123
|
+
## Inquiries and the companion seat
|
|
124
|
+
|
|
125
|
+
Both show what would happen before anything does, and act only on `y`:
|
|
126
|
+
Enter never confirms, so a double press cannot start a seat or land a commit.
|
|
127
|
+
|
|
128
|
+
- **Inquiries** (`i`) are the room's Inquiries card
|
|
129
|
+
(`GET /api/chronicle/episodes/{ref}/inquiry`): what the episode holds, and
|
|
130
|
+
for a writer the shelf, newest first, `/` to filter (`?q=`). Enter on a
|
|
131
|
+
shelf item asks for its thread (`owner/repo#number`, optional), posts a dry
|
|
132
|
+
run and shows it in the card's words, with the copy's size and what the door
|
|
133
|
+
noticed. `y` attaches: the door relates, copies, registers on the wheel and
|
|
134
|
+
commits. A refused dry run offers nothing to confirm.
|
|
135
|
+
- **The companion seat** (`t`) is the cockpit's "Open Mia for this episode"
|
|
136
|
+
(`POST /api/tide/companion`, jgwill/Miadi#714). The plan comes first: the
|
|
137
|
+
session, tide's state, the listener, the commands and the starter prompt.
|
|
138
|
+
`y` runs that plan, only when it would act (`create`, a re-arm that starts a
|
|
139
|
+
listener or relaunches, `archive then create`); a seat already listening, a
|
|
140
|
+
refusal or a pane running something else offer nothing. Archiving the
|
|
141
|
+
running session is the person's word, so `A` asks for a new plan with it.
|
|
142
|
+
The run takes up to a few minutes and ends with `tmux attach -t <session>`.
|
|
143
|
+
The browser never types into the seat's pane; tide does, once it is idle.
|
|
144
|
+
|
|
98
145
|
## Talking circles
|
|
99
146
|
|
|
100
147
|
`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;
|
|
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
|
@@ -19,6 +19,9 @@ 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";
|
|
24
|
+
import { attachHere, closePane, focusPane, idleFor, insideTmux, listSessions, openBeside, orderSessions, paneAlive, tmuxEnv, } from "./sessions.js";
|
|
22
25
|
import { VISITOR, wheelDoors, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
|
|
23
26
|
import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
|
|
24
27
|
import { clean, cleanLine, link } from "./text.js";
|
|
@@ -86,7 +89,7 @@ const PLAIN_KEY = {
|
|
|
86
89
|
};
|
|
87
90
|
export function App(props) {
|
|
88
91
|
const { root, actions: extra = [], url, withheld = [] } = props;
|
|
89
|
-
const { exit } = useApp();
|
|
92
|
+
const { exit, suspendTerminal } = useApp();
|
|
90
93
|
const { columns, rows } = useWindowSize();
|
|
91
94
|
const [, render] = useReducer((n) => n + 1, 0);
|
|
92
95
|
const ui = useRef({
|
|
@@ -598,6 +601,140 @@ export function App(props) {
|
|
|
598
601
|
await reloadCeremony(handle);
|
|
599
602
|
}, (error) => set({ status: cleanLine(error.message) }));
|
|
600
603
|
};
|
|
604
|
+
// ── inquiries and the companion seat: a plan or a dry run, then `y` ───────
|
|
605
|
+
const reloadInquiries = async (handle) => {
|
|
606
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
607
|
+
if (!entry || entry.overlay.type !== "inquiries" || !ui.current.api)
|
|
608
|
+
return;
|
|
609
|
+
const view = await readInquiries(ui.current.api, entry.overlay.episode, entry.overlay.query);
|
|
610
|
+
updateById(handle, (o) => (o.type === "inquiries" ? { ...o, view, error: undefined, selected: clamp(o.selected, view.shelf?.items.length ?? 0) } : o));
|
|
611
|
+
};
|
|
612
|
+
const filterInput = (handle, episode, query) => ({
|
|
613
|
+
type: "input",
|
|
614
|
+
title: "Filter the shelf by name",
|
|
615
|
+
hint: "Enter filters; an empty filter shows the newest.",
|
|
616
|
+
value: query,
|
|
617
|
+
allowEmpty: true,
|
|
618
|
+
submit: async (value) => {
|
|
619
|
+
const q = value.trim();
|
|
620
|
+
const view = await readInquiries(ui.current.api, episode, q);
|
|
621
|
+
updateById(handle, (o) => (o.type === "inquiries" ? { ...o, view, query: q, selected: 0 } : o));
|
|
622
|
+
return `${view.shelf?.items.length ?? 0} of ${view.shelf?.total ?? 0} on the shelf${q ? ` for “${cleanLine(q)}”` : ""}, newest first.`;
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const attachInput = (handle, episode, label, item) => ({
|
|
626
|
+
type: "input",
|
|
627
|
+
title: `Attach ${cleanLine(item.artefact)} to ${label}`,
|
|
628
|
+
hint: "Its thread (optional, owner/repo#number). Enter shows what attaching does; nothing is written yet.",
|
|
629
|
+
value: item.issue ?? "",
|
|
630
|
+
allowEmpty: true,
|
|
631
|
+
submit: async (value) => {
|
|
632
|
+
const issue = value.trim();
|
|
633
|
+
if (issue && !THREAD.test(issue))
|
|
634
|
+
throw new Error("A thread is owner/repo#number, or nothing.");
|
|
635
|
+
const api = ui.current.api;
|
|
636
|
+
const preview = await attachInquiry(api, episode, { artefact: item.artefact, issue, dryRun: true });
|
|
637
|
+
return {
|
|
638
|
+
next: {
|
|
639
|
+
type: "confirm",
|
|
640
|
+
title: `Attach ${cleanLine(item.artefact)} to ${label}?${issue ? ` · ${issue}` : ""}`,
|
|
641
|
+
lines: previewLines(preview, item.artefact),
|
|
642
|
+
...(preview.ok === false ? {} : { yes: "attach" }),
|
|
643
|
+
run: async () => attachedMessage(await attachInquiry(api, episode, { artefact: item.artefact, issue, dryRun: false }), item.artefact),
|
|
644
|
+
after: () => reloadInquiries(handle),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
const askCompanion = (handle, archive) => {
|
|
650
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
651
|
+
if (!entry || entry.overlay.type !== "companion" || !ui.current.api)
|
|
652
|
+
return;
|
|
653
|
+
const episode = entry.overlay.episode;
|
|
654
|
+
updateById(handle, (o) => (o.type === "companion" ? { ...o, archive, busy: "plan", answer: undefined, error: undefined } : o));
|
|
655
|
+
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)));
|
|
656
|
+
};
|
|
657
|
+
const runPlan = (handle) => {
|
|
658
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
659
|
+
if (!entry || entry.overlay.type !== "companion" || !ui.current.api)
|
|
660
|
+
return;
|
|
661
|
+
const { episode, archive, answer } = entry.overlay;
|
|
662
|
+
if (!planActs(answer))
|
|
663
|
+
return set({ status: `Nothing to run: ${cleanLine(answer?.plan?.state ?? "no plan on screen")}.` });
|
|
664
|
+
updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: "run", error: undefined } : o));
|
|
665
|
+
set({ status: "Opening the seat: up to a few minutes. Esc leaves this open; the outcome comes here." });
|
|
666
|
+
runCompanion(ui.current.api, episode, { archive }).then((ran) => {
|
|
667
|
+
updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, ran } : o));
|
|
668
|
+
set({ status: ranMessage(ran) });
|
|
669
|
+
}, (error) => {
|
|
670
|
+
updateById(handle, (o) => (o.type === "companion" ? { ...o, busy: undefined, error: cleanLine(error.message) } : o));
|
|
671
|
+
set({ status: cleanLine(`The seat did not open: ${error.message}`) });
|
|
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
|
+
}, []);
|
|
601
738
|
// ── keys ─────────────────────────────────────────────────────────────────
|
|
602
739
|
// A paste into an input arrives whole, line breaks kept (bracketed paste).
|
|
603
740
|
const pasteActive = ui.current.overlays[ui.current.overlays.length - 1]?.overlay.type === "input";
|
|
@@ -685,6 +822,9 @@ export function App(props) {
|
|
|
685
822
|
if (top) {
|
|
686
823
|
if (top.type === "help")
|
|
687
824
|
return closeTop();
|
|
825
|
+
// An act under way is not left half-seen.
|
|
826
|
+
if (top.type === "confirm" && top.busy)
|
|
827
|
+
return;
|
|
688
828
|
if (key.escape || input === "q" || input === "h" || key.backspace || key.leftArrow)
|
|
689
829
|
return closeTop();
|
|
690
830
|
if (top.type === "doors") {
|
|
@@ -704,6 +844,62 @@ export function App(props) {
|
|
|
704
844
|
}
|
|
705
845
|
if (top.type === "qr" && input === "i")
|
|
706
846
|
return updateTop({ ...top, invert: !top.invert });
|
|
847
|
+
if (top.type === "confirm") {
|
|
848
|
+
// Only an act the dry run allowed: a refusal offers nothing to confirm.
|
|
849
|
+
if (input !== "y" || !top.run || !top.yes)
|
|
850
|
+
return;
|
|
851
|
+
const handle = topHandle();
|
|
852
|
+
const run = top.run;
|
|
853
|
+
updateTop({ ...top, busy: true, error: undefined });
|
|
854
|
+
run().then(async (message) => {
|
|
855
|
+
set({ status: cleanLine(message), overlays: ui.current.overlays.filter((entry) => entry.id !== handle) });
|
|
856
|
+
try {
|
|
857
|
+
await top.after?.();
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
set({ status: cleanLine(`Done, but the refresh failed: ${error.message}`) });
|
|
861
|
+
}
|
|
862
|
+
}, (error) => updateById(handle, (o) => (o.type === "confirm" ? { ...o, busy: false, error: cleanLine(error.message) } : o)));
|
|
863
|
+
return;
|
|
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
|
+
}
|
|
876
|
+
if (top.type === "inquiries") {
|
|
877
|
+
const handle = topHandle();
|
|
878
|
+
const items = top.view?.shelf?.items ?? [];
|
|
879
|
+
if (key.downArrow || input === "j")
|
|
880
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, items.length) });
|
|
881
|
+
if (key.upArrow || input === "k")
|
|
882
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, items.length) });
|
|
883
|
+
if (input === "/" && top.view?.attach)
|
|
884
|
+
return void openOverlay(filterInput(handle, top.episode, top.query));
|
|
885
|
+
const item = items[clamp(top.selected, items.length)];
|
|
886
|
+
if (item && (key.return || input === "l" || key.rightArrow)) {
|
|
887
|
+
if (item.wovenHere)
|
|
888
|
+
return set({ status: `${cleanLine(item.artefact)} is already woven into this episode.` });
|
|
889
|
+
return void openOverlay(attachInput(handle, top.episode, top.label, item));
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (top.type === "companion") {
|
|
894
|
+
const handle = topHandle();
|
|
895
|
+
if (top.busy || top.ran)
|
|
896
|
+
return;
|
|
897
|
+
if (input === "A")
|
|
898
|
+
return askCompanion(handle, !top.archive);
|
|
899
|
+
if (input === "y")
|
|
900
|
+
return runPlan(handle);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
707
903
|
if (top.type === "circles" && top.index) {
|
|
708
904
|
const entries = circleEntries(top.index);
|
|
709
905
|
const step = (from, by) => {
|
|
@@ -898,6 +1094,16 @@ export function App(props) {
|
|
|
898
1094
|
return forward();
|
|
899
1095
|
if (key.escape && s.relateFrom)
|
|
900
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
|
+
}
|
|
901
1107
|
if (key.escape || key.leftArrow || input === "h" || key.backspace)
|
|
902
1108
|
return back();
|
|
903
1109
|
if (input === "?")
|
|
@@ -968,9 +1174,11 @@ export function App(props) {
|
|
|
968
1174
|
const fileWindow = windowOf(d.files, clamp(s.fileSel, d.files.length), Math.max(1, bodyHeight - fixedLines));
|
|
969
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) => {
|
|
970
1176
|
const isSelected = s.view === "files" && fileWindow.start + index === clamp(s.fileSel, d.files.length);
|
|
971
|
-
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));
|
|
972
1178
|
})] })) : (_jsx(Text, { color: "gray", children: "Nothing selected." })) }) }));
|
|
973
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;
|
|
1180
|
+
/** How many screen rows these lines take inside an overlay. */
|
|
1181
|
+
const rowsOf = (texts) => Math.max(1, texts.reduce((sum, text) => sum + wrapSegments([{ text, style: "text" }], Math.max(10, columns - 4)).length, 0));
|
|
974
1182
|
// Children never shrink: a fixed-height box clips what does not fit instead
|
|
975
1183
|
// of squeezing rows to zero height, where Ink draws them over each other.
|
|
976
1184
|
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 }) }));
|
|
@@ -1023,6 +1231,43 @@ export function App(props) {
|
|
|
1023
1231
|
if (top.type === "item") {
|
|
1024
1232
|
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"] })] }));
|
|
1025
1233
|
}
|
|
1234
|
+
if (top.type === "confirm") {
|
|
1235
|
+
const room = Math.max(1, bodyHeight - 2 - (2 + (top.busy ? 1 : 0) + (top.error ? 1 : 0)));
|
|
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" })] })] }));
|
|
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
|
+
}
|
|
1249
|
+
if (top.type === "inquiries") {
|
|
1250
|
+
const view = top.view;
|
|
1251
|
+
const items = view?.shelf?.items ?? [];
|
|
1252
|
+
const wovenRows = view ? Math.max(1, view.woven.length) : 0;
|
|
1253
|
+
const room = Math.max(1, bodyHeight - 2 - (1 + Math.min(wovenRows, 6) + (view?.attach ? 1 : 0) + 1));
|
|
1254
|
+
const shelfWindow = windowOf(items, clamp(top.selected, items.length), room);
|
|
1255
|
+
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) => {
|
|
1256
|
+
const isSelected = shelfWindow.start + index === clamp(top.selected, items.length);
|
|
1257
|
+
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));
|
|
1258
|
+
}), _jsx(Text, { color: "gray", wrap: "truncate-end", children: view?.attach ? "↑↓ · ⏎ attach · / filter · Esc" : view ? "Attaching needs a writer the app accepts · Esc" : "Esc" })] }));
|
|
1259
|
+
}
|
|
1260
|
+
if (top.type === "companion") {
|
|
1261
|
+
const shown = top.ran ?? top.answer;
|
|
1262
|
+
const lines = shown ? companionLines(shown) : [];
|
|
1263
|
+
const acts = !top.ran && planActs(top.answer);
|
|
1264
|
+
const toneColor = { state: "yellow", meta: "gray", warn: "yellow", error: "red", step: "green" };
|
|
1265
|
+
const nothing = !acts && !top.busy && !top.ran && Boolean(shown?.plan?.state);
|
|
1266
|
+
// The plan is clipped above the keys, so `y` and Esc stay on screen however long the starter prompt is.
|
|
1267
|
+
const room = Math.max(1, bodyHeight - 2 - (2 + (top.busy ? 1 : 0) + (top.error ? 1 : 0) + (nothing ? 1 : 0)));
|
|
1268
|
+
const planRows = Math.min(room, rowsOf(lines.map((line) => line.text)));
|
|
1269
|
+
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] }));
|
|
1270
|
+
}
|
|
1026
1271
|
if (top.type === "circles") {
|
|
1027
1272
|
const entries = top.index ? circleEntries(top.index) : [];
|
|
1028
1273
|
const listWindow = windowOf(entries, clamp(top.selected, entries.length), Math.max(1, bodyHeight - 4));
|
|
@@ -1100,6 +1345,10 @@ export function App(props) {
|
|
|
1100
1345
|
const view = s.view;
|
|
1101
1346
|
if (s.overlays.length)
|
|
1102
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";
|
|
1103
1352
|
if (view === "file")
|
|
1104
1353
|
return compact ? "Tab ref · Esc · q" : "Tab next reference ⇧Tab previous Esc back ? keys q quit";
|
|
1105
1354
|
if (view === "attention")
|
|
@@ -1133,6 +1382,6 @@ export function App(props) {
|
|
|
1133
1382
|
? "…"
|
|
1134
1383
|
: "read-only"
|
|
1135
1384
|
: "";
|
|
1136
|
-
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 })] })] }));
|
|
1137
1386
|
}
|
|
1138
1387
|
//# sourceMappingURL=App.js.map
|