From 14160da8665d6f9e06d007b4b298275d62654ca5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 16:43:05 -0700 Subject: [PATCH 1/6] Copy vscode's node.pty --- build/.moduleignore | 1 + build/gulpfile.vscode.ts | 46 ++++++++++++++++++++++++++------- build/linux/debian/dep-lists.ts | 1 - build/linux/rpm/dep-lists.ts | 9 ------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/build/.moduleignore b/build/.moduleignore index a083e73b7d5..faa4973e2dc 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -191,6 +191,7 @@ zone.js/dist/** # @github/copilot - strip unneeded binaries and files @github/copilot/sdk/index.js +@github/copilot/prebuilds/** @github/copilot/clipboard/** @github/copilot/ripgrep/** @github/copilot/**/keytar.node diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 605a4a9f2ed..539bc163d78 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -332,14 +332,10 @@ function getCopilotExcludeFilter(platform: string, arch: string, quality: string const targetPlatformArch = `${platform}-${arch}`; const nonTargetPlatforms = copilotPlatforms.filter(p => p !== targetPlatformArch); - // Strip wrong-architecture @github/copilot-{platform} packages and prebuilds. - // Keep target-platform prebuilds (pty.node, spawn-helper, conpty) so the - // copilot CLI can use them from the read-only app bundle at runtime. - // keytar.node is stripped by .moduleignore since it requires libsecret on Linux. - const excludes = nonTargetPlatforms.flatMap(p => [ - `!**/node_modules/@github/copilot-${p}/**`, - `!**/node_modules/@github/copilot/prebuilds/${p}/**`, - ]); + // Strip wrong-architecture @github/copilot-{platform} packages. + // All copilot prebuilds are stripped by .moduleignore; VS Code's own + // node-pty is copied into the prebuilds location by a post-packaging task. + const excludes = nonTargetPlatforms.map(p => `!**/node_modules/@github/copilot-${p}/**`); // Strip agent host SDK dependencies entirely from stable builds if (quality === 'stable') { @@ -717,6 +713,37 @@ function patchWin32DependenciesTask(destinationFolderName: string) { }; } +/** + * Copies VS Code's own node-pty native module into the copilot SDK's + * `prebuilds/{platform}-{arch}/` directory so the copilot CLI subprocess + * can find it at runtime. This replaces the copilot-bundled prebuilds + * (which are stripped by .moduleignore) with the same binary VS Code + * already ships, avoiding new system dependency requirements. + */ +function copyNodePtyForCopilotTask(platform: string, arch: string, destinationFolderName: string) { + const outputDir = path.join(path.dirname(root), destinationFolderName); + + return async () => { + const appBase = platform === 'darwin' + ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app') + : path.join(outputDir, 'resources', 'app'); + + const nodePtySource = path.join(appBase, 'node_modules', 'node-pty', 'build', 'Release'); + const prebuildsArch = platform === 'win32' ? `${platform}-${arch}` : + platform === 'darwin' ? `darwin-${arch}` : `linux-${arch}`; + const copilotPrebuildsDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'prebuilds', prebuildsArch); + + if (!fs.existsSync(nodePtySource)) { + console.log(`[copyNodePtyForCopilot] node-pty source not found at ${nodePtySource}, skipping`); + return; + } + + fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); + fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); + console.log(`[copyNodePtyForCopilot] Copied node-pty from ${nodePtySource} to ${copilotPrebuildsDir}`); + }; +} + const buildRoot = path.dirname(root); const BUILD_TARGETS = [ @@ -741,7 +768,8 @@ BUILD_TARGETS.forEach(buildTarget => { const packageTasks: task.Task[] = [ compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), - packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) + packageTask(platform, arch, sourceFolderName, destinationFolderName, opts), + copyNodePtyForCopilotTask(platform, arch, destinationFolderName) ]; if (platform === 'win32') { diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 86b13a8bf2d..46c257da4f7 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -46,7 +46,6 @@ export const referenceGeneratedDepsByArch = { 'libnss3 (>= 2:3.30)', 'libnss3 (>= 3.26)', 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 6)', 'libudev1 (>= 183)', 'libx11-6', 'libx11-6 (>= 2:1.4.99.1)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 705bf75eff1..0424c8d37a6 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -104,15 +104,6 @@ export const referenceGeneratedDepsByArch = { 'libsmime3.so(NSS_3.10)(64bit)', 'libsmime3.so(NSS_3.2)(64bit)', 'libssl3.so(NSS_3.28)(64bit)', - 'libstdc++.so.6()(64bit)', - 'libstdc++.so.6(CXXABI_1.3)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.8)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.9)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.11)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.14)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', 'libudev.so.1()(64bit)', 'libudev.so.1(LIBUDEV_183)(64bit)', 'libutil.so.1()(64bit)', From f4a66a754351ad9ec2f59cf0d27cec25df1971cc Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 16:49:35 -0700 Subject: [PATCH 2/6] And ripgrep --- build/darwin/create-universal-app.ts | 6 +++- build/gulpfile.vscode.ts | 46 ++++++++++++++++++---------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index aebc765d2c3..f0c868857e5 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -61,6 +61,8 @@ async function main(buildDir?: string) { crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', `copilot-${plat}`)); // @github/copilot/prebuilds/{platform} (pty.node, spawn-helper) crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'prebuilds', plat)); + // @github/copilot/sdk/ripgrep/bin/{platform} (rg binary) + crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'sdk', 'ripgrep', 'bin', plat)); } } @@ -74,6 +76,8 @@ async function main(buildDir?: string) { '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', '**/node_modules/@github/copilot/prebuilds/darwin-x64/**', '**/node_modules/@github/copilot/prebuilds/darwin-arm64/**', + '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-x64/**', + '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-arm64/**', ]; await makeUniversalApp({ @@ -83,7 +87,7 @@ async function main(buildDir?: string) { outAppPath, force: true, mergeASARs: true, - x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*}', + x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/*}', filesToSkipComparison: (file: string) => { for (const expected of filesToSkip) { if (minimatch(file, expected)) { diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 539bc163d78..a851cdc15ff 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -714,13 +714,16 @@ function patchWin32DependenciesTask(destinationFolderName: string) { } /** - * Copies VS Code's own node-pty native module into the copilot SDK's - * `prebuilds/{platform}-{arch}/` directory so the copilot CLI subprocess - * can find it at runtime. This replaces the copilot-bundled prebuilds - * (which are stripped by .moduleignore) with the same binary VS Code - * already ships, avoiding new system dependency requirements. + * Copies VS Code's own node-pty and ripgrep binaries into the copilot SDK's + * expected locations so the copilot CLI subprocess can find them at runtime. + * The copilot-bundled prebuilds and ripgrep are stripped by .moduleignore; + * this replaces them with the same binaries VS Code already ships, avoiding + * new system dependency requirements. + * + * node-pty: `prebuilds/{platform}-{arch}/` (pty.node + spawn-helper) + * ripgrep: `sdk/ripgrep/bin/{platform}-{arch}/` (rg binary) */ -function copyNodePtyForCopilotTask(platform: string, arch: string, destinationFolderName: string) { +function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFolderName: string) { const outputDir = path.join(path.dirname(root), destinationFolderName); return async () => { @@ -728,19 +731,28 @@ function copyNodePtyForCopilotTask(platform: string, arch: string, destinationFo ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app') : path.join(outputDir, 'resources', 'app'); - const nodePtySource = path.join(appBase, 'node_modules', 'node-pty', 'build', 'Release'); - const prebuildsArch = platform === 'win32' ? `${platform}-${arch}` : - platform === 'darwin' ? `darwin-${arch}` : `linux-${arch}`; - const copilotPrebuildsDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'prebuilds', prebuildsArch); + const platformArch = `${platform === 'win32' ? 'win32' : platform}-${arch}`; - if (!fs.existsSync(nodePtySource)) { - console.log(`[copyNodePtyForCopilot] node-pty source not found at ${nodePtySource}, skipping`); - return; + // Copy node-pty (pty.node + spawn-helper) into copilot prebuilds + const nodePtySource = path.join(appBase, 'node_modules', 'node-pty', 'build', 'Release'); + const copilotPrebuildsDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'prebuilds', platformArch); + + if (fs.existsSync(nodePtySource)) { + fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); + fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); + console.log(`[copyCopilotNativeDeps] Copied node-pty to ${copilotPrebuildsDir}`); } - fs.mkdirSync(copilotPrebuildsDir, { recursive: true }); - fs.cpSync(nodePtySource, copilotPrebuildsDir, { recursive: true }); - console.log(`[copyNodePtyForCopilot] Copied node-pty from ${nodePtySource} to ${copilotPrebuildsDir}`); + // Copy ripgrep (rg binary) into copilot sdk/ripgrep + const rgBinary = platform === 'win32' ? 'rg.exe' : 'rg'; + const ripgrepSource = path.join(appBase, 'node_modules', '@vscode', 'ripgrep', 'bin', rgBinary); + const copilotRipgrepDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'sdk', 'ripgrep', 'bin', platformArch); + + if (fs.existsSync(ripgrepSource)) { + fs.mkdirSync(copilotRipgrepDir, { recursive: true }); + fs.copyFileSync(ripgrepSource, path.join(copilotRipgrepDir, rgBinary)); + console.log(`[copyCopilotNativeDeps] Copied ripgrep to ${copilotRipgrepDir}`); + } }; } @@ -769,7 +781,7 @@ BUILD_TARGETS.forEach(buildTarget => { compileNativeExtensionsBuildTask, util.rimraf(path.join(buildRoot, destinationFolderName)), packageTask(platform, arch, sourceFolderName, destinationFolderName, opts), - copyNodePtyForCopilotTask(platform, arch, destinationFolderName) + copyCopilotNativeDepsTask(platform, arch, destinationFolderName) ]; if (platform === 'win32') { From 1617f8d42dd708a9c6931d3c974c24aa1a4575c5 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 16:51:56 -0700 Subject: [PATCH 3/6] And thsi --- build/darwin/verify-macho.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/darwin/verify-macho.ts b/build/darwin/verify-macho.ts index 1884c72c227..3043d1c1e34 100644 --- a/build/darwin/verify-macho.ts +++ b/build/darwin/verify-macho.ts @@ -31,6 +31,9 @@ const FILES_TO_SKIP = [ '**/node_modules/@github/copilot-darwin-arm64/**', '**/node_modules.asar.unpacked/@github/copilot-darwin-x64/**', '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', + // Copilot prebuilds and ripgrep: single-arch binaries in per-platform directories + '**/node_modules/@github/copilot/prebuilds/darwin-*/**', + '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/**', ]; function isFileSkipped(file: string): boolean { From fa411a7cd10bbaf2cea857cceaf0a6648740b2ae Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 18:28:11 -0700 Subject: [PATCH 4/6] Ripgrep goes to non-SDK --- build/darwin/create-universal-app.ts | 10 +++++----- build/darwin/verify-macho.ts | 2 +- build/gulpfile.vscode.ts | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index f0c868857e5..c772efa8e4d 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -61,8 +61,8 @@ async function main(buildDir?: string) { crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', `copilot-${plat}`)); // @github/copilot/prebuilds/{platform} (pty.node, spawn-helper) crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'prebuilds', plat)); - // @github/copilot/sdk/ripgrep/bin/{platform} (rg binary) - crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'sdk', 'ripgrep', 'bin', plat)); + // @github/copilot/ripgrep/bin/{platform} (rg binary) + crossCopyPlatformDir(x64AppPath, arm64AppPath, path.join(base, '@github', 'copilot', 'ripgrep', 'bin', plat)); } } @@ -76,8 +76,8 @@ async function main(buildDir?: string) { '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', '**/node_modules/@github/copilot/prebuilds/darwin-x64/**', '**/node_modules/@github/copilot/prebuilds/darwin-arm64/**', - '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-x64/**', - '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-arm64/**', + '**/node_modules/@github/copilot/ripgrep/bin/darwin-x64/**', + '**/node_modules/@github/copilot/ripgrep/bin/darwin-arm64/**', ]; await makeUniversalApp({ @@ -87,7 +87,7 @@ async function main(buildDir?: string) { outAppPath, force: true, mergeASARs: true, - x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/*}', + x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node,**/node_modules/@github/copilot-darwin-*/copilot,**/node_modules/@github/copilot/prebuilds/darwin-*/*,**/node_modules/@github/copilot/ripgrep/bin/darwin-*/*}', filesToSkipComparison: (file: string) => { for (const expected of filesToSkip) { if (minimatch(file, expected)) { diff --git a/build/darwin/verify-macho.ts b/build/darwin/verify-macho.ts index 3043d1c1e34..d83d2499056 100644 --- a/build/darwin/verify-macho.ts +++ b/build/darwin/verify-macho.ts @@ -33,7 +33,7 @@ const FILES_TO_SKIP = [ '**/node_modules.asar.unpacked/@github/copilot-darwin-arm64/**', // Copilot prebuilds and ripgrep: single-arch binaries in per-platform directories '**/node_modules/@github/copilot/prebuilds/darwin-*/**', - '**/node_modules/@github/copilot/sdk/ripgrep/bin/darwin-*/**', + '**/node_modules/@github/copilot/ripgrep/bin/darwin-*/**', ]; function isFileSkipped(file: string): boolean { diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index a851cdc15ff..dd7a4173a60 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -721,7 +721,7 @@ function patchWin32DependenciesTask(destinationFolderName: string) { * new system dependency requirements. * * node-pty: `prebuilds/{platform}-{arch}/` (pty.node + spawn-helper) - * ripgrep: `sdk/ripgrep/bin/{platform}-{arch}/` (rg binary) + * ripgrep: `ripgrep/bin/{platform}-{arch}/` (rg binary) */ function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFolderName: string) { const outputDir = path.join(path.dirname(root), destinationFolderName); @@ -743,10 +743,10 @@ function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFo console.log(`[copyCopilotNativeDeps] Copied node-pty to ${copilotPrebuildsDir}`); } - // Copy ripgrep (rg binary) into copilot sdk/ripgrep + // Copy ripgrep (rg binary) into copilot ripgrep const rgBinary = platform === 'win32' ? 'rg.exe' : 'rg'; const ripgrepSource = path.join(appBase, 'node_modules', '@vscode', 'ripgrep', 'bin', rgBinary); - const copilotRipgrepDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'sdk', 'ripgrep', 'bin', platformArch); + const copilotRipgrepDir = path.join(appBase, 'node_modules', '@github', 'copilot', 'ripgrep', 'bin', platformArch); if (fs.existsSync(ripgrepSource)) { fs.mkdirSync(copilotRipgrepDir, { recursive: true }); From 354c9140c6510b46a04044a12761922a2d6f3c2b Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 19:31:36 -0700 Subject: [PATCH 5/6] Skip copy for stable build --- build/gulpfile.vscode.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index dd7a4173a60..f74f2b06c10 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -731,6 +731,17 @@ function copyCopilotNativeDepsTask(platform: string, arch: string, destinationFo ? path.join(outputDir, `${product.nameLong}.app`, 'Contents', 'Resources', 'app') : path.join(outputDir, 'resources', 'app'); + // On stable builds the copilot SDK is stripped entirely -- nothing to copy into. + const copilotDir = path.join(appBase, 'node_modules', '@github', 'copilot'); + if (!fs.existsSync(copilotDir)) { + const quality = (product as { quality?: string }).quality; + if (quality && quality !== 'stable') { + throw new Error(`[copyCopilotNativeDeps] Copilot SDK directory not found at ${copilotDir} -- unexpected for ${quality} build`); + } + console.log(`[copyCopilotNativeDeps] Skipping -- copilot SDK not present (stable build)`); + return; + } + const platformArch = `${platform === 'win32' ? 'win32' : platform}-${arch}`; // Copy node-pty (pty.node + spawn-helper) into copilot prebuilds From 594e560bf7cbc38fa321de962738665cb86fa639 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 12 Mar 2026 19:39:54 -0700 Subject: [PATCH 6/6] Remove outdated script --- .agents/skills/launch/SKILL.md | 17 -- .../node/createAndSendMessageAsLocalAgent.sh | 241 ------------------ 2 files changed, 258 deletions(-) delete mode 100755 src/vs/platform/agentHost/test/node/createAndSendMessageAsLocalAgent.sh diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md index 12effd47c4d..f967f6d0c1d 100644 --- a/.agents/skills/launch/SKILL.md +++ b/.agents/skills/launch/SKILL.md @@ -348,20 +348,3 @@ Verify it's gone: # Confirm no process is listening on the debug port lsof -i :9224 # should return nothing ``` - -## Quick Test: Send a Chat Message via Agent Host - -There's a helper script that automates the full flow — launch Code OSS, switch to agent host mode, send a message, and print the response: - -```bash -# From the repo root: -./src/vs/platform/agentHost/test/node/createAndSendMessageAsLocalAgent.sh "Hello, what can you do?" - -# Options: -# --port CDP port (default: 9224) -# --timeout Response wait in seconds (default: 30) -# --no-kill Keep Code OSS running after -# --skip-launch Connect to already-running instance -``` - -This uses the JS mouse-event focus + `press`-per-key approach internally, handles session target switching, and cleans up on exit. diff --git a/src/vs/platform/agentHost/test/node/createAndSendMessageAsLocalAgent.sh b/src/vs/platform/agentHost/test/node/createAndSendMessageAsLocalAgent.sh deleted file mode 100755 index 8a55277df90..00000000000 --- a/src/vs/platform/agentHost/test/node/createAndSendMessageAsLocalAgent.sh +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env bash -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -# Launches Code OSS, switches to Local Agent mode, sends a chat message, -# waits for the response, and prints it to stdout. -# -# Usage: -# ./createAndSendMessageAsLocalAgent.sh "Hello, what can you do?" -# ./createAndSendMessageAsLocalAgent.sh --port 9225 "Explain this code" -# -# Options: -# --port CDP debugging port (default: 9224) -# --timeout Seconds to wait for response (default: 30) -# --no-kill Don't kill Code OSS after the test -# --skip-launch Assume Code OSS is already running on the given port -# -# Requires: agent-browser (npm install -g agent-browser, or use npx) - -set -e - -ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)" -CDP_PORT=9224 -RESPONSE_TIMEOUT=30 -KILL_AFTER=true -SKIP_LAUNCH=false -MESSAGE="" - -# Parse arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --port) - CDP_PORT="$2" - shift 2 - ;; - --timeout) - RESPONSE_TIMEOUT="$2" - shift 2 - ;; - --no-kill) - KILL_AFTER=false - shift - ;; - --skip-launch) - SKIP_LAUNCH=true - shift - ;; - -*) - echo "Unknown option: $1" >&2 - exit 1 - ;; - *) - MESSAGE="$1" - shift - ;; - esac -done - -if [ -z "$MESSAGE" ]; then - echo "Usage: $0 [--port ] [--timeout ] [--no-kill] [--skip-launch] " >&2 - exit 1 -fi - -AB="npx agent-browser" - -cleanup() { - if [ "$KILL_AFTER" = true ] && [ "$SKIP_LAUNCH" = false ]; then - $AB close 2>/dev/null || true - local PID - PID=$(lsof -t -i :"$CDP_PORT" 2>/dev/null || true) - if [ -n "$PID" ]; then - kill "$PID" 2>/dev/null || true - fi - fi -} -trap cleanup EXIT - -# ---- Step 1: Launch Code OSS ------------------------------------------------ - -if [ "$SKIP_LAUNCH" = false ]; then - # Check if already running - if lsof -i :"$CDP_PORT" >/dev/null 2>&1; then - echo "ERROR: Port $CDP_PORT already in use. Use --skip-launch or --port " >&2 - exit 1 - fi - - echo "Launching Code OSS on CDP port $CDP_PORT..." >&2 - cd "$ROOT" - VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh --remote-debugging-port="$CDP_PORT" &>/dev/null & - - # Wait for it to start - echo "Waiting for Code OSS to start..." >&2 - for i in $(seq 1 20); do - if $AB connect "$CDP_PORT" 2>/dev/null; then - break - fi - sleep 2 - if [ "$i" -eq 20 ]; then - echo "ERROR: Code OSS did not start within 40 seconds" >&2 - exit 1 - fi - done -else - echo "Connecting to existing Code OSS on port $CDP_PORT..." >&2 - $AB connect "$CDP_PORT" 2>/dev/null || { - echo "ERROR: Cannot connect to Code OSS on port $CDP_PORT" >&2 - exit 1 - } -fi - -echo "Connected to Code OSS" >&2 - -# ---- Step 2: Switch to Local Agent mode ------------------------------------- - -# Check current session target -CURRENT_TARGET=$($AB snapshot -i 2>&1 | grep "Set Session Target" | head -1) - -if ! echo "$CURRENT_TARGET" | grep -q "Local Agent"; then - echo "Switching to Local Agent mode..." >&2 - - # Find and click the session target button - TARGET_REF=$($AB snapshot -i 2>&1 | grep "Set Session Target" | head -1 | grep -o 'ref=e[0-9]*' | head -1 | sed 's/ref=//') - if [ -z "$TARGET_REF" ]; then - echo "ERROR: Cannot find session target button" >&2 - exit 1 - fi - $AB click "@$TARGET_REF" 2>/dev/null - sleep 0.5 - - # Navigate to Local Agent via arrow keys - # Menu items: Local (checked), Copilot CLI, Cloud, Local Agent, ... - $AB press ArrowDown 2>/dev/null # Copilot CLI - $AB press ArrowDown 2>/dev/null # Cloud - $AB press ArrowDown 2>/dev/null # Local Agent - $AB press Enter 2>/dev/null - sleep 0.5 - - # Verify - VERIFY=$($AB snapshot -i 2>&1 | grep "Set Session Target" | head -1) - if echo "$VERIFY" | grep -q "Local Agent"; then - echo "Switched to Local Agent mode" >&2 - else - echo "WARNING: Could not confirm Local Agent mode. Current: $VERIFY" >&2 - fi -else - echo "Already in Local Agent mode" >&2 -fi - -# ---- Step 3: Focus chat input and type message ------------------------------ - -echo "Sending message: $MESSAGE" >&2 - -# Focus chat input via JavaScript mouse events (universal approach) -$AB eval ' -(() => { - const sidebar = document.querySelector(".part.auxiliarybar"); - if (!sidebar) return "no sidebar"; - const inputPart = sidebar.querySelector(".interactive-input-part"); - if (!inputPart) return "no input part"; - const editor = inputPart.querySelector(".monaco-editor"); - if (!editor) return "no 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 "focused"; -})()' >/dev/null 2>&1 - -sleep 0.3 - -# Clear any existing text -$AB press Meta+a 2>/dev/null -$AB press Backspace 2>/dev/null - -# Type message character by character -for (( i=0; i<${#MESSAGE}; i++ )); do - CHAR="${MESSAGE:$i:1}" - case "$CHAR" in - " ") $AB press Space 2>/dev/null ;; - "?") $AB press Shift+/ 2>/dev/null ;; - "!") $AB press Shift+1 2>/dev/null ;; - ",") $AB press , 2>/dev/null ;; - ".") $AB press . 2>/dev/null ;; - "'") $AB press "'" 2>/dev/null ;; - '"') $AB press 'Shift+'"'" 2>/dev/null ;; - *) $AB press "$CHAR" 2>/dev/null ;; - esac -done - -# Verify text entered -ENTERED=$($AB 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(""); -})()' 2>&1 | tr -d '"') - -echo "Entered text: $ENTERED" >&2 - -# Send the message -$AB press Enter 2>/dev/null - -# ---- Step 4: Wait for response ---------------------------------------------- - -echo "Waiting for response (timeout: ${RESPONSE_TIMEOUT}s)..." >&2 - -RESPONSE="" -for i in $(seq 1 "$RESPONSE_TIMEOUT"); do - sleep 1 - RESPONSE=$($AB eval ' -(() => { - const sidebar = document.querySelector(".part.auxiliarybar"); - if (!sidebar) return ""; - const items = sidebar.querySelectorAll(".interactive-item-container"); - if (items.length < 2) return ""; - // Last item is the response - const lastItem = items[items.length - 1]; - const text = lastItem.textContent || ""; - // Check if it looks like a complete response (has content beyond the header) - if (text.length > 20) return text; - return ""; -})()' 2>&1 | sed 's/^"//;s/"$//') - - if [ -n "$RESPONSE" ]; then - break - fi -done - -if [ -z "$RESPONSE" ]; then - echo "ERROR: No response received within ${RESPONSE_TIMEOUT}s" >&2 - exit 1 -fi - -# ---- Step 5: Output response ------------------------------------------------ - -echo "---" >&2 -echo "$RESPONSE"