Skip to content

fix: focus the terminal created from the "R Terminal" profile - #1839

Open
davidkane9 wants to merge 2 commits into
REditorSupport:mainfrom
davidkane9:focus-profile-terminal
Open

davidkane9 wants to merge 2 commits into
REditorSupport:mainfrom
davidkane9:focus-profile-terminal

Conversation

@davidkane9

Copy link
Copy Markdown

Problem

Creating an R terminal from the terminal panel's "+" menu → "R Terminal" (the contributed profile) opens the terminal without focus when the extension host is remote — GitHub Codespaces in the browser is where we hit it, and SSH / vscode.dev should behave the same. Every other profile (bash, zsh) takes focus, and so does "R: Create R Terminal" from the Command Palette, which makes the "+" path feel broken.

Cause

On the profile path the terminal is created by VS Code, not by createRTerm(), so the extension never calls show(). VS Code does try to focus it (terminalService.createContributedTerminalProfile → setActiveInstanceByIndex(instances.length - 1) → focusWhenReady()), but on the extension-host side $createContributedProfileTerminal calls createTerminalFromOptions(...) without awaiting it and returns. With a slow round trip to a remote extension host, "the last instance" at that moment is still the previous terminal; the new one registers afterwards, unfocused. On a local desktop the race is usually won, which is why this is mostly reported from Codespaces.

Fix

provideTerminalProfile() records that it just handed VS Code a terminal; onDidOpenTerminal focuses the first R Interactive terminal that opens while that request is pending (10 s expiry, so an aborted creation can never steal focus later). This mirrors what createRTerm() already does for the command path. No new settings.

Three tests in terminal.test.ts: focus exactly once per profile request, nothing without a pending request, and expiry.

Verified

  • Codespaces (web client, Chrome), vscode-R 3.0.1, arf console: before this change "+ → R Terminal" leaves focus where it was; the Command Palette command moves it. (The reporter runs a course of ~100 students on this setup.)
  • pnpm run lint, tsc --noEmit, and the extension test suite locally (389 passing; the two Session Communication timeouts are local-environment only — no sess in the local R library — and unrelated).

A separate upstream report to VS Code about the un-awaited creation would let this workaround go away eventually; this PR makes the extension behave correctly in the meantime.

A terminal created from the contributed profile (the terminal panel's "+"
menu) is created by VS Code, not by createRTerm(), so the extension never
called show() on it. VS Code does try to focus it, but the extension host
fires off the creation without awaiting it ($createContributedProfileTerminal
-> createTerminalFromOptions) and then focuses "the last terminal", which
with a remote extension host (Codespaces, SSH, the web client) is still the
previous terminal. Result: the new R terminal opens without focus, unlike
every other profile and unlike "R: Create R Terminal".

provideTerminalProfile() now records that it just handed VS Code a terminal,
and onDidOpenTerminal focuses the first "R Interactive" terminal that opens
while that request is pending (10 s expiry, so an aborted creation cannot
steal focus later). Tests cover focus-once, no-pending, and expiry.
Copilot AI balanced review requested due to automatic review settings October 7, 2026 00:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Pending requests are not uniquely correlated or counted, allowing overlapping or unrelated R terminals to consume the focus request.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds focus handling for R terminals created through the contributed terminal profile.

Changes:

  • Tracks pending profile-terminal creation and focuses the opened terminal.
  • Registers focus handling during terminal-open events.
  • Adds regression tests and changelog documentation.
File Description
src/​rTerminal.ts Adds pending-request focus logic.
src/​extension.ts Connects profile creation and terminal-open handling.
src/​test/​suite/​terminal.test.ts Tests focus, inactivity, and expiry behavior.
CHANGELOG.md Documents the fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/rTerminal.ts Outdated

