[eric] dictation: the cursor wins, so a transcript lands where you clicked, not where you started (reverts ENG-176)

This commit is contained in:
ciregenz
2026-08-07 16:01:05 -07:00
parent d1d8597513
commit 5b63c98854
2 changed files with 22 additions and 6 deletions
+7 -4
View File
@@ -9,10 +9,13 @@ export type InjectTarget = 'field' | 'webview' | 'composer' | null;
export function injectAtFocus(text: string): InjectTarget {
const snap = takeInjectSnapshot();
// The field the user dictated into is gone. Whatever holds focus now is a stranger's box, and
// typing their words into it is worse than dropping them, so drop them.
if (snap.targetLost && !snap.browserId) return null;
const active = snap.el || (document.activeElement as HTMLElement | null);
// The cursor wins, not where you started. Wispr's grammar, and Eric's call: you dictate, you click
// where you want it, it lands there. This deliberately reverts the snapshot-first version, which
// pinned the text to the origin field and dropped it outright when that field went away.
// The snapshot is still the fallback for the case it was really built for: focus drifting to
// nothing typeable (a button, the body) while you were talking.
const live = document.activeElement as HTMLElement | null;
const active = isUsableTarget(live) ? live : snap.el;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) {
try {
active.focus();
@@ -8,13 +8,13 @@ const field = (connected = true) => ({ tagName: 'TEXTAREA', isConnected: connect
beforeEach(() => clearInjectSnapshot());
test('the snapshotted field wins over whatever is focused later', () => {
test('the snapshot keeps the field it was given (injectAtFocus decides precedence, not this)', () => {
const a = field();
setInjectSnapshot({ el: a, browserId: null });
assert.equal(takeInjectSnapshot().el, a);
});
test('a detached field is refused so injection falls back to live focus', () => {
test('a detached field is refused, so a dead origin can never be the destination', () => {
setInjectSnapshot({ el: field(false), browserId: null });
assert.equal(takeInjectSnapshot().el, null);
});
@@ -37,3 +37,16 @@ test('a cancelled take leaves nothing behind', () => {
clearInjectSnapshot();
assert.equal(takeInjectSnapshot().el, null);
});
// Precedence lives in injectAtFocus, and Eric's call is Wispr's: the CURSOR wins, not the origin.
// injectAtFocus needs a live DOM, so what is pinned here is the predicate that decides whether the
// live element is allowed to win at all. Getting this wrong is how the text lands in a stranger's box.
test('a live click target only beats the origin when it is really typeable', () => {
const typeable = { tagName: 'INPUT', isConnected: true, isContentEditable: false } as unknown as HTMLElement;
const button = { tagName: 'BUTTON', isConnected: true, isContentEditable: false } as unknown as HTMLElement;
const body = { tagName: 'BODY', isConnected: true, isContentEditable: false } as unknown as HTMLElement;
assert.equal(isUsableTarget(typeable), true, 'clicking another field must take the text');
assert.equal(isUsableTarget(button), false, 'clicking a button must NOT take the text');
assert.equal(isUsableTarget(body), false, 'clicking empty space must NOT take the text');
assert.equal(isUsableTarget(null), false);
});