CtrlK
BlogDocsLog inGet started
Tessl Logo

drive-positron

Launch Positron dev server in an isolated, disposable profile and control the Electron workbench. Use to reproduce UI bugs, verify UI changes, inspect the DOM, take screenshots, interact with the workbench, or attach a debugger without first writing an end-to-end test. Do not use to provide a persistent app instance for a person; use the launch-positron command for that. Only runs when a person invokes it explicitly.

SKILL.md
Quality
Evals
Security

Drive Positron through CDP

Use this skill to inspect and interact with a running development build of Positron. Treat the resulting profile and application state as disposable.

This workflow complements automated tests; it does not replace them. Add an appropriate test when the verified behavior needs regression coverage. See .claude/skills/author-e2e-tests.

Do not use this workflow to hand a persistent Positron instance to a person:

  • the profile is deleted during cleanup;
  • native file dialogs and modal message boxes are replaced with in-app equivalents;
  • no watch process recompiles subsequent source edits.

Use the launch-positron command for that case.

Know what this changes in your checkout

The disposable profile is isolated. The build state is not.

Before it starts the application, scripts/launch.sh runs build/lib/preLaunch.ts against your real checkout. Pre-launch writes to directories that your normal development build also uses:

  • .build/builtInExtensions/<name>: pre-launch deletes and re-downloads this directory for every built-in extension whose version on disk does not match product.json. A rebase that bumps a built-in extension version is enough to trigger it.
  • .build/electron: pre-launch deletes and re-downloads the whole directory when the installed Electron version does not match the expected one.
  • out/: pre-launch runs npm run compile when this directory is absent. That competes with the build daemons, which own compilation.

An interrupted or failed pre-launch can leave a built-in extension deleted or partially written. Your normal development build then fails to start until you repair it. To repair:

npm run download-builtin-extensions
npm run electron

Do not interrupt the script while it reports that it is running pre-launch.

Platform support

These scripts run on macOS, Linux, and Windows. On Windows, run them from Git Bash; they are bash scripts and will not work from PowerShell or cmd.

Tools they expect on PATH:

ToolUsed byNotes
node, npxallthe scripts call node_modules/.bin/playwright-cli directly and fall back to npx @playwright/cli
curllaunch.sh, stop.shCDP readiness and liveness probes
rsync or tarlaunch.shrsync preferred; tar is the fallback, and is what Git Bash has
jqmonaco-paste.sh, quickpick-enum.shnot present in a bare Git Bash; install it separately
sqlite3reseed.sh --list-keysoptional; only used to print the seeded storage keys
cygpathlaunch.sh on Windowsships with Git Bash
tasklist, powershelllaunch.sh on Windowsliveness check and the WMI launch below

Windows launches the app out-of-process on purpose

On Windows launch.sh does not spawn the app directly. It writes launch-app.cmd and launch-app.ps1 into the run directory and has WMI (Win32_Process.Create) start the app, which reparents it to WmiPrvSE while keeping it in the interactive session. Do not "simplify" this back to a direct background spawn.

The reason is Ark, the R kernel. It statically links a ZeroMQ built with the wepoll poller, which opens \Device\Afd directly, and that call fails for any process inside an agent session's process tree. A directly spawned app therefore starts, but every R session dies immediately with exit code 1073741845 and not a socket (...epoll.cpp:73). Python is unaffected, because its ZeroMQ uses the select poller -- so the symptom looks like an R-specific bug and is not.

macOS and Linux keep the plain background spawn: their ZeroMQ uses kqueue and kernel epoll, neither of which opens a device handle.

Launch Positron

Run:

.claude/skills/drive-positron/scripts/launch.sh -- \
	--folder-uri file:///private/tmp/myworkspace

Wait for the script to print one JSON object. Record at least:

  • pid
  • cdpPort
  • runDir
  • logFile

