herdrchat 1.1.0 → 1.1.1
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/lib/core/engine.mjs +39 -9
- package/lib/core/menu.mjs +6 -2
- package/package.json +1 -1
package/lib/core/engine.mjs
CHANGED
|
@@ -44,7 +44,8 @@ const KEY_SETTLE_MS = 700; // a dialog redraws this soon after a key
|
|
|
44
44
|
const CLICK_BUDGET_MS = 2000; // a click must be answered within 3 s
|
|
45
45
|
const OVERVIEW_WAIT_MS = 2000;
|
|
46
46
|
const UPLOAD_TRIES = 3;
|
|
47
|
-
const DRAW_TRIES =
|
|
47
|
+
const DRAW_TRIES = 5;
|
|
48
|
+
const BOX_GAP_MS = 120; // between two boxes pressed for one Next
|
|
48
49
|
const DRAW_WAIT_MS = 400;
|
|
49
50
|
const TITLE_MAX = 40;
|
|
50
51
|
const DIALOG_LINES = 15;
|
|
@@ -580,14 +581,18 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
580
581
|
// `screen`: the screen read when the event came (a promise), so a dialog answered in
|
|
581
582
|
// the terminal while earlier work was still being posted still gets its card. `again`:
|
|
582
583
|
// post it even if it was posted (you typed in the room while it waits). A screen read
|
|
583
|
-
// the moment `blocked` arrives may come before the dialog is drawn
|
|
584
|
-
// the
|
|
584
|
+
// the moment `blocked` arrives may come before the dialog is drawn, and then a numbered
|
|
585
|
+
// list the agent wrote looks like a menu: a dialog is believed once it has its top
|
|
586
|
+
// border and two reads DRAW_WAIT_MS apart show the same one, or after DRAW_TRIES reads.
|
|
585
587
|
async function postBlocked(session, address, record, { screen: first = null, again = false } = {}) {
|
|
586
588
|
const firstScreen = await first;
|
|
587
589
|
let dialog = firstScreen ? dialogOf(record.status, firstScreen) : null;
|
|
588
|
-
for (let i = 0; i < DRAW_TRIES
|
|
589
|
-
if (i) await sleep(DRAW_WAIT_MS);
|
|
590
|
-
|
|
590
|
+
for (let i = 0; i < DRAW_TRIES; i += 1) {
|
|
591
|
+
if (dialog || i) await sleep(DRAW_WAIT_MS);
|
|
592
|
+
const next = dialogOf(record.status, await hc.screenTail(session, address, SCREEN_LINES));
|
|
593
|
+
const steady = dialog?.menu?.framed && next.menu?.key === dialog.menu.key;
|
|
594
|
+
dialog = next;
|
|
595
|
+
if (steady) break;
|
|
591
596
|
}
|
|
592
597
|
const k = key(session, address);
|
|
593
598
|
if (!claimDialog(k, dialog, record.status) && !again) return;
|
|
@@ -630,23 +635,48 @@ export function createEngine({ cfg, chat, hc, readers, store, log, logWarn, logE
|
|
|
630
635
|
async function onAction({ value: v = {}, message, answer = '' }) {
|
|
631
636
|
const started = Date.now();
|
|
632
637
|
const shown = dialogCards.get(message) || { title: NEEDS_TITLE, markdown: '', doneTitle: DONE_TITLE };
|
|
638
|
+
// A box in a multi-select is ticked on the card only, at once; Next sends them all.
|
|
639
|
+
const box = shown.menu?.multi && typeof v.pick === 'number' ? shown.menu.options.find((o) => o.n === v.pick && o !== shown.menu.free && o.checked !== null) : null;
|
|
640
|
+
if (box) {
|
|
641
|
+
const menu = { ...shown.menu, options: shown.menu.options.map((o) => (o === box ? { ...o, checked: !o.checked } : o)) };
|
|
642
|
+
const toggled = { ...shown, menu };
|
|
643
|
+
remember(message, toggled);
|
|
644
|
+
return { ...toast('info', `${box.checked ? '☐' : '☑'} ${box.label}`), card: { kind: 'dialog', dialog: toggled, value: v } };
|
|
645
|
+
}
|
|
633
646
|
const record = views.get(v.s)?.terminals.get(v.a);
|
|
634
|
-
if (!record || record.state !== 'live')
|
|
647
|
+
if (!record || record.state !== 'live') {
|
|
648
|
+
log(`-> ${v.s}/${v.a}: click on ${message} not pressed: the terminal has ended`);
|
|
649
|
+
return answered(shown, 'The terminal has ended.');
|
|
650
|
+
}
|
|
635
651
|
const status = (await hc.getTerminal(v.s, v.a)).status;
|
|
636
652
|
const menu = status?.state === 'blocked' ? menuOf(await hc.screenTail(v.s, v.a, SCREEN_LINES)) : null;
|
|
637
653
|
if (!menu || menu.key !== v.k) {
|
|
654
|
+
log(`-> ${v.s}/${v.a}: click on ${message} not pressed: ${menu ? `the dialog changed (card ${v.k}, screen ${menu.key})` : `nothing waits (status ${status?.state || 'none'})`}`);
|
|
638
655
|
if (menu) later(v.s, v.a, 'dialog', () => postBlocked(v.s, v.a, { ...record, status }));
|
|
639
656
|
return answered(shown, menu ? 'It now asks something else; see the new card.' : 'It no longer waits on this.');
|
|
640
657
|
}
|
|
641
658
|
const k = key(v.s, v.a);
|
|
642
659
|
if (openCard.get(k)?.message === message) openCard.delete(k);
|
|
643
660
|
let picked;
|
|
644
|
-
if (v.pick === '
|
|
661
|
+
if (v.pick === 'tab' && shown.menu?.multi) {
|
|
662
|
+
// the boxes the card shows, made so on the screen: each one that differs is pressed
|
|
663
|
+
const changed = shown.menu.options.filter((o) => o.checked !== null && o.checked !== menu.options.find((m) => m.n === o.n)?.checked);
|
|
664
|
+
for (const o of changed) {
|
|
665
|
+
await hc.keys(v.s, v.a, [String(o.n)]);
|
|
666
|
+
await sleep(BOX_GAP_MS);
|
|
667
|
+
}
|
|
668
|
+
await hc.keys(v.s, v.a, ['tab']);
|
|
669
|
+
const ticked = shown.menu.options.filter((o) => o.checked).map((o) => o.label);
|
|
670
|
+
picked = ticked.length ? ticked.join(', ') : 'nothing ticked';
|
|
671
|
+
} else if (v.pick === 'esc' || v.pick === 'tab') {
|
|
645
672
|
await hc.keys(v.s, v.a, [v.pick]);
|
|
646
673
|
picked = v.pick === 'esc' ? 'Esc pressed' : 'Next';
|
|
647
674
|
} else {
|
|
648
675
|
const option = menu.options.find((o) => o.n === v.pick);
|
|
649
|
-
if (!option || option.label !== v.label)
|
|
676
|
+
if (!option || option.label !== v.label) {
|
|
677
|
+
log(`-> ${v.s}/${v.a}: click on ${message} not pressed: option ${v.pick} is now ${option ? `"${option.label}"` : 'gone'}, not "${v.label}"`);
|
|
678
|
+
return answered(shown, 'The options changed; nothing was pressed.');
|
|
679
|
+
}
|
|
650
680
|
if (option === menu.free) {
|
|
651
681
|
const text = answer.trim();
|
|
652
682
|
if (!text) return toast('warning', 'Type your answer first.');
|
package/lib/core/menu.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// `text` is what the dialog says above its options: from its top border (a line of ─),
|
|
10
10
|
// without rules and blank lines; what was on the screen above the dialog is left out.
|
|
11
11
|
// `key` names the dialog: a hash of it from its top border to its last option, without
|
|
12
|
-
// the cursor and
|
|
12
|
+
// the cursor and any check mark (an option's [✔], and the ☐/☒/✔ of the question tabs
|
|
13
|
+
// above a multi-question dialog, which change as you answer: the same dialog keeps its key). A card click is pressed only when the
|
|
13
14
|
// screen still shows the dialog with that key, so an old card can never answer a newer
|
|
14
15
|
// dialog that looks alike (two permission prompts both offer "1. Yes").
|
|
15
16
|
import crypto from 'node:crypto';
|
|
@@ -27,8 +28,10 @@ function topOf(lines, first) {
|
|
|
27
28
|
return BORDER.test(lines[top]) ? top : Math.max(0, first - TEXT_LINES);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
const MARKS = /[☐☑☒✔✓✗✘■□●○◉◯]/gu;
|
|
32
|
+
|
|
30
33
|
function keyOf(lines, top, last) {
|
|
31
|
-
const body = lines.slice(top, last + 1).map((l) => l.replace(/❯/g, '').replace(/^(\s*\d{1,2}\.\s+)\[.\]/, '$1').trim()).join('\n');
|
|
34
|
+
const body = lines.slice(top, last + 1).map((l) => l.replace(/❯/g, '').replace(/^(\s*\d{1,2}\.\s+)\[.\]/, '$1').replace(MARKS, '').replace(/\s+/g, ' ').trim()).join('\n');
|
|
32
35
|
return crypto.createHash('sha1').update(body).digest('hex').slice(0, 12);
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -53,6 +56,7 @@ export function menuOf(screen) {
|
|
|
53
56
|
const top = topOf(lines, menu.first);
|
|
54
57
|
return {
|
|
55
58
|
key: keyOf(lines, top, menu.last),
|
|
59
|
+
framed: BORDER.test(lines[top]), // a dialog's top border; a numbered list in text has none
|
|
56
60
|
text: lines.slice(top, menu.first).filter((l) => !RULE.test(l)).map((l) => l.trim()).join('\n'),
|
|
57
61
|
options: menu.options,
|
|
58
62
|
multi: menu.options.some((o) => o.checked !== null),
|