Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .agents/skills/launch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The clone is **slim**: workspace storage, browser caches, file history, cached V

> The launcher always sets `files.simpleDialog.enable: true` in the launched profile's `User/settings.json`. This is required for automation: VS Code's native OS file dialogs cannot be driven via `@playwright/cli` over CDP and are completely unreachable over SSH on headless macOS. The simple (quick-input) dialog can be navigated with `press` and clipboard paste. The override is per-launch and only affects throwaway profiles.

> When launching a regular editor window from an agent session, first call `get_current_session` and pass its `title` as `--session-title`. The launcher writes that title into the throwaway profile's `window.title` setting so each editor window can be mapped back to its originating session. This never modifies the source profile. Do not combine `--session-title` with `--agents`: `window.title` is read-only in the Agents window.

> For unattended automation, pass `--disable-workspace-trust` so a trust dialog cannot block the flow or extension-host startup. The override is process-scoped and does not modify the source profile. Only use it with content you trust.

## Launch
Expand All @@ -45,8 +47,9 @@ The launcher script lives next to this SKILL.md at `scripts/launch.sh` (macOS/Li

```bash
# LAUNCH=<dir-of-this-SKILL.md>/scripts/launch.sh
"$LAUNCH" # default: workbench
"$LAUNCH" --agents # Agents window
SESSION_TITLE=<title-from-get_current_session>
"$LAUNCH" --session-title "$SESSION_TITLE" # default: workbench
"$LAUNCH" --agents # Agents window (no custom title)
"$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
Expand All @@ -61,8 +64,9 @@ On Windows, invoke the PowerShell launcher with the same flags:
```powershell
$skillDir = '<dir-of-this-SKILL.md>'
$launch = Join-Path $skillDir 'scripts\launch.ps1'
& $launch # default: workbench
& $launch --agents # Agents window
$sessionTitle = '<title-from-get_current_session>'
& $launch --session-title $sessionTitle # default: workbench
& $launch --agents # Agents window (no custom title)
& $launch -- --use-mock-keychain # forward extra args to code.bat
& $launch --source-user-data-dir C:\path\to\profile
& $launch --repo C:\path\to\vscode
Expand Down
119 changes: 21 additions & 98 deletions .agents/skills/launch/scripts/launch.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ $cloneExtensions = $false
$full = $false
$skipPreLaunch = $false
$disableWorkspaceTrust = $false
$sessionTitle = ''
if ($null -eq $cliArgs) {
$cliArgs = @()
}
Expand Down Expand Up @@ -279,103 +280,6 @@ function Test-SourceHasGitHubAuthenticationSecret([string]$node, [string]$source
return $false
}

function Get-JsoncCodeMask([string]$text) {
# Returns a same-length copy of $text with every comment span blanked out.
# Offsets are preserved so a match found in the mask can be applied to the
# original. String contents are respected, so a `//` inside a value (a URL,
# say) is not mistaken for a comment.
$chars = $text.ToCharArray()
$masked = [char[]]::new($chars.Length)
[Array]::Copy($chars, $masked, $chars.Length)

$inString = $false
$inLineComment = $false
$inBlockComment = $false
$escaped = $false

for ($i = 0; $i -lt $chars.Length; $i++) {
$current = $chars[$i]
$next = if ($i + 1 -lt $chars.Length) { $chars[$i + 1] } else { [char]0 }

if ($inLineComment) {
if ($current -eq "`n") { $inLineComment = $false } else { $masked[$i] = ' ' }
continue
}
if ($inBlockComment) {
if ($current -eq '*' -and $next -eq '/') {
$masked[$i] = ' '
$masked[$i + 1] = ' '
$i++
$inBlockComment = $false
} elseif ($current -ne "`n") {
$masked[$i] = ' '
}
continue
}
if ($inString) {
if ($escaped) { $escaped = $false }
elseif ($current -eq '\') { $escaped = $true }
elseif ($current -eq '"') { $inString = $false }
continue
}

if ($current -eq '"') { $inString = $true }
elseif ($current -eq '/' -and $next -eq '/') { $masked[$i] = ' '; $inLineComment = $true }
elseif ($current -eq '/' -and $next -eq '*') { $masked[$i] = ' '; $masked[$i + 1] = ' '; $i++; $inBlockComment = $true }
}

return (-join $masked)
}

function Ensure-SimpleDialogSetting([string]$settingsFile) {
$key = 'files.simpleDialog.enable'
$settingsDirectory = Split-Path -Parent $settingsFile
New-Item -ItemType Directory -Force -Path $settingsDirectory | Out-Null

if (Test-Path -LiteralPath $settingsFile -PathType Leaf) {
$text = [IO.File]::ReadAllText($settingsFile)
} else {
$text = ''
}

if ([string]::IsNullOrWhiteSpace($text)) {
[IO.File]::WriteAllText($settingsFile, "{`n `"$key`": true`n}`n", [Text.UTF8Encoding]::new($false))
return
}

# Match against a comment-masked copy so a commented-out occurrence such as
# `// "files.simpleDialog.enable": false` is not mistaken for the real
# setting. Offsets line up with the original, so the value is rewritten in
# place without disturbing comments.
$maskedText = Get-JsoncCodeMask $text
$keyPattern = [regex]::Escape($key)
$keyValueRegex = [regex]::new("(`"$keyPattern`"\s*:\s*)(true|false|null|`"[^`"`r`n]*`"|-?\d+(?:\.\d+)?)")
$keyMatch = $keyValueRegex.Match($maskedText)
if ($keyMatch.Success) {
$valueGroup = $keyMatch.Groups[2]
$updated = $text.Substring(0, $valueGroup.Index) + 'true' + $text.Substring($valueGroup.Index + $valueGroup.Length)
[IO.File]::WriteAllText($settingsFile, $updated, [Text.UTF8Encoding]::new($false))
return
}

$lastBrace = $maskedText.LastIndexOf('}')
if ($lastBrace -eq -1) {
throw "settings.json has no closing brace - refusing to clobber it: $settingsFile"
}
$firstBrace = $maskedText.IndexOf('{')
if ($firstBrace -eq -1 -or $firstBrace -ge $lastBrace) {
throw "settings.json has no opening brace - refusing to clobber it: $settingsFile"
}

# Whether a leading comma is needed depends only on real content, so decide
# it from the masked copy too.
$between = $maskedText.Substring($firstBrace + 1, $lastBrace - $firstBrace - 1).Trim()
$separator = if ($between.Length -eq 0 -or $between.EndsWith(',')) { '' } else { ',' }
$insertion = "$separator`n `"$key`": true`n"
$updated = $text.Substring(0, $lastBrace) + $insertion + $text.Substring($lastBrace)
[IO.File]::WriteAllText($settingsFile, $updated, [Text.UTF8Encoding]::new($false))
}

function Write-LogTail([string]$logFile) {
if (Test-Path -LiteralPath $logFile) {
Get-Content -LiteralPath $logFile -Tail 80 | ForEach-Object { [Console]::Error.WriteLine($_) }
Expand Down Expand Up @@ -442,6 +346,13 @@ for ($index = 0; $index -lt $cliArgs.Count; $index++) {
$agents = $true
continue
}
'--session-title' {
if ($index + 1 -ge $cliArgs.Count) {
Exit-Usage 'Missing value for --session-title.'
}
$sessionTitle = $cliArgs[++$index]
continue
}
'--source-user-data-dir' {
if ($index + 1 -ge $cliArgs.Count) {
Exit-Usage 'Missing value for --source-user-data-dir.'
Expand Down Expand Up @@ -489,6 +400,10 @@ for ($index = 0; $index -lt $cliArgs.Count; $index++) {
}
}

if ($agents -and -not [string]::IsNullOrWhiteSpace($sessionTitle)) {
Exit-Usage '--session-title is only supported for regular editor windows; window.title is read-only in the Agents window.'
}

try {
$launchStopwatch = [Diagnostics.Stopwatch]::StartNew()
if ([string]::IsNullOrWhiteSpace($repo)) {
Expand Down Expand Up @@ -574,8 +489,16 @@ try {
}

$settingsFile = Join-Path $destinationUdd 'User\settings.json'
Ensure-SimpleDialogSetting $settingsFile
$sourceSettingsFile = Join-Path $sourceUserDataDir 'User\settings.json'
$settingsScript = Join-Path $PSScriptRoot 'updateSettings.ts'
& $node $settingsScript $settingsFile $sessionTitle $sourceSettingsFile
if ($LASTEXITCODE -ne 0) {
throw "Failed to update launch settings in $settingsFile"
}
Write-LaunchError "[launch.ps1] ensured files.simpleDialog.enable=true in $settingsFile"
if (-not [string]::IsNullOrWhiteSpace($sessionTitle)) {
Write-LaunchError "[launch.ps1] set window.title for session: $sessionTitle"
}
$profileReadyMs = $launchStopwatch.ElapsedMilliseconds

$launchArgs = [System.Collections.Generic.List[string]]::new()
Expand Down
83 changes: 22 additions & 61 deletions .agents/skills/launch/scripts/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# caller can pick them up programmatically. Logs go to stderr.
#
# Usage:
# launch.sh [--agents] [--source-user-data-dir <path>] [--repo <vscode-repo-root>]
# launch.sh [--agents] [--session-title <title>] [--source-user-data-dir <path>] [--repo <vscode-repo-root>]
# [--clone-extensions] [--full] [--skip-prelaunch]
# [--disable-workspace-trust] [-- <extra code.sh args>]
#
Expand Down Expand Up @@ -43,10 +43,19 @@ CLONE_EXTENSIONS=0
FULL=0
SKIP_PRELAUNCH=0
DISABLE_WORKSPACE_TRUST=0
SESSION_TITLE=""

while [[ $# -gt 0 ]]; do
case "$1" in
--agents) AGENTS=1; shift ;;
--session-title)
if [[ $# -lt 2 ]]; then
echo "Missing value for --session-title." >&2
exit 2
fi
SESSION_TITLE="$2"
shift 2
;;
--source-user-data-dir) SOURCE_UDD="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--clone-extensions|--copy-extensions) CLONE_EXTENSIONS=1; shift ;;
Expand All @@ -58,6 +67,11 @@ while [[ $# -gt 0 ]]; do
esac
done

if [[ "$AGENTS" == "1" && -n "$SESSION_TITLE" ]]; then
echo "--session-title is only supported for regular editor windows; window.title is read-only in the Agents window." >&2
exit 2
fi

monotonic_ms() {
node -e 'process.stdout.write(String(process.hrtime.bigint() / 1_000_000n))'
}
Expand Down Expand Up @@ -166,70 +180,17 @@ fi
# always applied because every launched instance under this skill is
# a throwaway used for automation.
SETTINGS_FILE="$DEST_UDD/User/settings.json"
SOURCE_SETTINGS_FILE="$SOURCE_UDD/User/settings.json"
mkdir -p "$(dirname "$SETTINGS_FILE")"
# Data-preserving text-based merge: insert/update `files.simpleDialog.enable`
# without reparsing the whole file. Avoids dropping user comments and
# string values containing `//` (e.g. URLs). Fails loudly if the file
# exists but has no recognizable JSON object shape — never silently
# overwrites with `{}`.
if ! node - "$SETTINGS_FILE" <<'NODE'
const fs = require('fs');
const f = process.argv[2];
const KEY = 'files.simpleDialog.enable';

let text;
try { text = fs.readFileSync(f, 'utf8'); }
catch (e) {
if (e.code === 'ENOENT') text = '';
else { console.error('[launch.sh] cannot read ' + f + ': ' + e.message); process.exit(1); }
}

// Empty file → write a fresh object.
if (text.trim() === '') {
fs.writeFileSync(f, '{\n "' + KEY + '": true\n}\n');
process.exit(0);
}

// Key already present (with any value) → update its value to `true`
// via a targeted regex on the value slot only.
const keyValueRe = new RegExp('("' + KEY.replace(/\./g, '\\.') + '"\\s*:\\s*)(true|false|null|"[^"\\n]*"|-?\\d+(?:\\.\\d+)?)', 'g');
if (keyValueRe.test(text)) {
const updated = text.replace(keyValueRe, '$1true');
fs.writeFileSync(f, updated);
process.exit(0);
}

// Otherwise: find the LAST `}` and insert the new key before it.
// We deliberately don't parse JSONC — this preserves comments and
// any other content the source profile had.
const lastBrace = text.lastIndexOf('}');
if (lastBrace === -1) {
console.error('[launch.sh] settings.json has no closing brace — refusing to clobber it: ' + f);
process.exit(1);
}

// Decide whether to add a leading comma. If the only thing between the
// first `{` and the last `}` is whitespace and comments, the object is
// empty for our purposes and no comma is needed.
const firstBrace = text.indexOf('{');
if (firstBrace === -1 || firstBrace >= lastBrace) {
console.error('[launch.sh] settings.json has no opening brace — refusing to clobber it: ' + f);
process.exit(1);
}
const between = text.slice(firstBrace + 1, lastBrace)
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/[^\n]*/g, '')
.trim();
const separator = between.length === 0 || between.endsWith(',') ? '' : ',';
const insertion = separator + '\n "' + KEY + '": true\n';

fs.writeFileSync(f, text.slice(0, lastBrace) + insertion + text.slice(lastBrace));
NODE
then
echo "[launch.sh] failed to ensure files.simpleDialog.enable=true in $SETTINGS_FILE — automation may need to fall back to per-key input" >&2
SETTINGS_SCRIPT="$(cd "$(dirname "$0")" && pwd)/updateSettings.ts"
if ! node "$SETTINGS_SCRIPT" "$SETTINGS_FILE" "$SESSION_TITLE" "$SOURCE_SETTINGS_FILE"; then
echo "[launch.sh] failed to update launch settings in $SETTINGS_FILE" >&2
exit 1
fi
echo "[launch.sh] ensured files.simpleDialog.enable=true in $SETTINGS_FILE" >&2
if [[ -n "$SESSION_TITLE" ]]; then
echo "[launch.sh] set window.title for session: $SESSION_TITLE" >&2
fi
PROFILE_READY_MS=$(monotonic_ms)

# Strip ELECTRON_RUN_AS_NODE, commonly inherited from VS Code's integrated
Expand Down
Loading
Loading