The launcher:

  • copies the source profile from $POSITRON_DEV_USER_DATA_DIR or ~/.positron-dev, using rsync when present and tar otherwise;
  • writes only to the disposable copy;
  • creates an isolated shared-data directory;
  • uses a short run directory under /tmp by default;
  • assigns unique ports for CDP and the debug endpoints;
  • converts the profile paths for the native binary on Windows;
  • waits for CDP and verifies that the app remains alive before returning;
  • keeps the renderer painting while the window is covered by passing Chromium's --disable-backgrounding-occluded-windows and --disable-renderer-backgrounding. Without them a fully occluded window stops producing frames, so every click and element screenshot times out on Playwright's stability check while keyboard input and eval still work.

Arguments to pass yourself

Everything before the -- configures the launcher; everything after it is handed to the app. Putting a launcher argument after the -- does not warn: the app ignores it and the launcher uses its default instead. Misplacing --source-user-data-dir this way copies the real ~/.positron-dev profile.

ArgumentPurpose
--folder-uri file:///private/tmp/myworkspaceOpen a workspace reliably. Do not pass a bare positional folder: Positron may discard it. On macOS, use the canonical /private/tmp path rather than /tmp. On Windows the URI needs a drive letter, so build it with cygpath -m: --folder-uri "file:///$(cygpath -m /tmp/myworkspace)".
--log debugRecommended. At the default level the [Runtime startup] Phase changed to ... lines are absent, so runtime startup, discovery, and cache replay cannot be told apart from the log.

Arguments the launcher supplies

You do not pass these, and should not need to think about them:

ArgumentPurpose
--disable-workspace-trustPrevent a modal trust dialog from blocking automation when the seed profile has no trust state. Without it the app starts in restricted mode with extensions disabled, so interpreter discovery never runs and an empty picker looks like a product bug.
--use-mock-keychainAvoid using the per-user OS keychain from the disposable instance. A GitHubLoginFailed message in the log is expected.
--skip-welcomeKeep the Welcome editor from receiving the initial focus.
--shared-data-dirKeep the disposable instance off the normal ~/.positron-shared store.

Repeating one of the first three after -- overrides the supplied copy. Pass --no-default-app-args before -- to launch without any of them, which is only useful when the scenario under test is one of the behaviors they suppress, such as the workspace trust prompt itself.

Protect the source profile

The launcher reads the source profile with a one-way rsync into the run directory. It does not use --delete, and it applies files.simpleDialog.enable and window.dialogStyle only to the disposable copy.

It excludes lock files, sockets, singleton state, caches, logs, and workspace storage so the copied profile can run alongside a normal development instance.

To avoid reading the normal development profile at all, create a minimal seed:

mkdir -p /tmp/positron-seed/User
echo '{"positron.notebook.enabled": true}' \
	> /tmp/positron-seed/User/settings.json

Then launch with it before the --, since it is a launcher argument:

.claude/skills/drive-positron/scripts/launch.sh \
	--source-user-data-dir /tmp/positron-seed -- \
	--folder-uri file:///private/tmp/myworkspace

Launch a second time with the state the first run wrote

A fresh profile only exercises the cold-start path. Anything that depends on state from a previous run -- the runtime discovery cache, storage-backed migrations, the recently opened list -- is untested by a single launch, so a bug that only appears on the second launch cannot be seen at all.

To carry the profile forward, stop the instance and turn its profile into a seed:

.claude/skills/drive-positron/scripts/reseed.sh \
	--run-dir "$RUN_DIR" --seed /tmp/positron-warm-seed \
	--cdp-port "$CDP_PORT" --list-keys

reseed.sh stops the instance without deleting its run directory, copies the profile database and settings into the seed, and prints the launch command for the warm run. The instance has to be stopped first: the running app holds User/globalStorage/state.vscdb open and a copy taken mid-write can be torn.

It leaves the run directory in place, so still remove it during cleanup.

Attach Playwright

Use a literal session name and reuse it for every command:

./node_modules/.bin/playwright-cli -s=positron \
	attach --cdp=http://127.0.0.1:"$CDP_PORT"

./node_modules/.bin/playwright-cli -s=positron snapshot

Do not derive the session name from $$. Separate shell invocations receive different process IDs and would silently create different sessions.

