@assuredloop/orca-bot-kit 0.23.0 → 0.25.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/SETUP.md +22 -0
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +12 -2
- package/src/book.js +94 -3
- package/src/clear.js +141 -42
- package/src/cli.js +45 -1
- package/src/groom.js +17 -10
- package/src/hooks.js +67 -31
- package/src/message.js +15 -1
- package/src/name.js +138 -0
- package/src/orca.js +34 -2
- package/src/skills.js +10 -1
- package/src/temp.js +208 -27
- package/src/up.js +49 -27
package/src/temp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `obk temp make` and `obk temp retire`: a session's own temporary sessions
|
|
2
|
-
// (PRD 6.4, #227). And `obk temp trust-hooks
|
|
3
|
-
// Codex run's hooks review (#238)
|
|
2
|
+
// (PRD 6.4, #227). And `obk temp trust-hooks` and `obk temp answer`, the
|
|
3
|
+
// answers a maker gives its Codex run's hooks review (#238) and its Claude
|
|
4
|
+
// session's Teach auto mode form (#489).
|
|
4
5
|
//
|
|
5
6
|
// Any long-lived session can make a temporary session of its own bot for a
|
|
6
7
|
// piece of work, and retire it when the work is done, without Bot Father, who
|
|
@@ -28,7 +29,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
28
29
|
import path from 'node:path';
|
|
29
30
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
30
31
|
|
|
31
|
-
import { readBook, updateBook } from './book.js';
|
|
32
|
+
import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook } from './book.js';
|
|
32
33
|
import { addSession, dropSession, NAME, readBot, tempRoles } from './bot.js';
|
|
33
34
|
import { DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
34
35
|
import { sessionInTab } from './message.js';
|
|
@@ -348,30 +349,18 @@ export async function trustHooks(bots, { tab, name }) {
|
|
|
348
349
|
if (harness !== 'codex') {
|
|
349
350
|
throw new Error(`${run} runs on ${harness}, and temp trust-hooks answers a Codex run's "${HOOKS_REVIEW}" alone. ${nothing}`);
|
|
350
351
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
// The return is inside the text: `--enter` would be a second key.
|
|
365
|
-
orca(['terminal', 'send', '--terminal', handle, '--text', keys]);
|
|
366
|
-
for (let waited = 0; ; waited += 500) {
|
|
367
|
-
const after = screenRows(handle);
|
|
368
|
-
if (after.rows !== undefined && !after.rows.some((row) => row.includes(HOOKS_REVIEW))) break;
|
|
369
|
-
if (waited >= REVIEW_GONE_MS) {
|
|
370
|
-
const still = after.rows === undefined ? `its screen could not be read again (${after.unreadable})` : `its screen still shows "${HOOKS_REVIEW}"`;
|
|
371
|
-
throw new Error(`${run}: "${TRUST_ALL}" was chosen on its hooks review, but ${REVIEW_GONE_MS / 1000} seconds later ${still}. Look at its tab.`);
|
|
372
|
-
}
|
|
373
|
-
await pause(500);
|
|
374
|
-
}
|
|
352
|
+
await answerScreen(caller, name, {
|
|
353
|
+
what: 'hooks review',
|
|
354
|
+
mark: HOOKS_REVIEW,
|
|
355
|
+
done: `"${TRUST_ALL}" was chosen on its hooks review`,
|
|
356
|
+
answer(rows, { send }) {
|
|
357
|
+
const keys = keysToTrustAll(rows);
|
|
358
|
+
if (keys === undefined) {
|
|
359
|
+
throw new Error(`${run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(rows)}. ${nothing}`);
|
|
360
|
+
}
|
|
361
|
+
send(keys);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
375
364
|
return { bot: caller.bot, session: name, maker: caller.session };
|
|
376
365
|
}
|
|
377
366
|
|
|
@@ -396,6 +385,198 @@ function shown(rows) {
|
|
|
396
385
|
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
397
386
|
}
|
|
398
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Answer a screen of `name`, a temporary session the caller made: under the
|
|
390
|
+
* session's turn for a line (#482), read its screen and hand it to
|
|
391
|
+
* `answer(rows, { send, look })`, which sends keys with `send`, may read the
|
|
392
|
+
* screen again with `look`, and throws when the screen is not its to answer.
|
|
393
|
+
* Then wait for `mark` to leave the screen. `what` names the screen and `done`
|
|
394
|
+
* says what was sent, for the sentences that refuse or fail; `done` may be a
|
|
395
|
+
* function of what `answer` returned.
|
|
396
|
+
*/
|
|
397
|
+
async function answerScreen(caller, name, { what, mark, done, answer }) {
|
|
398
|
+
const run = `${caller.bot}/${name}`;
|
|
399
|
+
const nothing = 'Nothing was typed.';
|
|
400
|
+
const tabId = readBook(caller.home).sessions[name]?.tab;
|
|
401
|
+
const handle = tabId === undefined ? undefined : tabs(caller.home).find((one) => one.tabId === tabId)?.handle;
|
|
402
|
+
if (handle === undefined) {
|
|
403
|
+
throw new Error(`${run} has no tab open in Orca, so there is no ${what} of its to answer. ${nothing}`);
|
|
404
|
+
}
|
|
405
|
+
const turn = takeLineTurn(caller.home, name, TYPING_WAIT_MS);
|
|
406
|
+
if (turn === undefined) throw new Error(`${run}: ${TYPING_HELD}.`);
|
|
407
|
+
let answered;
|
|
408
|
+
try {
|
|
409
|
+
const seen = screenRows(handle);
|
|
410
|
+
if (seen.rows === undefined) {
|
|
411
|
+
throw new Error(`${run}'s screen could not be read (${seen.unreadable}), so the kit cannot tell whether its ${what} is there. ${nothing}`);
|
|
412
|
+
}
|
|
413
|
+
answered = answer(seen.rows, {
|
|
414
|
+
// The return, where there is one, is inside the text: `--enter` would be a second key.
|
|
415
|
+
send: (keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys]),
|
|
416
|
+
look: () => screenRows(handle),
|
|
417
|
+
});
|
|
418
|
+
} finally {
|
|
419
|
+
turn.release();
|
|
420
|
+
}
|
|
421
|
+
for (let waited = 0; ; waited += 500) {
|
|
422
|
+
const after = screenRows(handle);
|
|
423
|
+
if (after.rows !== undefined && !after.rows.some((row) => row.includes(mark))) return answered;
|
|
424
|
+
if (waited >= REVIEW_GONE_MS) {
|
|
425
|
+
const still = after.rows === undefined ? `its screen could not be read again (${after.unreadable})` : `its screen still shows "${mark}"`;
|
|
426
|
+
throw new Error(`${run}: ${typeof done === 'function' ? done(answered) : done}, but ${REVIEW_GONE_MS / 1000} seconds later ${still}. Look at its tab.`);
|
|
427
|
+
}
|
|
428
|
+
await pause(500);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The title of Claude Code's "Teach auto mode" screen, in each known shape. */
|
|
433
|
+
const TEACH_TITLE = 'Teach auto mode about your environment?';
|
|
434
|
+
|
|
435
|
+
/** The foot that ends a Claude Code form, below which its input box sits. */
|
|
436
|
+
const TEACH_FOOT = /Enter to .*· Esc to cancel/;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The known shapes of the Teach screen, from its title to its foot, each as
|
|
440
|
+
* captured live, with what answers it. Rows are compared trimmed, the pointer
|
|
441
|
+
* `❯` taken out, so the pointer may sit on any one of them.
|
|
442
|
+
*
|
|
443
|
+
* Claude Code 2.1.283's form (#261's live test, #416) is answered with Esc,
|
|
444
|
+
* which cancels it (Not now), never a return, which is Continue. Claude Code
|
|
445
|
+
* 2.1.289's numbered list (#489's live run 1) is answered with "2. Not now",
|
|
446
|
+
* never "1. Yes" or "3. Don't show again" (the architect's ruling on #489).
|
|
447
|
+
*/
|
|
448
|
+
const TEACH_SHAPES = [
|
|
449
|
+
{
|
|
450
|
+
name: 'the 2.1.283 form',
|
|
451
|
+
rows: [
|
|
452
|
+
TEACH_TITLE,
|
|
453
|
+
'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
|
|
454
|
+
'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
|
|
455
|
+
'How you use Claude here Mixed',
|
|
456
|
+
'Also scan shell history true',
|
|
457
|
+
'Also scan your other repos false',
|
|
458
|
+
'Continue',
|
|
459
|
+
'←/→ to change usage · Enter to continue · Esc to cancel',
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'the 2.1.289 list',
|
|
464
|
+
rows: [
|
|
465
|
+
TEACH_TITLE,
|
|
466
|
+
'Auto mode works better when it knows your environment. Takes about a minute.',
|
|
467
|
+
'1. Yes',
|
|
468
|
+
'2. Not now',
|
|
469
|
+
"3. Don't show again",
|
|
470
|
+
'Enter to confirm · Esc to cancel',
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
/** The 2.1.289 list's choice this kit takes, and the row it is on. */
|
|
476
|
+
const NOT_NOW_CHOICE = '2. Not now';
|
|
477
|
+
|
|
478
|
+
/** A row as a shape is compared: trimmed, with its pointer, wherever it sits, taken out. */
|
|
479
|
+
const unpointed = (row) => row.replace('❯', ' ').trim();
|
|
480
|
+
|
|
481
|
+
/** The full-width rule Claude Code draws right above the screen's title: `─` in 2.1.289, `▔` in 2.1.283. */
|
|
482
|
+
const ABOVE_TITLE = /^(?:─+|▔+)$/;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The Teach screen on `rows`, from its title to its foot, the rows below the
|
|
486
|
+
* foot (the input box) left out: `{ shape, block }` when its non-blank rows
|
|
487
|
+
* are one known shape's, in its order and nothing else, with the pointer on
|
|
488
|
+
* exactly one; otherwise `{ misfit }`, why not.
|
|
489
|
+
*
|
|
490
|
+
* It counts only in the frame Claude Code drew it in, in both live captures:
|
|
491
|
+
* a full-width rule as the non-blank row right above the title, and, right
|
|
492
|
+
* below the foot, nothing or the input box's top rule. A quote of the screen
|
|
493
|
+
* in a turn, with the turn's own rows around it, is refused (the review of
|
|
494
|
+
* #490). A quote of the frame as well would still look the same: the frame
|
|
495
|
+
* narrows what passes, and is no proof that the screen is live.
|
|
496
|
+
*/
|
|
497
|
+
function teachScreen(rows) {
|
|
498
|
+
const from = rows.findIndex((row) => row.trim() === TEACH_TITLE);
|
|
499
|
+
if (from < 0) return { misfit: `it shows no "${TEACH_TITLE}"` };
|
|
500
|
+
const foot = rows.findIndex((row, at) => at > from && TEACH_FOOT.test(row));
|
|
501
|
+
if (foot < 0) return { misfit: `it shows "${TEACH_TITLE}" with no foot row ending "Esc to cancel" under it` };
|
|
502
|
+
const above = rows.slice(0, from).findLast((row) => row.trim() !== '');
|
|
503
|
+
if (above === undefined || !ABOVE_TITLE.test(above.trim())) {
|
|
504
|
+
return { misfit: `the row right above its title is ${above === undefined ? 'nothing' : `"${above.trim()}"`}, not the full-width rule Claude Code draws there` };
|
|
505
|
+
}
|
|
506
|
+
const below = rows.slice(foot + 1).find((row) => row.trim() !== '');
|
|
507
|
+
if (below !== undefined && !below.trimStart().startsWith('─')) {
|
|
508
|
+
return { misfit: `the row right below its foot is "${below.trim()}", not the top rule of the input box` };
|
|
509
|
+
}
|
|
510
|
+
const block = rows.slice(from, foot + 1).filter((row) => row.trim() !== '');
|
|
511
|
+
const misfits = TEACH_SHAPES.map((shape) => {
|
|
512
|
+
const pointers = block.filter((row) => row.includes('❯')).length;
|
|
513
|
+
if (pointers !== 1) return `${pointers} rows with the pointer ❯, where ${shape.name} has one`;
|
|
514
|
+
const odd = block.find((row) => !shape.rows.includes(unpointed(row)));
|
|
515
|
+
if (odd !== undefined) return `a row ${shape.name} does not have: ${odd.trim()}`;
|
|
516
|
+
const missing = shape.rows.find((row) => !block.map(unpointed).includes(row));
|
|
517
|
+
if (missing !== undefined) return `no row ${missing}, which ${shape.name} has`;
|
|
518
|
+
if (block.length !== shape.rows.length) return `${block.length} rows, where ${shape.name} has ${shape.rows.length}`;
|
|
519
|
+
const moved = block.findIndex((row, at) => unpointed(row) !== shape.rows[at]);
|
|
520
|
+
if (moved >= 0) return `the row ${block[moved].trim()} where ${shape.name} has ${shape.rows[moved]}`;
|
|
521
|
+
return undefined;
|
|
522
|
+
});
|
|
523
|
+
const fits = misfits.findIndex((misfit) => misfit === undefined);
|
|
524
|
+
if (fits >= 0) return { shape: TEACH_SHAPES[fits], block };
|
|
525
|
+
return { misfit: `its Teach screen is no shape the kit knows: it has ${misfits.join('; and it has ')}` };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** The whole screen, a row to a line: what decides is often at its foot. */
|
|
529
|
+
const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row) => ` ${row}`).join('\n');
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Answer the first-run screen of a temporary session the caller made (#489):
|
|
533
|
+
* Claude Code's Teach auto mode screen, in one of the shapes the kit knows,
|
|
534
|
+
* matched exactly. The 2.1.283 form gets Esc. The 2.1.289 list gets arrows to
|
|
535
|
+
* "2. Not now" where needed, a second look that the pointer is there and the
|
|
536
|
+
* list is otherwise as it was, and only then a return. Anything else is refused with
|
|
537
|
+
* nothing typed. Like `trustHooks`, it is one command a permission rule of its
|
|
538
|
+
* own can allow. Returns `{ bot, session, maker, sent }`, `sent` saying what
|
|
539
|
+
* answered it.
|
|
540
|
+
*/
|
|
541
|
+
export async function answerTemp(bots, { tab, name }) {
|
|
542
|
+
const caller = callerIn(bots, tab, 'answer', "answer a session's first-run screen");
|
|
543
|
+
const nothing = 'Nothing was typed.';
|
|
544
|
+
ownTemp(caller, name, 'answer for', nothing);
|
|
545
|
+
const run = `${caller.bot}/${name}`;
|
|
546
|
+
const sent = await answerScreen(caller, name, {
|
|
547
|
+
what: `"${TEACH_TITLE}"`,
|
|
548
|
+
mark: TEACH_TITLE,
|
|
549
|
+
done: (said) => `${said} was sent to its Teach auto mode screen`,
|
|
550
|
+
answer(rows, { send, look }) {
|
|
551
|
+
const seen = teachScreen(rows);
|
|
552
|
+
if (seen.misfit !== undefined) {
|
|
553
|
+
const hooks = rows.some((row) => row.includes(HOOKS_REVIEW)) ? ` A Codex run's "${HOOKS_REVIEW}" is answered with temp trust-hooks.` : '';
|
|
554
|
+
throw new Error(`${run}'s screen is not one temp answer answers: ${seen.misfit}.${hooks} ${nothing} It shows:\n${wholeScreen(rows)}`);
|
|
555
|
+
}
|
|
556
|
+
if (seen.shape === TEACH_SHAPES[0]) {
|
|
557
|
+
send('\x1b');
|
|
558
|
+
return 'Esc (Not now)';
|
|
559
|
+
}
|
|
560
|
+
const at = seen.block.findIndex((row) => row.includes('❯'));
|
|
561
|
+
const moves = seen.block.findIndex((row) => unpointed(row) === NOT_NOW_CHOICE) - at;
|
|
562
|
+
if (moves !== 0) send(moves > 0 ? '\x1b[B'.repeat(moves) : '\x1b[A'.repeat(-moves));
|
|
563
|
+
// The guard, before every return, arrows or none: the list again, its
|
|
564
|
+
// rows in the same order (an exact shape), and the pointer on "2. Not now".
|
|
565
|
+
const again = look();
|
|
566
|
+
const now = again.rows === undefined ? { misfit: `it could not be read again (${again.unreadable})` } : teachScreen(again.rows);
|
|
567
|
+
const on = now.block?.find((row) => row.includes('❯'));
|
|
568
|
+
if (now.shape !== seen.shape || unpointed(on) !== NOT_NOW_CHOICE) {
|
|
569
|
+
const why = now.misfit ?? `its pointer is on ${unpointed(on)}, not ${NOT_NOW_CHOICE}`;
|
|
570
|
+
const sent = moves === 0 ? 'Nothing was typed' : `The arrows to "${NOT_NOW_CHOICE}" went in`;
|
|
571
|
+
throw new Error(`${run}: ${sent}, but then ${why}, so no return was sent. Look at its tab.${again.rows === undefined ? '' : ` It shows:\n${wholeScreen(again.rows)}`}`);
|
|
572
|
+
}
|
|
573
|
+
send('\r');
|
|
574
|
+
return `"${NOT_NOW_CHOICE}"`;
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
return { bot: caller.bot, session: name, maker: caller.session, sent };
|
|
578
|
+
}
|
|
579
|
+
|
|
399
580
|
/**
|
|
400
581
|
* The session this runs in, as the book knows it, with its home and whether it
|
|
401
582
|
* is itself temporary.
|
package/src/up.js
CHANGED
|
@@ -8,7 +8,7 @@ import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
10
10
|
|
|
11
|
-
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeMailboxTurn, updateBook, withUnclaimed } from './book.js';
|
|
11
|
+
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
12
12
|
import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
13
13
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
14
|
import { installHook } from './hooks.js';
|
|
@@ -374,7 +374,7 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
374
374
|
// 0.157.1 (#226). So it is given one line, through the same gate as a mail
|
|
375
375
|
// notice, which the owner allowed. A fresh start's first turn is its prompt.
|
|
376
376
|
const list = harness === 'codex' && launch.resume !== undefined && tui.running === true
|
|
377
|
-
? await typeListLine(home, made.tabId)
|
|
377
|
+
? await typeListLine(home, session.name, made.tabId)
|
|
378
378
|
: undefined;
|
|
379
379
|
|
|
380
380
|
return entry(made, {
|
|
@@ -408,43 +408,65 @@ const LIST_LOOK_MS = 2000;
|
|
|
408
408
|
const LIST_ASK_MS = 500;
|
|
409
409
|
|
|
410
410
|
/**
|
|
411
|
-
* Type `line`, LIST_LINE unless told otherwise, into the tab `tabId`
|
|
412
|
-
* gate every typed line goes through lets it:
|
|
413
|
-
* `{ typed: false, why }` with the gate's last reason
|
|
414
|
-
* Nothing goes into a question, a form or a menu
|
|
415
|
-
* the kit cannot tell about.
|
|
411
|
+
* Type `line`, LIST_LINE unless told otherwise, into the tab `tabId` of
|
|
412
|
+
* `session` once the gate every typed line goes through lets it:
|
|
413
|
+
* `{ typed: true }`, or `{ typed: false, why }` with the gate's last reason
|
|
414
|
+
* once LIST_WAIT_MS is up. Nothing goes into a question, a form or a menu
|
|
415
|
+
* (#329, #416), nor into a tab the kit cannot tell about. Each look and its
|
|
416
|
+
* line are taken in the session's turn for a line (#482), unless the caller
|
|
417
|
+
* says it holds the session's typing turn already (`held`).
|
|
416
418
|
*/
|
|
417
|
-
export async function typeListLine(home, tabId, line = LIST_LINE) {
|
|
419
|
+
export async function typeListLine(home, session, tabId, { line = LIST_LINE, held = false } = {}) {
|
|
418
420
|
const until = Date.now() + LIST_WAIT_MS;
|
|
419
421
|
for (;;) {
|
|
420
|
-
let
|
|
421
|
-
|
|
422
|
-
// Each look waits no longer than what is left of the wait.
|
|
423
|
-
found = tabToTypeInto(home, tabId, Math.max(1, Math.min(LIST_LOOK_MS, until - Date.now())));
|
|
424
|
-
} catch (error) {
|
|
425
|
-
return { typed: false, why: `Orca would not say whether it may be typed into: ${error.message}` };
|
|
426
|
-
}
|
|
427
|
-
// A yes that comes after the wait is too late: the wait is a cutoff, not a
|
|
428
|
-
// count of looks (review of PR #421).
|
|
429
|
-
if (found.handle !== undefined && Date.now() > until) {
|
|
430
|
-
return { typed: false, why: `the ${LIST_WAIT_MS / 1000} s wait for it ran out before the kit could tell it may be typed into` };
|
|
431
|
-
}
|
|
432
|
-
if (found.handle !== undefined) {
|
|
422
|
+
let turn;
|
|
423
|
+
if (!held) {
|
|
433
424
|
try {
|
|
434
|
-
|
|
435
|
-
return { typed: true };
|
|
425
|
+
turn = takeLineTurn(home, session, TYPING_WAIT_MS);
|
|
436
426
|
} catch (error) {
|
|
437
|
-
return { typed: false, why:
|
|
427
|
+
return { typed: false, why: error.message };
|
|
438
428
|
}
|
|
429
|
+
if (turn === undefined) return { typed: false, why: TYPING_HELD };
|
|
439
430
|
}
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
431
|
+
try {
|
|
432
|
+
const done = lookAndType(home, tabId, line, until);
|
|
433
|
+
if (done !== undefined) return done;
|
|
434
|
+
} finally {
|
|
435
|
+
turn?.release();
|
|
443
436
|
}
|
|
444
437
|
await pause(LIST_ASK_MS);
|
|
445
438
|
}
|
|
446
439
|
}
|
|
447
440
|
|
|
441
|
+
/** One look of typeListLine's, and its line when the gate lets it: what it returns, or undefined to look again. */
|
|
442
|
+
function lookAndType(home, tabId, line, until) {
|
|
443
|
+
let found;
|
|
444
|
+
try {
|
|
445
|
+
// Each look waits no longer than what is left of the wait.
|
|
446
|
+
found = tabToTypeInto(home, tabId, Math.max(1, Math.min(LIST_LOOK_MS, until - Date.now())));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return { typed: false, why: `Orca would not say whether it may be typed into: ${error.message}` };
|
|
449
|
+
}
|
|
450
|
+
// A yes that comes after the wait is too late: the wait is a cutoff, not a
|
|
451
|
+
// count of looks (review of PR #421).
|
|
452
|
+
if (found.handle !== undefined && Date.now() > until) {
|
|
453
|
+
return { typed: false, why: `the ${LIST_WAIT_MS / 1000} s wait for it ran out before the kit could tell it may be typed into` };
|
|
454
|
+
}
|
|
455
|
+
if (found.handle !== undefined) {
|
|
456
|
+
try {
|
|
457
|
+
typeIntoTab(found.handle, line);
|
|
458
|
+
return { typed: true };
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return { typed: false, why: `Orca refused the line: ${error.message}` };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (Date.now() >= until) {
|
|
464
|
+
const last = found.blocked !== undefined ? `it is waiting on ${found.blocked}` : found.unsure ?? 'no harness is running in it';
|
|
465
|
+
return { typed: false, why: `the ${LIST_WAIT_MS / 1000} s wait for it ran out: ${last}` };
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
448
470
|
/**
|
|
449
471
|
* The line that starts this session: resuming `resume`, or, with none, a fresh
|
|
450
472
|
* start carrying its duty. Returns `{ resume, prompt, promptFile, command, address }`.
|