[Bug]: preview_click leaves keyboard focus inside the preview tab, so the user's typing goes to the page
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/desktop
Summary
Agent automation in the preview panel takes the application's keyboard focus and never gives it
back. After a single preview_click from an agent, keystrokes from the user's physical keyboard are
delivered to the previewed page instead of to the T3 UI, until the user notices and clicks somewhere
by hand.
This also happens when the preview tab is invisible (opened with open: false), so there is no
visual cue at all. From the user's side it looks like the app randomly stops accepting typing.
A second, related problem in the same code path: preview_press does restore focus, but so quickly
that the key is never delivered to the page, while the tool still reports success.
Steps to reproduce
- Serve a page that logs focus and input events with timestamps. The probe used here is at the
bottom of this issue. - From an agent, call
preview_openwithopen: false, so the tab exists but is never shown. - Call
preview_clickonce on any element in that hidden tab. - Touch nothing else, wait, then type on the physical keyboard.
Expected behavior
After the automated click completes, keyboard focus returns to whatever had it before the click,
the same way performAutomationPress already restores it. A hidden preview tab in particular should
never end up holding the application's input focus.
Actual behavior
Focus stays in the previewed page. document.hasFocus() is true there, and subsequent keydown
events arrive with isTrusted: true while no agent action is running, i.e. they are the user's own
keystrokes. Focus only comes back when the user clicks elsewhere by hand.
Timeline from the probe page, measured 2026-09-09 (UTC):
| Time | Event | document.hasFocus() |
Source |
|---|---|---|---|
| 17:16:29.576 | page loaded | false | baseline |
| 17:16:40.801 | window focus (twice, same ms) |
true | agent: one preview_click, panel hidden |
| 17:16:40.803 | pointerdown (68, 291), isTrusted: true |
true | the synthetic click itself |
| 17:17:10.862 | keydown Alt / AltRight, isTrusted: true |
true | physical keyboard, 30 s later, no agent action running |
| 17:18:16.390 | window focus |
true | agent: preview_press Tab |
| 17:18:16.400 | window blur |
false | focus restored after 10 ms, Tab never arrived as a keydown |
| 17:18:43.198 | window focus |
true | agent: preview_press b |
| 17:18:43.294 | window blur |
false | focus restored after 96 ms, b never arrived as a keydown |
| 17:18:58.277 | window focus |
true | agent: preview_click |
| 17:19:00.441 | keydown b |
true | agent: preview_press b, now it does arrive, because the click already parked focus in the tab |
| 17:19:13.998 | keydown Meta / MetaLeft |
true | physical keyboard |
| 17:19:14.012 | keydown v |
true | physical keyboard, a Cmd+V paste that landed in the hidden page |
| 17:19:16.886 | window blur |
false | user clicked back into the app by hand |
Three of the four keystrokes that reached the hidden page came from the physical keyboard, one of
them a paste. Focus sat in an invisible page for 2 minutes and 36 seconds.
The two preview_press calls at 17:18:16 and 17:18:43 both returned successfully ({}) while
producing zero keydown events on the page.
Cause in the source
In apps/desktop/src/preview/Manager.ts on main:
performAutomationPress (line 3915) handles focus correctly. It captures the previously focused
WebContents at lines 3926 to 3929:
const previouslyFocused = yield* attempt(
{ operation: "automationPress.getFocusedWebContents", tabId, webContentsId: wc.id },
() => webContents.getFocusedWebContents(),
);
and restores it in releaseInput at lines 3938 to 3947, guarded by Effect.ensuring at line 3964.
The comment at lines 3950 to 3954 states the intent explicitly: "Restore the previous renderer after
dispatch so automation never leaves the app's input focus behind."
performAutomationClick (line 3722) has no equivalent. Its body is prepareAutomationInput ->
resolveClickPoint -> viewport bounds check -> emitPointerEvent (move) -> sleep ->
emitPointerEvent (click) -> sleep -> expectAgentInput -> Input.dispatchMouseEvent
mousePressed / mouseReleased. There is no getFocusedWebContents, no wc.focus() and no
restore. The mousePressed dispatch moves focus into the guest WebContents as a side effect and
nothing puts it back. automationClick (line 3779) also does not pass sendCleanup into it, so
there is currently no cleanup channel on the click path at all.
For the second problem: releaseInput runs as soon as the effect body finishes. The body's last
step is send("Input.dispatchKeyEvent", keySequence.keyDown) at line 3963, which resolves when the
CDP command is acknowledged, not when the renderer has processed the key. releaseInput then
immediately disables focus emulation (line 3935) and calls previouslyFocused.focus() (line 3945).
The measurement shows a 10 ms window between focus and blur with no keydown reaching the page, and
a control case where the same key does arrive once a prior preview_click has already left focus
in the tab. The restore is racing the delivery.
Suggested fix
- Give
performAutomationClickthe same save-and-restore asperformAutomationPress: capture
webContents.getFocusedWebContents()before dispatch and restore it in anEffect.ensuring
block. This needsautomationClickto passwcandsendCleanupthrough, the way
automationPressalready does. - Before restoring focus in
performAutomationPress, wait for the renderer to have actually
processed the key, for example by round-tripping aRuntime.evaluateon the same session after
thekeyUpdispatch, rather than restoring right after the CDP acknowledgement. - Consider treating a hidden tab (
open: false) as never eligible to hold application focus at
all, since the user has no way to see where their keystrokes went.
Impact
Major degradation or frequent failure
Worse than a cosmetic focus glitch: keystrokes and pastes from the user's real keyboard are silently
delivered to an arbitrary web page the agent chose, with no visual indication.
Version or commit
T3 Code (Alpha) 0.0.40, com.t3tools.t3code
Environment
macOS 15 (Darwin 25.5.0), Apple Silicon. Preview tab opened over MCP with
preview_open { open: false }, so the panel was never shown.
Logs
Raw event log from the probe page (JSON)
[
{"kind":"START","tMs":2,"wallClock":"2026-09-09T17:16:29.576Z","hasFocus":false,"active":"BODY"},
{"kind":"WIN-FOCUS","tMs":11227,"wallClock":"2026-09-09T17:16:40.801Z","hasFocus":true,"active":"BODY","source":"agent: preview_click #1 (panel NOT visible)"},
{"kind":"WIN-FOCUS","tMs":11227,"wallClock":"2026-09-09T17:16:40.801Z","hasFocus":true,"active":"BODY","source":"agent: preview_click #1 (second focus event, same ms)"},
{"kind":"POINTERDOWN","tMs":11229,"wallClock":"2026-09-09T17:16:40.803Z","hasFocus":true,"active":"BODY","x":68,"y":291,"button":0,"isTrusted":true},
{"kind":"KEYDOWN","tMs":41257,"wallClock":"2026-09-09T17:17:10.862Z","hasFocus":true,"active":"buttonA","key":"Alt","code":"AltRight","isTrusted":true,"source":"HUMAN: physical keyboard, 30s after the agent click, no agent action running"},
{"kind":"WIN-BLUR","tMs":42048,"wallClock":"2026-09-09T17:17:11.622Z","hasFocus":false,"active":"buttonA"},
{"kind":"WIN-FOCUS","tMs":106815,"wallClock":"2026-09-09T17:18:16.390Z","hasFocus":true,"active":"buttonA","source":"agent: preview_press Tab"},
{"kind":"WIN-BLUR","tMs":106825,"wallClock":"2026-09-09T17:18:16.400Z","hasFocus":false,"active":"buttonA","source":"press restores focus after 10 ms; Tab did NOT arrive as a keydown"},
{"kind":"WIN-FOCUS","tMs":133624,"wallClock":"2026-09-09T17:18:43.198Z","hasFocus":true,"active":"buttonA","source":"agent: preview_press b"},
{"kind":"WIN-BLUR","tMs":133720,"wallClock":"2026-09-09T17:18:43.294Z","hasFocus":false,"active":"buttonA","source":"press restores focus after 96 ms; b did NOT arrive as a keydown"},
{"kind":"WIN-FOCUS","tMs":148703,"wallClock":"2026-09-09T17:18:58.277Z","hasFocus":true,"active":"buttonA","source":"agent: preview_click #2"},
{"kind":"POINTERDOWN","tMs":148704,"wallClock":"2026-09-09T17:18:58.278Z","hasFocus":true,"active":"buttonA","x":68,"y":291,"button":0,"isTrusted":true},
{"kind":"KEYDOWN","tMs":150867,"wallClock":"2026-09-09T17:19:00.441Z","hasFocus":true,"active":"buttonA","key":"b","code":"KeyB","isTrusted":true,"source":"agent: preview_press b, DID arrive because the click had already parked focus here"},
{"kind":"KEYDOWN","tMs":164424,"wallClock":"2026-09-09T17:19:13.998Z","hasFocus":true,"active":"buttonA","key":"Meta","code":"MetaLeft","isTrusted":true,"source":"HUMAN: physical keyboard, no agent action running"},
{"kind":"KEYDOWN","tMs":164438,"wallClock":"2026-09-09T17:19:14.012Z","hasFocus":true,"active":"buttonA","key":"v","code":"KeyV","isTrusted":true,"source":"HUMAN: Cmd+V (paste) landed in the invisible preview tab"},
{"kind":"WIN-BLUR","tMs":167312,"wallClock":"2026-09-09T17:19:16.886Z","hasFocus":false,"active":"buttonA","source":"user clicked back into the app by hand"}
]
Probe page used for the measurement
<!doctype html>
<meta charset="utf-8">
<title>focus probe</title>
<body style="font:14px system-ui;background:#111;color:#eee;padding:24px">
<h1 id="state">hasFocus: ?</h1>
<button id="buttonA">button A</button>
<input id="field" placeholder="type here">
<pre id="log"></pre>
<script>
const start = performance.now();
window.__log = [];
const note = (kind, extra) => {
const row = {
kind,
tMs: Math.round(performance.now() - start),
wallClock: new Date().toISOString(),
hasFocus: document.hasFocus(),
active: document.activeElement
? (document.activeElement.id || document.activeElement.tagName)
: null,
...extra,
};
window.__log.push(row);
document.getElementById('log').textContent =
window.__log.map((r) => JSON.stringify(r)).join('\n');
document.getElementById('state').textContent = 'hasFocus: ' + document.hasFocus();
};
note('START', { url: location.href });
addEventListener('focus', () => note('WIN-FOCUS', {}));
addEventListener('blur', () => note('WIN-BLUR', {}));
addEventListener('keydown', (e) =>
note('KEYDOWN', { key: e.key, code: e.code, isTrusted: e.isTrusted }), true);
addEventListener('pointerdown', (e) =>
note('POINTERDOWN', { x: e.clientX, y: e.clientY, button: e.button, isTrusted: e.isTrusted }), true);
let last = document.hasFocus();
setInterval(() => {
const now = document.hasFocus();
if (now !== last) { last = now; note('POLL-CHANGE', {}); }
}, 100);
</script>
</body>
A rendered screenshot of the same timeline is available; I can attach it in a comment if that helps.
Workaround
None from the user's side. Turning off the floating preview panel does not help: the bug reproduces
with the panel hidden. The only mitigation is to avoid agent preview_click calls, or to notice the
lost focus and click back into the app by hand.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/desktop/src/preview/Manager.ts by reading performAutomationClick, automationClick, and the existing performAutomationPress focus handling. Reproduce the hidden-tab preview_click and preview_press cases, then trace the cleanup and key dispatch timing. Done means clicks no longer leave focus in the preview and presses reliably deliver their key before focus is restored.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100