Run every command from the repository root, and call the binary directly rather than through npx. It is the same package npx resolves to there, but npx re-resolves it every invocation and costs about a second each time. From another working directory npx also installs its own copy, which keeps its sessions elsewhere and reports the attached session as The browser 'NAME' is not open.

Common operations:

./node_modules/.bin/playwright-cli -s=positron click e153
./node_modules/.bin/playwright-cli -s=positron click e980 right
./node_modules/.bin/playwright-cli -s=positron type "some text"
./node_modules/.bin/playwright-cli -s=positron press Enter
./node_modules/.bin/playwright-cli -s=positron resize 1600 1100
./node_modules/.bin/playwright-cli -s=positron eval '(() => document.title)()'
./node_modules/.bin/playwright-cli -s=positron console warning
./node_modules/.bin/playwright-cli -s=positron \
	screenshot --filename="$PWD/shots/01.png"

Use element references from the latest snapshot. Do not substitute screen coordinates, and use the positional right argument for a right-click.

click also takes a selector. Selectors must be unique, or Playwright's strict mode fails the step: button:has-text("Install uv") also matches a dropdown reading "Install uv to select a Python version", where .install-uv-button does not.

Snapshot a subtree, not the page: a bare snapshot renders the whole workbench, about 250 lines of YAML, where the ref of the dialog you are in returns a dozen. Once a flow's container has a ref, keep reusing it.

Filter a large snapshot rather than piping the whole thing through grep. To read the tree around a known control, find returns only the matching nodes and their context:

./node_modules/.bin/playwright-cli -s=positron find "Run Cell"

To capture a reference for a script, query the structured snapshot. Matching role and name avoids escaping a regexp over the YAML rendering:

R=$(./node_modules/.bin/playwright-cli -s=positron --json snapshot \
	| jq -r '.. | objects | select(.role == "button" and .name == "Run Cell") | .ref' \
	| head -1)

Take a screenshot early when the UI does not match expectations. Pass --hires when the detail being judged is finer than a CSS pixel. A screenshot often reveals blocking dialogs, an unopened workspace, missing kernels, or focus in the wrong editor faster than DOM inspection.

To make a screenshot point at one control rather than leaving the reader to hunt for it in a full workbench, draw an overlay on the element first. highlight --hide clears every overlay on the page:

./node_modules/.bin/playwright-cli -s=positron highlight e153
./node_modules/.bin/playwright-cli -s=positron \
	screenshot --hires --filename="$PWD/shots/01.png"
./node_modules/.bin/playwright-cli -s=positron highlight --hide

console reads the renderer console, which is a separate source from the log file the launcher reports as logFile. It takes a minimum level and defaults to info.

Enter text in Monaco

Do not use type or fill for notebook cell editors or chat inputs backed by Monaco. Use:

.claude/skills/drive-positron/scripts/monaco-paste.sh \
	--session positron "text to insert"

Use individual press operations when testing actual keyboard handling.

Read a whole quick pick

Do not count .monaco-list-row elements and do not set scrollTop. Quick picks render only a window of rows and move it with a transform, so both report a short list without failing. Use:

.claude/skills/drive-positron/scripts/quickpick-enum.sh --session positron

It walks the picker with ArrowDown and prints index|kind|label|description|detail|active for every row, headings included, leaving the picker on the item it started from.

Read references/reading-ui-state.md before trusting any other reading of a list, tree, or quick input widget. It covers the virtualization, the hidden widgets left behind by closed pickers, and how separators are rendered.

