Merge remote-tracking branch 'origin/roblou/agent-host' into connor4312/agent-host-server

This commit is contained in:
Rob Lourens
2026-03-12 21:19:14 -07:00
5 changed files with 20 additions and 267 deletions

View File

@@ -372,20 +372,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 <N> CDP port (default: 9224)
# --timeout <N> 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.

View File

@@ -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)) {

View File

@@ -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 {

View File

@@ -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);
@@ -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
@@ -743,10 +754,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 });

View File

@@ -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 <N> CDP debugging port (default: 9224)
# --timeout <N> 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 <N>] [--timeout <N>] [--no-kill] [--skip-launch] <message>" >&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 <other>" >&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"