2026-09-04 OpenRouter Interceptor Generate-and-Send From the Prompt Popup
What I set out to do
The / saved-prompts popup on JanitorAI inserts a template and fires Alt+Enter so JanitorAI writes a message for the user into the composer. That left the generated text sitting in the box. I wanted a one-keystroke path that generates and then sends once the generation is done.
What I actually did
- New helper
src/utils/janitorai-composer.ts:findChatInput,requestGeneration(synthetic Alt+Enter),sendComposer(synthetic bare Enter), andwaitForGenerationToSettle. The last one polls the textarea because there is no event for “generation finished”: JanitorAI streams intovalueprogrammatically (noinputevents, nothing for a MutationObserver), and shows a_stopButtonwhile a request is in flight. Settled means all of: the value has moved off the instruction text or a stop button appeared, no stop button is present, the textarea is enabled, the value is non-empty, and it has held still for 1.5 s. A 120 s cap or an AbortSignal ends the wait without sending, so the raw template or a half-written message never goes out. - Popup: Enter still generates; Ctrl/Cmd+Enter (and Ctrl/Cmd+click on an item) generates and arms the send. A footer hint shows both keys. While armed, a small pill reads “Sending when generation finishes · Esc to cancel”. Any trusted keystroke, or a real click on the stop button, cancels. Synthetic events are ignored via
isTrusted. - Bug found by the test: pressing Cmd fires its own keydown for
Meta, which hit the popup’s “any other key closes me” branch. The popup closed before Enter arrived, the Enter fell through to the page, and the page sent an empty message. Modifier-only keys are now ignored by that branch. - Tests written first: 13 vitest cases (happy-dom, fake timers) for the settle logic, and
test/playwright/janitorai-generate-and-send.spec.tswith a mock composer that streams words on Alt+Enter and records a bare Enter as a send. 6 Playwright cases on both browsers, including never-sends-the-instruction and Escape-cancels.
What was striking
- Chrome was not logged in to JanitorAI, so I could not confirm the composer’s live behaviour. The design leans on the same synthetic-keydown mechanism the existing popup already relied on, plus a conjunction of every DOM signal available, rather than any one assumption about the page. Worth a real check in the PWA: whether the stop button shows during user-message generation, and whether the settle window is long enough for slow models.
- A
settleMsheuristic is inherently a guess about token pacing. The conjunction with the stop button is what makes it safe when the button is present; when it is not, 1.5 s of silence is the only signal.
Verification
vitest 1301 pass (13 new) · Playwright generate-and-send + saved-prompts specs 68 pass (Chromium + Firefox) · lint, typecheck, and CSS reachability clean. Uncommitted at time of writing.
Related
- 2026-07-13 OpenRouter Interceptor Continue-Message Shortcut (the Alt+Enter shortcut and the aria-label-only lesson)
- 2026-09-04 OpenRouter Interceptor Review Findings Actioned
Follow-up (2026-09-05): it did not work, and the capture said why
The first cut sent a bare Enter once the composer text held still. A diagnostic capture from the real chat showed two things the mock page could not:
- JanitorAI’s generate-for-user (“enhance”) ends in a review state, not in the composer.
pageControlslisted Keep / Regenerate / Discard (_reviewButton_130io_66 _keepButton_130io_85), an active “Cancel enhance” slash button, and abutton[aria-label="Send"]. The screenshot showed the bar sitting above the composer with the generated text below it. A bare Enter there sends nothing; the text has to be kept first. - The send was never armed anyway. The page keydown buffer showed a plain
Enterbefore each of the four insertions, with noMetaorControlkeydown ahead of it, and the page console had no “Generation …” line at all. Either the PWA was still running the old script (the symlinked dist needs the extension reloaded) or plain Enter was pressed expecting the send.
Rework: acceptGenerationAndSend(doc) in janitorai-composer.ts replaces the settle heuristic entirely. It polls for the Keep button (generation finished), clicks it, waits for the bar to go and Send to be enabled, then clicks Send. Timeouts at each stage and an AbortSignal end it without sending. Cancellation now also fires on a real click on any review-bar or slash button. The value-stability guess is gone: the page offers a precise signal, and the capture was what surfaced it. Unit tests rewritten around a review-bar fixture (10 cases), Playwright mock rebuilt to the same shape with a Discard-cancels case (7 cases). 1298 vitest, 70 Playwright, lint clean. Capture deleted after use.
Lesson, same as 2026-07-13 OpenRouter Interceptor Continue-Message Shortcut: pageControls answered the question in one look. Building against a guessed DOM before taking a capture was the wrong order.
Follow-up 2 (2026-09-05): the key was Enter all along
A second capture showed the same thing as the first: a plain Enter on the popup, no Meta or Control keydown ahead of it, no “generate and send” console line. Asked, and the answer was “i had just pressed enter”. The feature had been bound to a chord nobody was pressing.
Two changes:
- The keydown buffer now records held modifiers on each entry (
modifiers: ["Meta"]), with the schema defaulting older bundles to none. “Meta” then “Enter” as two entries could equally be a Cmd tap and a plain Enter; the flags on the Enter itself are what settle it. The insertion log line now also says “(generate and send)” or “(generate only)“. - Keys swapped. Enter, or a plain click, now generates and sends. Ctrl/Cmd+Enter, or a modified click, generates only, which is the old behaviour kept for a look before sending. The popup hint reflects it. Playwright spec rewritten to the new bindings, 70 pass across both browsers; 1299 vitest; lint clean.
Lesson: when the user says a feature “didn’t work” twice and the capture shows the feature was never invoked, the binding is the bug, not the mechanism. Two rounds of mechanism work happened before the one-line question.
Follow-up 3 (2026-09-05): Cmd+Tab was cancelling the send
Works now, but switching apps with Cmd+Tab while it waited cancelled the send. Cmd+Tab delivers a trusted “Meta” keydown to the page before macOS takes the chord, and the “any real keystroke means the user took over” rule counted it. Modifier-only keys (Control, Meta, Alt, Shift) are now excluded from that rule, using the same set the popup already used to ignore the first half of Cmd+Enter. Playwright case added (“a modifier key on its own leaves the send armed”), watched fail against the old script via a stash, then pass.
Follow-up 4 (2026-09-05): Escape only, and a status that belongs to the page
Feedback: no cancel outside Escape, and the floating dark pill “felt odd and out of place”. Cancellation is now a trusted Escape keydown and nothing else; Cmd+Tab, swipe arrows, and even the review bar’s own buttons leave the send armed (Regenerate therefore ends in a kept-and-sent regenerated message, which is the consistent reading of “only Escape stops it”). The status became a span inserted right after the persona switcher (button[class*="_switcher"], “Dragon” in the captures) inside the composer’s footer row, with font: inherit, color: inherit, 0.6 opacity, and a pulsing dot; no background, border, or shadow of its own. Fallback is right after the textarea when the switcher is absent. Playwright mock grew a composer footer with the switcher so the placement is asserted (status is the switcher’s next sibling). 72 Playwright, 1299 vitest, lint clean.