Add code-oss-dev skill (#317811)

* Add code-oss-dev skill

* Copy over launch skill

* Address launch skill review feedback

(Written by Copilot)
This commit is contained in:
Rob Lourens
2026-05-21 19:12:53 -07:00
committed by GitHub
parent 05794df8bd
commit 4ffd5897be
2 changed files with 367 additions and 324 deletions

View File

@@ -1,401 +1,273 @@
---
name: launch
description: "Launch and automate VS Code (Code OSS) using @playwright/cli via Chrome DevTools Protocol. Use when you need to interact with the VS Code UI, automate the chat panel, test UI features, or take screenshots of VS Code. Triggers include 'automate VS Code', 'interact with chat', 'test the UI', 'take a screenshot', 'launch Code OSS with debugging'."
metadata:
allowed-tools: Bash(npx @playwright/cli:*)
description: "Launch Code OSS (VS Code from sources) into an isolated throwaway profile with unique debug ports so you can drive it with @playwright/cli AND attach a Node debugger via dap-cli in the same session. Use when working on VS Code itself and you want to interact with the running workbench, automate chat or UI flows, test UI features, take screenshots, set breakpoints in the renderer / extension host / main process, or combine UI driving with debugging."
---
# VS Code Automation
# Code OSS Dev - Launch + Debug
Automate VS Code (Code OSS) using `@playwright/cli`. VS Code is built on Electron/Chromium and exposes a Chrome DevTools Protocol (CDP) port that `@playwright/cli` can attach to, enabling the same snapshot-interact workflow used for web pages.
You're working on VS Code itself and you want to:
1. Launch a Code OSS build from sources that is **already signed in** (Copilot, GitHub, etc.) so chat / agent flows work end-to-end.
2. Drive it with `@playwright/cli` over CDP (UI automation).
3. Optionally attach a debugger via **dap-cli** to set breakpoints in the renderer, extension host, or main process.
4. Run multiple instances at once without port conflicts.
This skill provides a launcher that clones an authenticated user-data-dir to a throwaway temp folder, picks free ports for every debug surface, and prints them as JSON so you can pick them up programmatically.
The clone is **slim**: workspace storage, browser caches, file history, cached VSIX backups, and old logs are excluded by default. Auth tokens themselves live in the OS keychain (shared automatically) plus small files inside `User/globalStorage` - both of which *are* preserved.
## Prerequisites
- **`@playwright/cli` is available via devDependencies.** Run `npm install` at the repo root, then use `npx @playwright/cli` to invoke commands. Alternatively, install globally with `npm install -g @playwright/cli`.
- **For Code OSS (VS Code dev build):** The repo must be built before launching. `./scripts/code.sh` runs the build automatically if needed, or set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built.
- **CSS selectors are internal implementation details.** Selectors like `.interactive-input-part`, `.interactive-input-editor`, and `.part.auxiliarybar` used in `eval` commands are VS Code internals that may change across versions. If they stop working, use `npx @playwright/cli snapshot` to re-discover the current DOM structure.
- macOS or Linux. The launcher is a bash script and depends on `rsync`, `lsof`, `jq`, `nohup`, and Node on `PATH`.
- A VS Code checkout with sources built. Run `npm run watch` in another terminal (or rely on `./scripts/code.sh` to compile the first time).
- An **authenticated** Code OSS profile to seed from. By default the launcher uses `~/.vscode-oss-dev`, which is the user-data-dir the repo's `launch.json` configs use - if the user has ever signed in to Copilot in a dev build, this should work. Only pass `--source-user-data-dir <path>` (or set `$CODE_OSS_DEV_AUTHED_USER_DATA_DIR`) when you specifically want to seed from a different profile (e.g. your regular `~/Library/Application Support/Code` install).
- `@playwright/cli` available (it's a devDependency in the vscode repo - `npm install` then use `npx @playwright/cli`).
- For debugger work: `dap-cli` on `PATH`. If debugger support would be useful but the `dap-cli` skill is not present, prompt the user to install it from https://github.com/roblourens/dap-cli.
- CSS selectors are internal implementation details. If a selector-based `eval` stops working, take a fresh `snapshot`, inspect the current DOM, and update the selector rather than assuming an old one still applies.
## Core Workflow
> The launcher **copies** the source profile to a temp dir and never mutates the original. Each launch gets its own isolated `--user-data-dir` and `--extensions-dir`.
1. **Launch** Code OSS with remote debugging enabled
2. **Attach** npx @playwright/cli to the CDP port
3. **Snapshot** to discover interactive elements
4. **Interact** using element refs
5. **Re-snapshot** after navigation or state changes
## Launch
> **📸 Take screenshots for a paper trail.** Use `npx @playwright/cli screenshot --filename=<path>` at key moments — after launch, before/after interactions, and when something goes wrong. Screenshots provide visual proof of what the UI looked like and are invaluable for debugging failures or documenting what was accomplished.
>
> Save screenshots inside a timestamped subfolder so each run is isolated and nothing gets overwritten:
>
> ```bash
> # Create a timestamped folder for this run's screenshots
> SCREENSHOT_DIR="/tmp/code-oss-screenshots/$(date +%Y-%m-%dT%H-%M-%S)"
> mkdir -p "$SCREENSHOT_DIR"
>
> # Save a screenshot
> npx @playwright/cli screenshot --filename="$SCREENSHOT_DIR/after-launch.png"
> ```
The launcher script lives next to this SKILL.md at `scripts/launch.sh`. Resolve it relative to wherever this skill file is installed - do not hardcode an absolute path.
```bash
# Launch Code OSS with remote debugging
# NOTE: unset ELECTRON_RUN_AS_NODE if running from a Claude Code terminal
unset ELECTRON_RUN_AS_NODE
./scripts/code.sh --remote-debugging-port=9224
# LAUNCH=<dir-of-this-SKILL.md>/scripts/launch.sh
"$LAUNCH" # default: workbench
"$LAUNCH" --agents # Agents window
"$LAUNCH" -- <workspace-path> # forward extra args to code.sh
"$LAUNCH" --source-user-data-dir <path> # pick a specific authed profile
"$LAUNCH" --repo <vscode-repo-root> # if not run from the repo
"$LAUNCH" --clone-extensions # start with a copy of the source extensions/ (~few seconds)
"$LAUNCH" --full # skip slim excludes; copy everything
```
### What gets copied (slim mode, the default)
The exclude list mirrors the one used by VS Code's own perf-test skill (`.github/skills/auto-perf-optimize`), which is known to keep Copilot auth and language-model availability working. Specifically `WebStorage/`, `Service Worker/`, `Local Storage/`, `Cookies`, `Network Persistent State`, `TransportSecurity`, `Trust Tokens`, `Preferences`, `machineid`, and the entire `User/globalStorage/` (which holds `state.vscdb` - where extension `SecretStorage` blobs live, encrypted with the OS keychain key) are all preserved. Auth tokens themselves stay in the OS keychain, which is per-user, so they follow automatically.
Excluded (transient, regenerable, or known-not-needed):
- `User/workspaceStorage/` - per-workspace state, **including stored chat sessions** (often multi-GB)
- `User/History/` - local file edit history
- `CachedExtensionVSIXs` - backup VSIXs (hundreds of MB)
- `logs`
- Chromium caches: `Cache`, `Code Cache`, `CachedData`, `GPUCache`, `ShaderCache`, `Dawn*Cache`, `component_crx_cache`
- `Backups`, `blob_storage`, `BrowserMetrics`, `Crashpad`, `Session Storage`
- `Singleton*`, `*.lock`, `*.sock` (would conflict with the source instance)
`extensions/` defaults to a **fresh empty directory** - fastest and conflict-free, but the launched instance starts with no third-party extensions installed. Pass `--clone-extensions` to copy the source extensions dir into the temp profile so the new instance is independent of the source. Pass `--full` to skip all excludes if you suspect the slim copy is missing something you need.
> **Why never share the source `extensions/` dir directly?** The extension management service writes a shared `.obsolete` file; two concurrent writers crash each other's shared background process. The launcher always uses an isolated extensions dir for the same reason it uses `--shared-data-dir` (see below).
> If the launched window says "language model unavailable" or otherwise looks unauthed, ask the user to sign in.
The script prints one JSON line on stdout (logs go to stderr):
```json
{"pid":12345,"cdpPort":53111,"extHostPort":53112,"mainPort":53113,"agentHostPort":53114,"userDataDir":".../user-data","extensionsDir":".../extensions","sharedDataDir":".../shared-data","runDir":"...","logFile":".../code.log","repo":"...","agents":false}
```
Capture it with `jq`:
```bash
INFO=$("$LAUNCH" | tail -n1)
CDP=$(jq -r .cdpPort <<<"$INFO")
EXT=$(jq -r .extHostPort <<<"$INFO")
MAIN=$(jq -r .mainPort <<<"$INFO")
AGENT=$(jq -r .agentHostPort <<<"$INFO")
LOG=$(jq -r .logFile <<<"$INFO")
PID=$(jq -r .pid <<<"$INFO")
```
### What each port is for
| Port | Process | Use with |
|------|---------|----------|
| `cdpPort` (`--remote-debugging-port`) | Renderer (the workbench window) | `@playwright/cli` over CDP, also Chrome DevTools |
| `extHostPort` (`--inspect-extensions`) | Extension host (Node) | `dap-cli` (Node inspector protocol) |
| `mainPort` (`--inspect`) | Electron main process (Node) | `dap-cli` (Node inspector protocol) |
| `agentHostPort` (`--inspect-agenthost`) | Agent host process (Node) | `dap-cli` (Node inspector protocol) |
## Drive the UI with @playwright/cli
Use the dynamic `cdpPort` from the launch JSON. The normal loop is: attach, confirm the target, snapshot, interact, then re-snapshot after meaningful UI changes.
```bash
# Wait for Code OSS to start, retry until attached
for i in 1 2 3 4 5; do npx @playwright/cli attach --cdp=http://127.0.0.1:9224 2>/dev/null && break || sleep 3; done
for i in 1 2 3 4 5; do
npx @playwright/cli attach --cdp=http://127.0.0.1:$CDP 2>/dev/null && break || sleep 3
done
# Verify you're connected to the right target (not about:blank)
# If `tab-list` shows the wrong target, run `npx @playwright/cli close` and reattach
npx @playwright/cli tab-list
# Discover UI elements
npx @playwright/cli snapshot
# Focus the chat input (macOS)
npx @playwright/cli press Control+Meta+i
```
## Attaching
After `attach`, later `@playwright/cli` commands keep using the connected app until you close or reattach.
### Selecting the right Electron target
Electron apps can expose multiple windows or webviews. If `tab-list` shows `about:blank`, a webview, or otherwise the wrong target, switch targets before interacting:
```bash
# Attach to a specific CDP port
npx @playwright/cli attach --cdp=http://127.0.0.1:9222
```
After `attach`, all subsequent commands target the connected app without needing to reattach.
## Tab Management
Electron apps often have multiple windows or webviews. Use tab commands to list and switch between them:
```bash
# List all available targets (windows, webviews, etc.)
npx @playwright/cli tab-list
# Switch to a specific tab by index
npx @playwright/cli tab-select 2
```
## Launching Code OSS (VS Code Dev Build)
The VS Code repository includes `scripts/code.sh` which launches Code OSS from source. It passes all arguments through to the Electron binary, so `--remote-debugging-port` works directly:
```bash
cd <repo-root> # the root of your VS Code checkout
./scripts/code.sh --remote-debugging-port=9224
```
Wait for the window to fully initialize, then attach:
```bash
# Wait for Code OSS to start, retry until attached
for i in 1 2 3 4 5; do npx @playwright/cli attach --cdp=http://127.0.0.1:9224 2>/dev/null && break || sleep 3; done
# Verify you're connected to the right target (not about:blank)
# If `tab-list` shows the wrong target, run `npx @playwright/cli close` and reattach
npx @playwright/cli tab-list
npx @playwright/cli snapshot
```
**Tips:**
- Unset `ELECTRON_RUN_AS_NODE` before launching if it's set in your shell (e.g., in Claude Code terminals): `unset ELECTRON_RUN_AS_NODE && ./scripts/code.sh --remote-debugging-port=9224`
- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built: `VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh --remote-debugging-port=9224` (from the repo root)
- Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running.
- If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance.
If a target looks stale after relaunching, run `npx @playwright/cli close`, attach again with `$CDP`, and re-check `tab-list`.
## Launching the Agents App (Agents Window)
The Agents app is a separate workbench mode launched with the `--agents` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance.
### Focusing the chat input (works on Code OSS, including the Agents window)
```bash
cd <repo-root> # the root of your VS Code checkout
./scripts/code.sh --agents --remote-debugging-port=9224
```
Wait for the window to fully initialize, then attach:
```bash
# Wait for Agents app to start, retry until attached
for i in 1 2 3 4 5; do npx @playwright/cli attach --cdp=http://127.0.0.1:9224 2>/dev/null && break || sleep 3; done
# Verify you're connected to the right target (not about:blank)
npx @playwright/cli tab-list
npx @playwright/cli snapshot
```
**Tips:**
- The `--agents` flag launches the Agents workbench instead of the standard VS Code workbench.
- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built.
> **🚨 Driving the Agents window UI is not the same as driving regular Code OSS.**
> The session sidebar, agent picker, and chat input have their own quirks that
> trip up the obvious `click + type` patterns. Before you spend time fighting
> the UI, read **[references/agents-window-guide.md](./references/agents-window-guide.md)**
> — it covers the agent picker (Copilot CLI vs Claude), `[Local]` vs non-`[Local]`
> sessions, the reliable keyboard-navigation trick for the agent dropdown, how
> to send a follow-up turn in the same session, how to restore an existing
> chat, and the kill / relaunch / verify-out-is-fresh dance you need after a
> source code change.
## Launching VS Code Extensions for Debugging
To debug a VS Code extension via npx @playwright/cli, launch VS Code Insiders with `--extensionDevelopmentPath` and `--remote-debugging-port`. Use `--user-data-dir` to avoid conflicting with an already-running instance.
```bash
# Build the extension first
cd <extension-repo-root> # e.g., the root of your extension checkout
npm run compile
# Launch VS Code Insiders with the extension and CDP
code-insiders \
--extensionDevelopmentPath="<extension-repo-root>" \
--remote-debugging-port=9223 \
--user-data-dir=/tmp/vscode-ext-debug
# Wait for VS Code to start, retry until attached
for i in 1 2 3 4 5; do npx @playwright/cli attach --cdp=http://127.0.0.1:9223 2>/dev/null && break || sleep 3; done
# Verify you're connected to the right target (not about:blank)
# If `tab-list` shows the wrong target, run `npx @playwright/cli close` and reattach
npx @playwright/cli tab-list
npx @playwright/cli snapshot
```
**Key flags:**
- `--extensionDevelopmentPath=<path>` — loads your extension from source (must be compiled first)
- `--remote-debugging-port=9223` — enables CDP (use 9223 to avoid conflicts with other apps on 9222)
- `--user-data-dir=<path>` — uses a separate profile so it starts a new process instead of sending to an existing VS Code instance
**Without `--user-data-dir`**, VS Code detects the running instance, forwards the args to it, and exits immediately — you'll see "Sent env to running instance. Terminating..." and CDP never starts.
## Restarting After Code Changes
**After making changes to Code OSS source code, you must restart to pick up the new build.** The workbench loads the compiled JavaScript at startup — changes are not hot-reloaded.
### Restart Workflow
1. **Rebuild** the changed code
2. **Kill** the running Code OSS instance
3. **Relaunch** with the same flags
```bash
# 1. Ensure your build is up to date.
# Normally you can skip a manual step here and let ./scripts/code.sh in step 3
# trigger the build when needed (or run `npm run watch` in another terminal).
# 2. Kill the Code OSS instance listening on the debug port (if running)
pids=$(lsof -t -i :9224)
if [ -n "$pids" ]; then
kill $pids
fi
# 3. Relaunch
./scripts/code.sh --remote-debugging-port=9224
# 4. Reattach npx @playwright/cli
for i in 1 2 3 4 5; do npx @playwright/cli attach --cdp=http://127.0.0.1:9224 2>/dev/null && break || sleep 3; done
npx @playwright/cli tab-list
npx @playwright/cli snapshot
```
> **Tip:** If you're iterating frequently, run `npm run watch` in a separate terminal so compilation happens automatically. You still need to kill and relaunch Code OSS to load the new build.
## Interacting with Monaco Editor (Chat Input, Code Editors)
VS Code uses Monaco Editor for all text inputs including the Copilot Chat input. Monaco editors require specific npx @playwright/cli techniques — standard `click`, `fill`, and `type` commands may not work depending on the VS Code build.
### The Universal Pattern: Focus via Keyboard Shortcut + `press`
This works on **all** VS Code builds (Code OSS, Insiders, stable):
```bash
# 1. Open and focus the chat input with the keyboard shortcut
# macOS:
# macOS
npx @playwright/cli press Control+Meta+i
# Linux / Windows:
# Linux / Windows
npx @playwright/cli press Control+Alt+i
# 2. Type using individual press commands
npx @playwright/cli press H
npx @playwright/cli press e
npx @playwright/cli press l
npx @playwright/cli press l
npx @playwright/cli press o
npx @playwright/cli press Space # Use "Space" for spaces
npx @playwright/cli press w
npx @playwright/cli press o
npx @playwright/cli press r
npx @playwright/cli press l
npx @playwright/cli press d
# Verify text appeared (optional)
npx @playwright/cli eval '
(() => {
const sidebar = document.querySelector(".part.auxiliarybar");
const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line");
return Array.from(viewLines).map(vl => vl.textContent).join("|");
})()'
# 3. Send the message (same on all platforms)
npx @playwright/cli press Enter
```
**Chat focus shortcut by platform:**
- **macOS:** `Ctrl+Cmd+I``npx @playwright/cli press Control+Meta+i`
- **Linux:** `Ctrl+Alt+I``npx @playwright/cli press Control+Alt+i`
- **Windows:** `Ctrl+Alt+I``npx @playwright/cli press Control+Alt+i`
### Typing into Monaco (chat input, editors)
This shortcut focuses the chat input and sets `document.activeElement` to a `DIV` with class `native-edit-context` — VS Code's native text editing surface that correctly processes key events from `npx @playwright/cli press`.
`fill` and `type` **silently fail** on Code OSS. Prefer focus-via-shortcut plus either per-key `press` or clipboard paste, and verify text when the scenario depends on the exact prompt.
### `fill <ref>` — Works on Some Builds
- **Per-key `press`** (universal but slow):
```bash
npx @playwright/cli press H
npx @playwright/cli press i
npx @playwright/cli press Enter
```
- **Clipboard paste** (fast, macOS):
```bash
printf '%s' "Your prompt here" | pbcopy
npx @playwright/cli press Control+Meta+i # focus chat input
npx @playwright/cli press Meta+v # paste
npx @playwright/cli press Enter
```
On VS Code Insiders (extension debug mode), `fill` handles focus and input in one step:
The focus shortcut should leave `document.activeElement` on VS Code's `native-edit-context` editing surface. That is a useful sanity check when key presses appear to do nothing.
```bash
npx @playwright/cli snapshot
# Look for: textbox "The editor is not accessible..." [ref=e62]
npx @playwright/cli fill e62 "Hello from George!"
```
### Agents window selector differences
> **Tip:** If `fill` silently drops text (the editor stays empty), the ref may be stale or the editor not yet ready. Re-snapshot to get a fresh ref and try again. You can verify text was entered using the snippet in "Verifying Text and Clearing" below.
However, **`fill` silently fails on Code OSS** — the command completes without error but no text appears. Always verify text appeared after typing, and fall back to the keyboard shortcut + `press` pattern if it didn't. The `press`-per-key approach works universally across all builds.
### Fastest Method on Code OSS / Agents Window: Clipboard Paste (macOS only)
For prompts longer than ~20 characters, `press`-per-key is tediously slow. On macOS, paste via the system clipboard:
```bash
PROMPT="Edit sample.txt and replace \"hello world\" with \"hello phase 8\". Use the Edit tool."
printf '%s' "$PROMPT" | pbcopy
npx @playwright/cli press Control+Meta+i # focus chat input
npx @playwright/cli press Meta+v # paste
npx @playwright/cli press Enter # submit
```
This is reliable on both the regular workbench and the **Agents window** (where the chat input editor doesn't have an `.interactive-input-editor` wrapper — see Selector Notes below). Linux / Windows equivalents use `xclip` / `clip.exe` and `Control+v`; not validated here.
### Selector Notes — Agents Window
The Agents window (`./scripts/code.sh --agents`) does NOT use the `.interactive-input-editor` wrapper that the regular workbench uses. Selectors that work in the workbench but FAIL in the Agents window:
The Agents window does not use the regular workbench `.interactive-input-editor` wrapper. Selector checks that are scoped to that wrapper may return nothing even when the Agents chat input is focused.
```js
// ❌ Returns 0 elements in the Agents window
document.querySelectorAll(".interactive-input-editor .view-line")
// Regular-workbench-specific selector; do not assume this exists in Agents.
document.querySelectorAll('.interactive-input-editor .view-line')
// ✅ Use one of these instead
document.querySelectorAll(".view-line") // unscoped
document.activeElement?.className === "native-edit-context" // focus check
// More useful checks in Agents.
document.querySelectorAll('.view-line')
document.activeElement?.className === 'native-edit-context'
```
The `Control+Meta+i` focus shortcut still works; only the post-focus DOM selectors differ.
The `Control+Meta+i` / `Control+Alt+i` focus shortcut still works; only the DOM shape after focus differs.
### Compatibility Matrix
### Verifying and clearing chat text
| Method | VS Code Insiders | Code OSS |
|--------|-----------------|----------|
| `press` per key (after focus shortcut) | ✅ Works | ✅ Works |
| `pbcopy` + `Meta+v` paste (macOS) | ✅ Works | ✅ Works (incl. Agents window) |
| `fill <ref> "text"` | ✅ Works | ❌ Silent fail |
| `type "text"` (after focus) | ✅ Works | ❌ Silent fail |
| `click <ref>` on editor | ❌ Blocked by overlay | ❌ Blocked by overlay |
### Fallback: Focus via JavaScript Mouse Events
If the keyboard shortcut doesn't work (e.g., chat panel isn't configured), you can focus the editor via JavaScript:
For the regular workbench sidebar, this confirms that text landed in the Monaco input:
```bash
npx @playwright/cli eval '
(() => {
const inputPart = document.querySelector(".interactive-input-part");
const editor = inputPart.querySelector(".monaco-editor");
const rect = editor.getBoundingClientRect();
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
editor.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: x, clientY: y }));
editor.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: x, clientY: y }));
editor.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: x, clientY: y }));
return "activeElement: " + document.activeElement?.className;
})()'
# Then use press for each character
npx @playwright/cli press H
npx @playwright/cli press e
# ...
```
### Verifying Text and Clearing
```bash
# Verify text in the chat input
npx @playwright/cli eval '
(() => {
const sidebar = document.querySelector(".part.auxiliarybar");
const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line");
return Array.from(viewLines).map(vl => vl.textContent).join("|");
const viewLines = sidebar?.querySelectorAll(".interactive-input-editor .view-line") ?? [];
return Array.from(viewLines).map(viewLine => viewLine.textContent).join("|");
})()'
```
# Clear the input (Select All + Backspace)
# macOS:
For the Agents window, use a fresh snapshot plus the broader selector/focus checks above instead of assuming the regular sidebar wrapper is present.
To clear the focused Monaco input:
```bash
# macOS
npx @playwright/cli press Meta+a
# Linux / Windows:
# Linux / Windows
npx @playwright/cli press Control+a
# Then delete:
npx @playwright/cli press Backspace
```
### Screenshot Tips for VS Code
If the keyboard shortcut cannot focus chat because the surface is not available yet, take a snapshot and navigate the UI into a state where chat exists before retrying. Avoid treating completed CLI commands as proof that text was entered.
On ultrawide monitors, the chat sidebar may be in the far-right corner of the CDP screenshot. Options:
- Use `npx @playwright/cli screenshot --full-page` to capture the entire window
- Use element screenshots: `npx @playwright/cli screenshot <ref> --filename=sidebar.png` (get the ref from a snapshot)
- Maximize the sidebar first: click the "Maximize Secondary Side Bar" button
### Screenshots (paper trail)
> **macOS:** If `npx @playwright/cli screenshot` returns "Permission denied", your terminal needs Screen Recording permission. Grant it in **System Settings → Privacy & Security → Screen Recording**. As a fallback, use the `eval` verification snippet to confirm text was entered — this doesn't require screen permissions.
```bash
SHOTS="$PWD/screenshots/$(date +%Y-%m-%dT%H-%M-%S)"
mkdir -p "$SHOTS"
npx @playwright/cli screenshot --filename="$SHOTS/after-launch.png"
```
## Troubleshooting
> Keep screenshots inside the workspace, not `/tmp`, so they survive for review.
### "Connection refused" or "Cannot connect"
For wide windows, `--full-page` can make layout easier to inspect, and element screenshots are useful when a snapshot gives a stable ref for the panel you care about:
- Make sure Code OSS was launched with `--remote-debugging-port=NNNN`
- If Code OSS was already running, quit and relaunch with the flag
- Check that the port isn't in use by another process:
- macOS / Linux: `lsof -i :9224`
- Windows: `netstat -ano | findstr 9224`
```bash
npx @playwright/cli screenshot --full-page --filename="$SHOTS/full-window.png"
npx @playwright/cli screenshot e42 --filename="$SHOTS/panel.png"
```
### Elements not appearing in snapshot
On macOS, a screenshot "Permission denied" failure usually means the terminal lacks Screen Recording permission. Use text/state verification while resolving that permission issue.
- VS Code uses multiple webviews. Use `npx @playwright/cli tab-list` to list targets and switch to the right one with `npx @playwright/cli tab-select <index>`
## Debug with dap-cli
### Cannot type in Monaco Editor inputs
To set breakpoints in VS Code source while the window is running, attach `dap-cli` to one of the ports. If `dap-cli` would help but the corresponding skill is unavailable, prompt the user to install it from https://github.com/roblourens/dap-cli before continuing with debugger-specific steps.
- Use `npx @playwright/cli press` for individual keystrokes after focusing the input. Focus the chat input with the keyboard shortcut (macOS: `Ctrl+Cmd+I`, Linux/Windows: `Ctrl+Alt+I`).
- `fill` and `type` work on VS Code Insiders but **silently fail on Code OSS** — they complete without error but no text appears. The `press`-per-key approach works universally.
- See the "Interacting with Monaco Editor" section above for the full compatibility matrix.
**Read the `dap-cli` skill for the full attach/breakpoint/inspect workflow when it is available** - this skill only tells you which port to point it at:
- **Extension host** (most common - Copilot Chat extension, built-in extensions, your own extension under development) -> `extHostPort`
- **Main process** (Electron lifecycle, window/menu wiring, IPC) -> `mainPort`
- **Local agent host** (`src/vs/platform/agentHost/node/...`, agent session lifecycle, AHP wiring, Claude/Copilot agent providers) -> `agentHostPort`
- **Renderer** (the workbench itself, `src/vs/workbench/...`) -> `cdpPort`
You can run `@playwright/cli` and `dap-cli` against the **same window simultaneously** - drive the UI with one terminal, hit a breakpoint and inspect state in another.
## Multiple instances
Every launch picks fresh ports and a fresh temp `runDir`, so you can run as many concurrent Code OSS windows as your machine can handle. Each one's ports come back in its own JSON blob - keep them separate.
The launcher also passes `--shared-data-dir=<runDir>/shared-data`. This is **required** for multi-instance isolation: Code OSS keeps a fixed-path SQLite DB at `~/.<dataFolderName>-shared/sharedStorage/state.vscdb` that is *not* covered by `--user-data-dir`. Without overriding it, two concurrent instances would fight over the same file and one would die with "shared background process terminated unexpectedly". Each launch gets its own `shared-data` dir.
## Restart after source changes
Workbench code is loaded when the Code OSS window starts; source changes are not hot-reloaded into an already-running instance. After the build output is current, kill the launched process, launch again, and reattach to the new `cdpPort` from the new JSON blob.
```bash
kill "$PID" 2>/dev/null || true
INFO=$("$LAUNCH" | tail -n1)
CDP=$(jq -r .cdpPort <<<"$INFO")
PID=$(jq -r .pid <<<"$INFO")
for i in 1 2 3 4 5; do
npx @playwright/cli attach --cdp=http://127.0.0.1:$CDP 2>/dev/null && break || sleep 3
done
npx @playwright/cli tab-list
npx @playwright/cli snapshot
```
If you are iterating frequently, keep the repo build/watch task running separately so relaunches pick up already-generated output.
## Cleanup
**Always kill the Code OSS instance when you're done.** Code OSS is a full Electron app that consumes significant memory (often 14 GB+). Leaving it running wastes resources and holds the CDP port.
The launcher writes everything under a temp `runDir` (printed in the JSON). When you're done:
```bash
# Disconnect npx @playwright/cli
# Disconnect playwright
npx @playwright/cli close
# Kill the Code OSS instance listening on the debug port (if running)
# macOS / Linux:
pids=$(lsof -t -i :9224)
if [ -n "$pids" ]; then
kill $pids
fi
# Kill the Code OSS instance
kill "$PID" 2>/dev/null || true
# Or by port if you've lost the pid:
pids=$(lsof -t -i :$CDP); [ -n "$pids" ] && kill $pids
# Windows:
# taskkill /F /PID <PID>
# Or use Task Manager to end "Code - OSS"
# Remove the throwaway profile
rm -rf "$(dirname "$LOG")"
```
Verify it's gone:
```bash
# Confirm no process is listening on the debug port
lsof -i :9224 # should return nothing
```
Code OSS is a full Electron app and easily eats 1-4 GB. Always clean up.
## Troubleshooting
- **"Sent env to running instance. Terminating..."** - The dynamic `--user-data-dir` should prevent this. If you see it, another Code OSS is using the same profile path; pass `--source-user-data-dir` to a different source or check that the temp copy actually happened (`ls "$(jq -r .userDataDir <<<"$INFO")"`).
- **Renderer ESM errors / `import { Menu } from 'electron'`** - `ELECTRON_RUN_AS_NODE` is set in your env. The launcher unsets it for the child, but if you spawn `code.sh` yourself, do the same.
- **Built-in extension fails to load (`Cannot find module .../extensions/.../out/extension.js`)** - extensions weren't compiled. Run `npm run watch-extensions` (or `npm run compile-extensions`).
- **CDP connect refused** - give it a few seconds; the launcher returns before the renderer is ready. Use the retry loop above.
- **Snapshot shows the wrong page or no expected controls** - use `tab-list`, switch with `tab-select <index>` if needed, then re-snapshot before interacting.
- **CLI typing commands complete but the input stays empty** - focus chat with the platform shortcut, use `press` or clipboard paste rather than `fill` / `type`, then verify the input state before sending.
- **Auth missing in the launched window** - confirm the source profile is actually authed (`ls "$SOURCE_UDD"` should contain `User/`, and `ls "$SOURCE_UDD/User/globalStorage"` should show persisted extension state). Some auth lives in the OS keychain - that's per-user, so it follows automatically as long as you're running as the same user.

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# Launch Code OSS (VS Code from sources) with:
# - a fresh, slimmed copy of the authenticated user-data-dir (so Copilot/GitHub auth works)
# - an isolated --shared-data-dir (otherwise two instances share ~/.vscode-oss-shared and crash each other)
# - unique debug ports for renderer (CDP), extension host, main process, and agent host
#
# Auth on macOS comes from the OS keychain (per-app, shared automatically) plus
# the encrypted blob in User/globalStorage/state.vscdb (per-UDD). The slim copy
# keeps the auth-relevant state and drops caches / workspaceStorage / logs.
#
# Prints a single JSON line to stdout with the chosen ports + paths so the
# caller can pick them up programmatically. Logs go to stderr.
#
# Usage:
# launch.sh [--agents] [--source-user-data-dir <path>] [--repo <vscode-repo-root>]
# [--clone-extensions] [--full] [-- <extra code.sh args>]
#
# Flags:
# --clone-extensions Copy the source extensions/ into the new profile (~10s).
# Default: start with an EMPTY extensions/ dir - fastest
# and conflict-free, but no third-party extensions.
# --full Copy the entire profile (incl. extensions). Use if the
# slim copy is missing something you need.
#
# Defaults:
# --source-user-data-dir $CODE_OSS_DEV_AUTHED_USER_DATA_DIR (else ~/.vscode-oss-dev)
# --repo $PWD if it looks like a vscode checkout; otherwise pass it explicitly
set -euo pipefail
umask 077
AGENTS=0
SOURCE_UDD="${CODE_OSS_DEV_AUTHED_USER_DATA_DIR:-$HOME/.vscode-oss-dev}"
REPO=""
EXTRA_ARGS=()
CLONE_EXTENSIONS=0
FULL=0
while [[ $# -gt 0 ]]; do
case "$1" in
--agents) AGENTS=1; shift ;;
--source-user-data-dir) SOURCE_UDD="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--clone-extensions|--copy-extensions) CLONE_EXTENSIONS=1; shift ;;
--full) FULL=1; shift ;;
--) shift; EXTRA_ARGS=("$@"); break ;;
*) echo "Unknown arg: $1" >&2; exit 2 ;;
esac
done
if [[ -z "$REPO" ]]; then
if [[ -x "$PWD/scripts/code.sh" ]]; then
REPO="$PWD"
else
echo "Could not find a vscode checkout in $PWD. Pass --repo <path>." >&2
exit 2
fi
fi
if [[ ! -d "$SOURCE_UDD" ]]; then
echo "Source user-data-dir does not exist: $SOURCE_UDD" >&2
echo "Pass --source-user-data-dir <path> or set CODE_OSS_DEV_AUTHED_USER_DATA_DIR." >&2
exit 2
fi
pick_port() {
node -e '
const net = require("net");
const s = net.createServer();
s.listen(0, "127.0.0.1", () => { const p = s.address().port; s.close(() => console.log(p)); });
'
}
CDP_PORT=$(pick_port)
EXTHOST_PORT=$(pick_port)
MAIN_PORT=$(pick_port)
AGENTHOST_PORT=$(pick_port)
STAMP=$(date +%Y%m%d-%H%M%S)-$$
RUN_DIR="${TMPDIR:-/tmp}/code-oss-dev/$STAMP"
DEST_UDD="$RUN_DIR/user-data"
SHARED_DATA_DIR="$RUN_DIR/shared-data"
mkdir -p "$DEST_UDD" "$SHARED_DATA_DIR"
# Excludes (deny-list, so future VS Code additions copy through by default).
# Anchored excludes (starting with /) match only at the top level so we don't
# accidentally strip files inside subdirs that share a name.
EXCLUDES=(
'/extensions' # handled separately below
'/workspaceStorage' 'User/workspaceStorage' # per-workspace state, incl. chat sessions
'User/History' # local file edit history
'/CachedExtensionVSIXs' # backup VSIXs
'/logs'
'/Cache' '/Code Cache' '/CachedData' '/component_crx_cache'
'/GPUCache' '/ShaderCache' '/Dawn*Cache'
'/Backups' '/blob_storage' '/BrowserMetrics' '/Crashpad'
'/Session Storage'
'/Singleton*'
'*.lock' '*.sock'
)
if [[ "$FULL" == "1" ]]; then
echo "[launch.sh] full copy: $SOURCE_UDD -> $DEST_UDD" >&2
rsync -a "$SOURCE_UDD/" "$DEST_UDD/"
else
echo "[launch.sh] slim copy: $SOURCE_UDD -> $DEST_UDD" >&2
RSYNC_ARGS=(-a)
for e in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$e"); done
rsync "${RSYNC_ARGS[@]}" "$SOURCE_UDD/" "$DEST_UDD/"
fi
# Extensions:
# --full -> already copied above
# --clone-extensions -> copy into the new profile (~10s)
# default -> fresh empty dir
EXT_DIR="$DEST_UDD/extensions"
mkdir -p "$EXT_DIR"
if [[ "$FULL" != "1" && "$CLONE_EXTENSIONS" == "1" ]]; then
echo "[launch.sh] copying extensions: $SOURCE_UDD/extensions -> $EXT_DIR" >&2
rsync -a "$SOURCE_UDD/extensions/" "$EXT_DIR/"
fi
# Strip ELECTRON_RUN_AS_NODE, commonly inherited from VS Code's integrated
# terminal / agent runtimes; it breaks ./scripts/code.sh.
unset ELECTRON_RUN_AS_NODE
CODE_SH="$REPO/scripts/code.sh"
if [[ ! -x "$CODE_SH" ]]; then
echo "Could not find an executable Code OSS launcher at $CODE_SH. Pass --repo <vscode-repo-root>." >&2
exit 2
fi
ARGS=(
"--user-data-dir=$DEST_UDD"
"--extensions-dir=$EXT_DIR"
"--shared-data-dir=$SHARED_DATA_DIR"
"--remote-debugging-port=$CDP_PORT"
"--inspect-extensions=$EXTHOST_PORT"
"--inspect=$MAIN_PORT"
"--inspect-agenthost=$AGENTHOST_PORT"
)
if [[ "$AGENTS" == "1" ]]; then
ARGS=("--agents" "${ARGS[@]}")
fi
if (( ${#EXTRA_ARGS[@]} )); then
ARGS+=("${EXTRA_ARGS[@]}")
fi
LOG_FILE="$RUN_DIR/code.log"
echo "[launch.sh] launching: $CODE_SH ${ARGS[*]}" >&2
echo "[launch.sh] logs: $LOG_FILE" >&2
nohup "$CODE_SH" "${ARGS[@]}" >"$LOG_FILE" 2>&1 &
PID=$!
node -e '
console.log(JSON.stringify({
pid: '"$PID"',
cdpPort: '"$CDP_PORT"',
extHostPort: '"$EXTHOST_PORT"',
mainPort: '"$MAIN_PORT"',
agentHostPort: '"$AGENTHOST_PORT"',
userDataDir: process.argv[1],
extensionsDir: process.argv[2],
sharedDataDir: process.argv[3],
runDir: process.argv[4],
logFile: process.argv[5],
repo: process.argv[6],
agents: '"$AGENTS"' === 1,
}));
' "$DEST_UDD" "$EXT_DIR" "$SHARED_DATA_DIR" "$RUN_DIR" "$LOG_FILE" "$REPO"