Account for Positron behavior

  • Restore focus to the notebook before invoking a notebook action. Opening an output in a plot tab moves focus away from the notebook.

  • Ensure the selected Python environment contains the packages the scenario needs. A fresh profile may discover a bare interpreter without packages such as matplotlib or pandas.

  • To prioritize Positron's development environment, expose it as the workspace environment:

    ln -s <repo>/extensions/positron-python/.venv \
      /private/tmp/myworkspace/.venv
  • Allow interpreter discovery and marketplace extension installation to finish before concluding that a kernel is unavailable.

  • Set positron.notebook.enabled to true in the workspace or seed profile when testing the Positron notebook editor.

  • Modal message boxes are clickable because the launcher forces window.dialogStyle: "custom". Without it Electron draws a native dialog that CDP can neither see nor dismiss, and the blocked renderer looks like a hung app. Judge such a dialog's wording from this path but not its appearance; a real user sees the native one.

  • Two things are called a modal. .positron-modal-dialog-box, which the Modals page object matches, is Positron's own React modal such as the New Folder flow. A showInformationMessage(..., { modal: true }) raised from inside it is the upstream .monaco-dialog-box, which that page object will not find.

  • Expect selectors to change. Prefer the maintained page objects under test/e2e/pages/ when locating Positron controls; otherwise take a fresh snapshot.

Use upstream debugging guidance

scripts/launch.sh is a maintained fork of .agents/skills/launch/scripts/launch.sh. It does not inherit upstream fixes.

When the upstream script changes:

  1. Compare it with this fork.
  2. Port applicable fixes without removing the Positron-specific profile, path, isolation, and liveness behavior.
  3. Revalidate the launch workflow.

Read .agents/skills/launch/SKILL.md when you need:

  • the debug-port mapping;
  • dap-cli breakpoint instructions;
  • parallel-instance guidance;
  • additional Monaco input details.

Clean up

Always stop the disposable instance; Positron can retain several gigabytes of memory.

./node_modules/.bin/playwright-cli -s=positron close
.claude/skills/drive-positron/scripts/stop.sh \
	--cdp-port "$CDP_PORT" --run-dir "$RUN_DIR"

Run stop.sh in its own command, after you have confirmed that every screenshot or file you need exists. A command that fails earlier in the same chain, such as an element screenshot that times out, cannot be retried once the instance is gone.

stop.sh signals the process that owns the CDP port, waits for the port to stop answering, forces the stop if it does not, and then removes the run directory. It exits non-zero if the instance is still reachable, so a silent failure to clean up is not possible.

Do not signal the Electron helper processes yourself. Positron reads a terminated renderer as a window crash: it respawns the helpers, shows a "window terminated unexpectedly" dialog, and then ignores the signal sent to the main process, leaving the instance running.

Pass --run-dir only when it is the exact runDir the launcher reported. The script refuses any path that does not contain a generated positron-dev-launch component, and stops the instance without deleting anything when --run-dir is omitted.

Do not use kill "$PID" on its own. On Windows the reported pid belongs to the MSYS shell that exec'd the native Electron binary, so killing it can leave the application running. stop.sh locates the real process through the CDP port on every platform.

Remove .playwright-cli only if it is the session directory created in the intended workspace.

To confirm independently that no process remains, check that the CDP port no longer answers:

curl -sf -o /dev/null "http://127.0.0.1:$CDP_PORT/json/version" \
	&& echo "still running" || echo "stopped"

This works on every platform. pgrep -f "remote-debugging-port=$CDP_PORT" is equivalent on macOS and Linux, but pgrep is absent from Git Bash on Windows.

Troubleshoot failures

  • Attach reports connect ECONNREFUSED: The application exited after opening CDP. Inspect the path reported as logFile.

  • The log reports listen EINVAL or an IPC path longer than 103 characters: The run-directory base is too long. Unset $POSITRON_LAUNCH_TMP or point it at a shorter directory. This affects macOS and Linux only; Windows uses named pipes and has no such limit.

  • rsync: command not found (Windows): You are on an older copy of launch.sh. The current script falls back to tar when rsync is absent.

  • A command reports an error: The CLI exits non-zero on a failed command, so check the exit status rather than matching on its output.

  • Snapshot references disappear: Look for a modal dialog with a screenshot, then take a new snapshot.

  • A built-in extension does not load: Compile extensions with:

    npm run gulp compile-extensions

    Do not rely on watch-extensions when another extension is already preventing the watch task from starting.

Repository
posit-dev/positron
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.