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.
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:
Use the launch-positron command for that case.
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 electronDo not interrupt the script while it reports that it is running pre-launch.
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:
| Tool | Used by | Notes |
|---|---|---|
node, npx | all | the scripts call node_modules/.bin/playwright-cli directly and fall back to npx @playwright/cli |
curl | launch.sh, stop.sh | CDP readiness and liveness probes |
rsync or tar | launch.sh | rsync preferred; tar is the fallback, and is what Git Bash has |
jq | monaco-paste.sh, quickpick-enum.sh | not present in a bare Git Bash; install it separately |
sqlite3 | reseed.sh --list-keys | optional; only used to print the seeded storage keys |
cygpath | launch.sh on Windows | ships with Git Bash |
tasklist, powershell | launch.sh on Windows | liveness check and the WMI launch below |
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.
Run:
.claude/skills/drive-positron/scripts/launch.sh -- \
--folder-uri file:///private/tmp/myworkspaceWait for the script to print one JSON object. Record at least:
pidcdpPortrunDirlogFileThe launcher:
$POSITRON_DEV_USER_DATA_DIR or ~/.positron-dev, using rsync when present and tar otherwise;/tmp by default;--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.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.
| Argument | Purpose |
|---|---|
--folder-uri file:///private/tmp/myworkspace | Open 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 debug | Recommended. 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. |
You do not pass these, and should not need to think about them:
| Argument | Purpose |
|---|---|
--disable-workspace-trust | Prevent 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-keychain | Avoid using the per-user OS keychain from the disposable instance. A GitHubLoginFailed message in the log is expected. |
--skip-welcome | Keep the Welcome editor from receiving the initial focus. |
--shared-data-dir | Keep 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.
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.jsonThen 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/myworkspaceA 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-keysreseed.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.
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 snapshotDo 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 --hideconsole 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.
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.
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 positronIt 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.
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/.venvAllow 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.
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:
Read .agents/skills/launch/SKILL.md when you need:
dap-cli breakpoint instructions;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.
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-extensionsDo not rely on watch-extensions when another extension is already preventing the watch task from starting.
b0258bc
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.