Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce6e6d11c6 |
@@ -18,11 +18,13 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*
|
||||
* <p>Only a box that is positively empty clears the gate. A box with content, a pane this cannot
|
||||
* recognise, and a failed read all hold the delivery, because a held delivery is recoverable and a
|
||||
* submitted half-line is not. Every caller must therefore be a path that retries.
|
||||
* submitted half-line is not. A hold is bounded, though: after {@link #HOLD_GIVE_UP_STREAK}
|
||||
* consecutive holds on one target, the gate gives up and lets the delivery through anyway, because a
|
||||
* channel that never delivers again is worse than one clobbered line.
|
||||
*
|
||||
* <p>A pane that holds for {@link #HOLD_WARN_STREAK} consecutive checks gets one warning, so a box
|
||||
* that never clears is visible instead of silent. The warning repeats only after the box has cleared
|
||||
* again.
|
||||
* again, and giving up logs its own warning the same way.
|
||||
*/
|
||||
public final class PromptBox {
|
||||
|
||||
@@ -39,6 +41,9 @@ public final class PromptBox {
|
||||
/** Consecutive holds for one target before one warning is logged. */
|
||||
static final int HOLD_WARN_STREAK = 20;
|
||||
|
||||
/** Consecutive holds for one target before the gate stops holding and lets the delivery through. */
|
||||
static final int HOLD_GIVE_UP_STREAK = 1200;
|
||||
|
||||
/**
|
||||
* Input box markers, each matched only as a line's first characters: the caret the current TUI
|
||||
* draws, and the bordered box an older one drew. A marker further along a line is transcript text,
|
||||
@@ -70,7 +75,9 @@ public final class PromptBox {
|
||||
|
||||
/**
|
||||
* Whether {@code target}'s input box is empty, so a delivery would submit only its own text.
|
||||
* {@code false} means hold and come back; it never means the delivery failed.
|
||||
* {@code false} means hold and come back; it never means the delivery failed. After {@link
|
||||
* #HOLD_GIVE_UP_STREAK} consecutive holds on the same target, this returns {@code true} instead,
|
||||
* so the delivery goes through rather than holding the channel shut forever.
|
||||
*/
|
||||
public boolean clearToSubmit(String target) {
|
||||
Reading reading = inspect(target);
|
||||
@@ -79,6 +86,13 @@ public final class PromptBox {
|
||||
return true;
|
||||
}
|
||||
int streak = holdStreaks.merge(target, 1, Integer::sum);
|
||||
if (streak >= HOLD_GIVE_UP_STREAK) {
|
||||
holdStreaks.remove(target);
|
||||
log.warn("prompt box of {} never cleared after {} holds ({}, {} character(s) in the box)"
|
||||
+ " — giving up and sending this delivery anyway, which may submit whatever is in the box",
|
||||
target, streak, reading.state(), reading.characters());
|
||||
return true;
|
||||
}
|
||||
if (streak == HOLD_WARN_STREAK) {
|
||||
log.warn("prompt box of {} has held a delivery {} times in a row ({}, {} character(s) in the box)"
|
||||
+ " — nothing is lost, delivery resumes once the box is empty",
|
||||
@@ -114,10 +128,9 @@ public final class PromptBox {
|
||||
* an earlier turn's marker survives; treating that as a live turn would make {@link State#EMPTY}
|
||||
* unreachable and hold every delivery forever.
|
||||
*
|
||||
* <p>Whitespace, a trailing box border and a cursor block count as nothing. Any other character
|
||||
* counts as the operator's unsubmitted text, including a placeholder hint a future TUI might draw
|
||||
* there; that direction holds a delivery it could have sent, which {@link #HOLD_WARN_STREAK} makes
|
||||
* visible.
|
||||
* <p>Whitespace — including non-breaking variants such as U+00A0 — a trailing box border and a
|
||||
* cursor block all count as nothing. Any other character counts as the operator's unsubmitted
|
||||
* text, including a placeholder hint a TUI might draw there.
|
||||
*/
|
||||
static Reading classify(String pane) {
|
||||
if (pane == null || pane.isBlank()) return new Reading(State.UNREADABLE, 0);
|
||||
@@ -161,7 +174,7 @@ public final class PromptBox {
|
||||
if (line.endsWith("│")) line = line.substring(0, line.length() - 1);
|
||||
StringBuilder content = new StringBuilder();
|
||||
for (char c : line.toCharArray()) {
|
||||
if (Character.isWhitespace(c) || CURSOR_GLYPHS.indexOf(c) >= 0) continue;
|
||||
if (Character.isWhitespace(c) || Character.isSpaceChar(c) || CURSOR_GLYPHS.indexOf(c) >= 0) continue;
|
||||
content.append(c);
|
||||
}
|
||||
return content.toString();
|
||||
|
||||
@@ -90,6 +90,25 @@ class PromptBoxTest {
|
||||
"that marker survives in scrollback, and holding on it would hold every delivery forever");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aNoBreakSpacePaddedBoxIsEmpty() {
|
||||
assertEquals(new PromptBox.Reading(PromptBox.State.EMPTY, 0),
|
||||
PromptBox.classify("❯ "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anOrdinarySpacePaddedBoxIsStillEmpty() {
|
||||
assertEquals(new PromptBox.Reading(PromptBox.State.EMPTY, 0), PromptBox.classify("❯ "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aNoBreakSpaceLeadInDoesNotCountTowardTheDraftsCharacters() {
|
||||
PromptBox.Reading reading = PromptBox.classify("❯ hello");
|
||||
assertEquals(PromptBox.State.DRAFT, reading.state());
|
||||
assertEquals("hello".length(), reading.characters(),
|
||||
"the no-break space before the typed text does not count, only the text itself");
|
||||
}
|
||||
|
||||
// --- the gate ------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@@ -129,4 +148,41 @@ class PromptBoxTest {
|
||||
herdr.detectionText(EMPTY);
|
||||
assertTrue(box.clearToSubmit("term_a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aBoxThatNeverClearsIsLetThroughOnceTheGiveUpStreakIsReached() {
|
||||
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
|
||||
PromptBox box = new PromptBox(new AgentControl(herdr));
|
||||
|
||||
for (int i = 1; i < PromptBox.HOLD_GIVE_UP_STREAK; i++) {
|
||||
assertFalse(box.clearToSubmit("term_a"), "still holding before the give-up streak is reached");
|
||||
}
|
||||
assertTrue(box.clearToSubmit("term_a"), "the gate gives up and lets the delivery through");
|
||||
}
|
||||
|
||||
@Test
|
||||
void givingUpResetsTheStreakSoTheNextHoldStartsOver() {
|
||||
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
|
||||
PromptBox box = new PromptBox(new AgentControl(herdr));
|
||||
|
||||
for (int i = 0; i < PromptBox.HOLD_GIVE_UP_STREAK; i++) {
|
||||
box.clearToSubmit("term_a");
|
||||
}
|
||||
assertFalse(box.clearToSubmit("term_a"), "the streak started over, so one more hold is not another give-up");
|
||||
}
|
||||
|
||||
@Test
|
||||
void theStreakResetsWhenTheBoxEmptiesBeforeTheGiveUpStreak() {
|
||||
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
|
||||
PromptBox box = new PromptBox(new AgentControl(herdr));
|
||||
|
||||
for (int i = 0; i < PromptBox.HOLD_GIVE_UP_STREAK - 1; i++) {
|
||||
box.clearToSubmit("term_a");
|
||||
}
|
||||
herdr.detectionText(EMPTY);
|
||||
assertTrue(box.clearToSubmit("term_a"));
|
||||
|
||||
herdr.detectionText(DRAFTED);
|
||||
assertFalse(box.clearToSubmit("term_a"), "the streak started over, so one more hold is not a give-up");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user