Repository navigation
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.
Activity
juliusmarminge commentedon Sep 9, 2026
Triage
Confirmed on current
main. This is a real desktop bug, not a duplicate of #3715 or #5792.What happens
preview_clickleaves Electron keyboard focus in the preview guest. After that, physical keystrokes and pastes go to the previewed page instead of the T3 UI. It also reproduces when the tab was opened withopen: false, so there is no visual cue. The report’s probe log matches this: a hidden-tab click focuses the page, then humankeydown/ Cmd+V arrive there tens of seconds later with no agent action running.Why (code)
In
apps/desktop/src/preview/Manager.ts:performAutomationPressalready saveswebContents.getFocusedWebContents()and restores it inEffect.ensuring(releaseInput).performAutomationClickhas no equivalent. It dispatches CDPInput.dispatchMouseEventmousePressed/mouseReleasedand returns.automationClickalso does not passwcorsendCleanupthrough.mousePressedfocuses the guest as a side effect; nothing puts focus back.restoreFocus.preview_typewas explicitly written to avoid stealing foreground focus. Click never got the matching restore.Related, not the same
preview_presskeys miss the page or leak to the host. Here, after click, user keys leak to the page. fix(desktop): isolate preview keyboard input #8494 does not close this and does not add click save/restore.The secondary
preview_pressclaim (restore races key delivery; 10 ms focus→blur, tool still{}) is consistent withreleaseInputrunning on CDP ack rather than renderer processing. Track that with #3715 / #8494, not as a wash of this issue.Suggested fix
performAutomationClickthe same save/restore as press; passwc+sendCleanupfromautomationClick.Severity: major — user input can silently land in an invisible, agent-chosen page.
fix(desktop): hand keyboard focus back after an automation click (ups…
fix(desktop): hand keyboard focus back after an automation click (ups…
KarstenKreh commentedon Sep 28, 2026
Also seeing this on Linux (Pop!_OS 24.04, COSMIC desktop, T3 Code 0.0.42 AppImage), and it also happens across windows:
preview_clickon a website.So the focus steal isn't limited to the window or thread that owns the preview.
Created with AI · Commissioned by Karsten.
MSPortal commentedon Sep 30, 2026
Seeing this constantly on MacOS on the nightly suddenly. Only started in the past 2-3 days too. It's happening across conversations too which is exceptionally annoying.
JayHopsHQ commentedon Oct 1, 2026
Additional macOS reproduction from T3 Code on 1 October 2026:
This appears to match the confirmed
preview_clickfocus behaviour described above. The screenshot has been redacted to remove the conversation, attachments, and request title while retaining the T3 window and the focused preview/dropdown.fix(preview): agent clicks no longer take the user's typing focus
fix(desktop): agent keys reach iframe fields, settle before replying,…