export async function provideTerminalProfile(resource?: vscode.Uri): Promise<vscode.TerminalProfile> {
const options = await makeTerminalOptions(resource);
profileTerminalPendingUntil = Date.now() + PROFILE_TERMINAL_PENDING_MS;
@davidkane9

Copy link
Copy Markdown
Author

Filed the underlying VS Code bug: microsoft/vscode#340185 (contributed-profile terminal creation is not awaited before VS Code focuses the "last" instance). This PR is the extension-side workaround until that lands.

@renkun-ken renkun-ken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 07058c3. The workaround addresses the reported focus race, but the opened terminal needs to be correlated with the profile request before focus is taken. I reproduced a separate failure from the overlapping-request case already raised: an unrelated command-created R terminal can consume the request, override its preserve-focus behavior, and leave the actual profile terminal unfocused. Details are inline.

Validation: the 53 tests in terminal.test.ts pass in an isolated VS Code 1.110.0 extension host, and an additional diagnostic test confirms the incorrect event ordering described below. Type checking, lint, and the extension build pass. GitHub CI is also green.

Comment thread src/rTerminal.ts Outdated
Comment on lines +312 to +316
if (terminal.name !== 'R Interactive') {
return false;
}
profileTerminalPendingUntil = 0;
terminal.show();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Match the opened terminal to its profile request before focusing it

R Interactive is also the name used by createRTerm(), including the createRTerm(true) path used by Run Selection and Restart R Terminal. If one of those terminals opens while a profile terminal is still pending on a slow remote host, this handler consumes the profile request and calls show() on the command terminal, overriding its preserve-focus behavior. When the actual profile terminal opens, the deadline has already been cleared, so the reported focus bug remains.

I reproduced this against the PR code by requesting a profile, calling createRTerm(true), and delivering the command terminal's open event first: its show calls are [true] followed by [], and focusProfileTerminal() then returns false for the terminal created from profile.options. This can also happen after an aborted profile creation, throughout the 10-second pending window. Please attach a unique identifier to each profile's terminal options and consume only the corresponding request when that identifier appears in the opened terminal's creationOptions; a name match alone (even with a queue of deadlines) cannot distinguish these paths. Add a regression test where an unrelated R terminal opens before the profile terminal.

Review (renkun-ken, Copilot): a name match could be consumed by an
unrelated createRTerm(true) terminal (Run Selection, Restart) — overriding
its preserve-focus and leaving the real profile terminal unfocused — and a
single deadline could not represent several in-flight requests.

Each request is now keyed by the options object handed to VS Code, which
VS Code freezes and exposes unchanged as terminal.creationOptions; only the
terminal whose creationOptions IS that object is focused, each request is
consumed once, and stale requests expire. Regression tests: an unrelated R
terminal opening first, several in-flight requests, no request, expiry.
@davidkane9

Copy link
Copy Markdown
Author

Thanks, both points addressed in the follow-up commit.

Correlation is now by identity of the options object: provideTerminalProfile() keys the pending request on the exact TerminalOptions it returns, and VS Code hands that same object back (frozen) as terminal.creationOptions (extHostTerminalService.ts: new ExtHostTerminal(..., options, ...) → Object.freeze(this._creationOptions)). So only the terminal created from that request is focused; a createRTerm(true) terminal — same name, different object — is never matched and keeps its preserve-focus, and each in-flight request is matched to its own terminal. Entries expire individually.

New regression tests: an unrelated R terminal opening before the profile terminal (show never called on it, profile terminal still focused afterwards), several in-flight requests, no pending request, and expiry. Lint, tsc and the extension suite pass locally.

@renkun-ken renkun-ken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 15fd0a5. The options-identity correlation addresses my previous finding and the overlapping-request issue. I also verified it through a real contributed-profile creation in VS Code 1.110.0: terminal.creationOptions is the same frozen object returned by the provider, and the request is consumed exactly once. The previous createRTerm(true) reproduction now preserves focus and leaves the profile request intact.

One timing regression remains: the deadline is captured before asynchronous options preparation, so a slow preparation can exhaust the focus window before the profile is returned. Details and a deterministic reproduction are inline.

Validation: all 55 tests in terminal.test.ts pass, as do the previous regression reproduction, the slow-preparation diagnostic, and the real profile-creation check. Type checking, lint, and the extension build pass. All GitHub CI checks are green.

Comment thread src/rTerminal.ts
Comment on lines +303 to +305
export async function provideTerminalProfile(resource?: vscode.Uri, now: number = Date.now()): Promise<vscode.TerminalProfile> {
const options = await makeTerminalOptions(resource);
pendingProfileTerminals.set(options, now + PROFILE_TERMINAL_PENDING_MS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Start the expiry window after terminal options are ready

The default now = Date.now() is evaluated when provideTerminalProfile() is called, before await makeTerminalOptions(resource). That preparation awaits R-path discovery and, with the session watcher enabled, session-server initialization and discovery-file filesystem operations. Its duration now counts against the 10-second window even though VS Code cannot begin creating this terminal until the profile is returned. Slow setup can therefore return an already-expired request, or leave too little time for the remote creation round trip, and the handler drops the matching terminal without focusing it. The previous implementation sampled the deadline after the await.

I reproduced this without passing the test-only now argument: stub Date.now() to return 1,000 at entry, advance it to 12,000 inside the awaited getRterm() stub, then open a terminal whose creationOptions === profile.options immediately after the provider returns. focusProfileTerminal() returns false and never calls show(). Please calculate the deadline after makeTerminalOptions() resolves (if injecting a clock for tests, evaluate that clock there), and cover time advancing during options preparation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants