Skip to content

[Bug]: preview_click leaves keyboard focus inside the preview tab, so the user's typing goes to the page #10980

Closed

Description

@BenjaminPeters-code

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

  1. Serve a page that logs focus and input events with timestamps. The probe used here is at the
    bottom of this issue.
  2. From an agent, call preview_open with open: false, so the tab exists but is never shown.
  3. Call preview_click once on any element in that hidden tab.
  4. 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

  1. Give performAutomationClick the same save-and-restore as performAutomationPress: capture
    webContents.getFocusedWebContents() before dispatch and restore it in an Effect.ensuring
    block. This needs automationClick to pass wc and sendCleanup through, the way
    automationPress already does.
  2. Before restoring focus in performAutomationPress, wait for the renderer to have actually
    processed the key, for example by round-tripping a Runtime.evaluate on the same session after
    the keyUp dispatch, rather than restoring right after the CDP acknowledgement.
  3. 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

  1. juliusmarminge commented on Sep 9, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on current main. This is a real desktop bug, not a duplicate of #3715 or #5792.

    What happens

    preview_click leaves 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 with open: false, so there is no visual cue. The report’s probe log matches this: a hidden-tab click focuses the page, then human keydown / Cmd+V arrive there tens of seconds later with no agent action running.

    Why (code)

    In apps/desktop/src/preview/Manager.ts:

    • performAutomationPress already saves webContents.getFocusedWebContents() and restores it in Effect.ensuring (releaseInput).
    • performAutomationClick has no equivalent. It dispatches CDP Input.dispatchMouseEvent mousePressed / mouseReleased and returns. automationClick also does not pass wc or sendCleanup through.
    • CDP mousePressed focuses the guest as a side effect; nothing puts focus back.
    • Existing click tests only assert pointer dispatch. Press tests already assert restoreFocus.

    preview_type was explicitly written to avoid stealing foreground focus. Click never got the matching restore.

    Related, not the same

    The secondary preview_press claim (restore races key delivery; 10 ms focus→blur, tool still {}) is consistent with releaseInput running on CDP ack rather than renderer processing. Track that with #3715 / #8494, not as a wash of this issue.

    Suggested fix

    1. Give performAutomationClick the same save/restore as press; pass wc + sendCleanup from automationClick.
    2. Add Manager coverage that click restores the previous WebContents, including a hidden tab.
    3. Do not let a hidden tab keep application focus.
    4. When restoring, reuse the fix(desktop): preserve user focus during preview presses #9848 “don’t steal focus back from a newer user action” guard so this does not regress [Bug]: Browser automation steals composer focus across threads #5792.
    5. Follow-up: wait for the key to be processed (or land fix(desktop): isolate preview keyboard input #8494’s confirmed native path) before press restore.

    Severity: major — user input can silently land in an invisible, agent-chosen page.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 9, 2026
  3. KarstenKreh commented on Sep 28, 2026

    @KarstenKreh

    Also seeing this on Linux (Pop!_OS 24.04, COSMIC desktop, T3 Code 0.0.42 AppImage), and it also happens across windows:

    • An agent in one T3 Code window runs preview_click on a website.
    • I'm typing in the composer of a different T3 Code window.
    • The composer keeps losing focus while the other agent clicks, so I have to click back into the input again and again.

    So the focus steal isn't limited to the window or thread that owns the preview.

    Created with AI · Commissioned by Karsten.

  4. MSPortal commented on Sep 30, 2026

    @MSPortal

    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.

  5. JayHopsHQ commented on Oct 1, 2026

    @JayHopsHQ

    Additional macOS reproduction from T3 Code on 1 October 2026:

    • I was typing in the T3 composer while an agent was controlling the shared browser against an authenticated Odoo page.
    • An automated click opening a native dropdown moved focus away from the composer and interrupted my typing.
    • The dropdown appeared over the T3 desktop UI, providing a visible indication that the preview guest had retained focus.
    • This occurred during ordinary browser automation rather than a deliberate click into the preview.

    This appears to match the confirmed preview_click focus 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.

    Redacted T3 Code screenshot showing a preview dropdown retaining focus

  6. added a commit that references this issue on Oct 3, 2026
    a33c148
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions