Skip to content

Run rstudioapi::jobRunScript() in a new background console - #17042

Merged
juliasilge merged 3 commits into
mainfrom
feat/jobrunscript-new-session
Nov 7, 2026
Merged

juliasilge merged 3 commits into
mainfrom
feat/jobrunscript-new-session

Conversation

@juliasilge

Copy link
Copy Markdown
Member

Summary

rstudioapi::jobRunScript() now starts a new R console session in the background, and that session runs the script with source().

Screenshot 2026-11-01 at 3 47 52 PM

The caller's console stays selected and free while the script runs. For example, targets::tar_make(as_job = TRUE) now runs the pipeline in its own console. Before this change, the call failed with "Function runScriptJob not found in RStudio".

Ark sends a new UI comm request, execute_code_in_new_session, with the code and a session name. Positron starts a new console session of the caller's runtime and queues the code in it. The new session does not become the active console or the foreground session, and Positron does not open the Console view. The request returns the new session ID after the code is in the queue, and the console info popup shows a new start reason:

Screenshot 2026-11-01 at 3 48 09 PM

The R side uses setwd() in the job code and not ILanguageRuntimeSession.setWorkingDirectory(). I outlined why in posit-dev/ark#1460 but in short, setWorkingDirectory() for R does not wait for the result, it runs outside the console queue, and it does not change where .Rprofile and renv start.

Known limitations

  • The caller's console is busy until the new session starts and the code is in the queue. This can take some seconds, or actually a fair amount of time with an .Rprofile or renv.
  • If you interrupt the caller during this wait, R returns, but the job still starts.
  • If the console cannot send the code to the kernel after the request returns, the caller does not get an error. This is also true for all other code that goes through executeCode().
  • If you start a job from a notebook and no R console is open, Positron can also start its usual R console next to the job console.
  • I opened Sessions: one session start can clear another's entry, and a restart waits behind another start with no feedback #17041 to track two related problems in how the session service tracks starts.

Release Notes

New Features

Bug Fixes

  • A restart of an R or Python console now works when another console of the same interpreter is starting.

Validation Steps

@:console @:sessions @:plots @:ark

The unit tests cover the service changes:

npx vitest run src/vs/workbench/services/positronConsole/test/browser/ src/vs/workbench/services/runtimeSession/test/common/ src/vs/workbench/api/test/browser/positron/mainThreadLanguageRuntime.vitest.ts src/vs/workbench/api/test/common/positron/extHostMethods.vitest.ts

To do a manual test, make foo.R with Sys.sleep(10); print(getwd()) and plot.R with plot(1:10). Then:

  1. In the R console, run rstudioapi::jobRunScript("foo.R"). After a little bit, the call returns. A new console named foo.R shows in the session list, and the original console stays selected. You can type in the original console while the script runs. The new console prints the folder of the script.
  2. Run rstudioapi::jobRunScript("foo.R"); rstudioapi::jobRunScript("foo.R"). You get two new consoles.
  3. Run rstudioapi::jobRunScript("plot.R"). The plot shows in the Plots history, but the sidebar does not change its active tab, and focus stays in the console.
  4. Run rstudioapi::jobRunScript("foo.R"), and restart the original console while the new session starts. The original console restarts after the new session is ready.
  5. While a job console is open, make a Python console active. Then run R code from an R file in the editor. The code runs in the original R console, not in the job console.
  6. Run rstudioapi::jobRunScript("foo.R", workingDir = "no-such-dir"). You get an error in the original console, and no new console starts.
  7. Select a job console and open its console info popup. The start reason is "Code was sent to run in a new background console".
  8. For extra credit, with targets installed and a _targets.R project, run targets::tar_make(as_job = TRUE). A new console shows the pipeline output, and the original console stays free.

You can also use .ps.ui.executeCodeInNewSession("plot(sample(100, 10))", "Howdy!") directly.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:console @:sessions @:plots @:ark @:interpreter

Why these tags?
Tag Source
@:critical Always runs (required)
@:console PR description
@:sessions PR description
@:plots PR description
@:ark PR description
@:interpreter Changed files

More on automatic tags from changed files.

Test count: Linux: 352 tests across 2 shards.

readme  valid tags

@juliasilge
juliasilge marked this pull request as ready for review November 2, 2026 00:37
@juliasilge
juliasilge requested a review from jmcphers November 5, 2026 20:53
Comment thread positron/comms/ui-frontend-openrpc.json Outdated
{
"name": "execute_code_in_new_session",
"summary": "Execute code in a new session",
"description": "Start a new console session of the caller's runtime in the background, without making it the foreground session, and queue code in it. Returns once the code is queued. The code must be complete; it is run as-is without completeness checks.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this best modeled as a separate RPC? There are already cases where the existing execute_code will start a new session, and the existing method offers more control over the session's raise/focus behavior. What do you think about having a special "session id" value for the existing RPC that means "start a new session"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a very good question! I looked at this in detail one more time, and I do still think that a separate method is a better fit. Just for posterity about why:

  • The existing execute_code has no session ID parameter. It runs code in the console for the language, starting with the foreground console first, then the last active one. A special value would need a new session_id parameter that accepts only that one value.
  • In such a mode, most of the execute_code contract would actually work pretty differently. It would return no value, but the job needs the ID of the new session. session_name has no use for an existing session, allow_incomplete does not fit (because the job code needs to skip the completeness checks), the language server of the caller cannot reply while R waits for this request, and focus has no effect.
  • focus controls only the input focus. With focus: false, the target console still becomes the active console and the foreground session ,but the job type workflow needs a console that does not take the foreground, so a merged method needs a new parameter for this too.
  • When execute_code starts a session, it can join a console that is already starting, and in an untrusted workspace, it can run the code in the console of the caller. The job type workflow needs the opposite, with a new session every time and an error in an untrusted workspace.

If we find we need more control later, we can add optional parameters to the new method (for example, activate), which would not be a breaking change.

"description": "Start a new console session of the caller's runtime in the background, without making it the foreground session, and queue code in it. Returns once the code is queued. The code must be complete; it is run as-is without completeness checks.",
"params": [
{
"name": "language_id",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we keep this as-is, do we need to worry about the new session using a different runtime than the initiator, and thus lacking the context expected? (if so we might take a runtime id instead of a language id or have some code to align the initiating session)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, this is a good catch! If the calling session was not found or was for another language, the code used the preferred runtime and didn't tell anyone about it. I made some changes in f25e476, so now the request fails in these two cases and no session starts. The fallback to the preferred runtime now applies only when no session sent the request.

FWIW the same runtime does not give exactly the same context. The new session does not get the global variables of the caller (i.e. importEnv is not supported yet), the loaded packages, or the options. Also, the new session starts in the workspace folder before the setwd() call. Right now an .Rprofile or renv project in the working directory of the caller has no effect. IIUC this is the same as in RStudio, where a job also starts in a new R process, so not a huge problem for existing users.

@jmcphers

jmcphers commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator

This works great for me! A couple comments on the RPC design in the UI comm, up to you whether you want to refactor in any way.

juliasilge added a commit to posit-dev/ark that referenced this pull request Nov 7, 2026
- Addresses posit-dev/positron#6720
- Related to posit-dev/positron/pull/17042

`rstudioapi::jobRunScript()` now works in Positron. ark asks Positron to
start a new R console session in the background, and that session runs
the script with `source()`. The caller's console stays free while the
script runs. For example, `targets::tar_make(as_job = TRUE)` now runs
the pipeline in its own console.

Before this change, the call failed with "Function runScriptJob not
found in RStudio", because ark did not define `.rs.api.runScriptJob()`.

### Changes

- Add the `execute_code_in_new_session` frontend method to the UI comm.
`crates/amalthea/src/comm/ui_comm.rs` is generated from the Positron
contract.
- Add `.ps.ui.executeCodeInNewSession(code, sessionName)`. It parses
`code` first. If the code does not parse, it stops with an error in the
caller's session, and no session starts. It returns the ID of the new
session.
- Add `.rs.api.runScriptJob()` with the same arguments as in RStudio, so
that the `callFun()` of rstudioapi accepts it:
- `path`, `name`, `encoding`, and `workingDir` must each be a single
string.
- The script must exist. If `name` is `NULL` or empty, the session name
is the file name. If `workingDir` is `NULL`, the working directory is
the folder of the script. The working directory must exist.
- ark normalizes both paths in the caller's session, so relative paths
use the working directory of the caller.
- `importEnv = TRUE` and a non-empty `exportEnv` are not supported. Each
one gives a warning, and the job still runs.
  - The function returns the session ID invisibly.

The new session runs this code:

```r
{
    setwd("/path/to/working/dir")
    source("/path/to/script.R", encoding = "unknown")
}
```

ark makes each value with `deparse()`, so quotes and backslashes in
paths are safe. The braces make one expression. Positron can split
console input into statements and continue after an error. Without the
braces, a failed `setwd()` would let `source()` run in the wrong folder.

### Why not `setWorkingDirectory()`

I originally wanted to use
`ILanguageRuntimeSession.setWorkingDirectory()` from Positron, but it is
not a good fit here:

- For R, it sends `setwd()` directly to the kernel, outside the console
queue. It does not wait for the result, and it continues after an error.
The order of `setwd()` and `source()` running would then depend on
timing.
- It escapes only `\` and `"` while `deparse()` escapes all special
characters.
- The supervisor rejects it for kernels that do not implement it, such
as Python.
- It runs after the session starts, so `.Rprofile` and renv still use
the workspace folder. A real fix needs a working directory at session
start, and Positron does not have this option yet.

### Known limitations

- The caller's console is busy until the new session starts and the code
is in the queue. This takes some seconds, or even more with a
complicated `.Rprofile` or renv. ark needs the reply to return the
session ID and to report start errors.
- If the user interrupts the caller during this wait, R returns, but the
job still starts.
- The `.ps.ui.*` functions need an active execute request, so a call
from a `later::later()` callback fails. This limit is the same for all
`.ps.ui.*` functions currently.
- The other rstudioapi job functions (`jobAdd()`, `jobGetState()`, and
more) are not part of this change.

### Testing

- New integration tests in `crates/ark/tests/integration/rstudioapi.rs`
cover the request that ark sends (with a path that contains a space and
a quote), `name` and `workingDir`, relative paths, a missing file or
folder, bad argument types, the warnings, and code that does not parse.
Tests that call `rstudioapi::jobRunScript()` skip when rstudioapi is not
installed. Tests that call `.rs.api.runScriptJob()` directly always run.
- Run them with `cargo nextest run -p ark --test integration -E
'test(rstudioapi::)'`. They need nextest, because each test needs its
own R session.
- The expected paths remove the `\\?\` prefix that `canonicalize()` adds
on Windows. I did not run the tests on Windows, so so we'll want to
especially check the Windows CI result.
- To do a manual test in Positron (with the companion PR):
  1. Make `foo.R` with `Sys.sleep(10); print(getwd())`.
  2. In the R console, run `rstudioapi::jobRunScript("foo.R")`.
3. After some seconds, the call returns. A new console named `foo.R`
shows in the session list, and the original console stays selected. You
can type in the original console while the script runs, and the new
console prints the folder of the script.

### Positron Release Notes

#### New Features

- `rstudioapi::jobRunScript()` now runs the script in a new R console in
the background, so the current console stays free. As a result,
`targets::tar_make(as_job = TRUE)` now works (posit-dev/positron#6720).

#### Bug Fixes

- N/A
@juliasilge
juliasilge merged commit ceda307 into main Nov 7, 2026
20 of 25 checks passed
@juliasilge
juliasilge deleted the feat/jobrunscript-new-session branch November 7, 2026 02:19
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multisession console: add support to programmatically launch new console sessions

2 participants