Remove VSCode extension integration from OpenHands repo (#12234)

This commit is contained in:
Engel Nyst
2026-01-01 19:28:05 +01:00
committed by GitHub
parent 437046f5a4
commit 15bc78f4c1
19 changed files with 0 additions and 10285 deletions

View File

@@ -1,156 +0,0 @@
# Workflow that validates the VSCode extension builds correctly
name: VSCode Extension CI
# * Always run on "main"
# * Run on PRs that have changes in the VSCode extension folder or this workflow
# * Run on tags that start with "ext-v"
on:
push:
branches:
- main
tags:
- 'ext-v*'
pull_request:
paths:
- 'openhands/integrations/vscode/**'
- 'build_vscode.py'
- '.github/workflows/vscode-extension-build.yml'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
# Validate VSCode extension builds correctly
validate-vscode-extension:
name: Validate VSCode Extension Build
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: '22'
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install VSCode extension dependencies
working-directory: ./openhands/integrations/vscode
run: npm ci
- name: Build VSCode extension via build_vscode.py
run: python build_vscode.py
env:
# Ensure we don't skip the build
SKIP_VSCODE_BUILD: ""
- name: Validate .vsix file
run: |
# Verify the .vsix was created and is valid
if [ -f "openhands/integrations/vscode/openhands-vscode-0.0.1.vsix" ]; then
echo "✅ VSCode extension built successfully"
ls -la openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
# Basic validation that the .vsix is a valid zip file
echo "🔍 Validating .vsix structure..."
file openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
unzip -t openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
echo "✅ VSCode extension validation passed"
else
echo "❌ VSCode extension build failed - .vsix not found"
exit 1
fi
- name: Upload VSCode extension artifact
uses: actions/upload-artifact@v6
with:
name: vscode-extension
path: openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
retention-days: 7
- name: Comment on PR with artifact link
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Get file size for display
const vsixPath = 'openhands/integrations/vscode/openhands-vscode-0.0.1.vsix';
const stats = fs.statSync(vsixPath);
const fileSizeKB = Math.round(stats.size / 1024);
const comment = `## 🔧 VSCode Extension Built Successfully!
The VSCode extension has been built and is ready for testing.
**📦 Download**: [openhands-vscode-0.0.1.vsix](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (${fileSizeKB} KB)
**🚀 To install**:
1. Download the artifact from the workflow run above
2. In VSCode: \`Ctrl+Shift+P\` → "Extensions: Install from VSIX..."
3. Select the downloaded \`.vsix\` file
**✅ Tested with**: Node.js 22
**🔍 Validation**: File structure and integrity verified
---
*Built from commit ${{ github.sha }}*`;
// Check if we already commented on this PR and delete it
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('VSCode Extension Built Successfully')
);
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
}
// Create a new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
release:
name: Create GitHub Release
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: validate-vscode-extension
if: startsWith(github.ref, 'refs/tags/ext-v')
steps:
- name: Download .vsix artifact
uses: actions/download-artifact@v6
with:
name: vscode-extension
path: ./
- name: Create Release
uses: ncipollo/release-action@v1.20.0
with:
artifacts: "*.vsix"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
allowUpdates: true

View File

@@ -13,7 +13,6 @@ STAGED_FILES=$(git diff --cached --name-only)
# Check if any files match specific patterns
has_frontend_changes=false
has_backend_changes=false
has_vscode_changes=false
# Check each file individually to avoid issues with grep
for file in $STAGED_FILES; do
@@ -21,17 +20,12 @@ for file in $STAGED_FILES; do
has_frontend_changes=true
elif [[ $file == openhands/* || $file == evaluation/* || $file == tests/* ]]; then
has_backend_changes=true
# Check for VSCode extension changes (subset of backend changes)
if [[ $file == openhands/integrations/vscode/* ]]; then
has_vscode_changes=true
fi
fi
done
echo "Analyzing changes..."
echo "- Frontend changes: $has_frontend_changes"
echo "- Backend changes: $has_backend_changes"
echo "- VSCode extension changes: $has_vscode_changes"
# Run frontend linting if needed
if [ "$has_frontend_changes" = true ]; then
@@ -92,51 +86,6 @@ else
echo "Skipping backend checks (no backend changes detected)."
fi
# Run VSCode extension checks if needed
if [ "$has_vscode_changes" = true ]; then
# Check if we're in a CI environment
if [ -n "$CI" ]; then
echo "Skipping VSCode extension checks (CI environment detected)."
echo "WARNING: VSCode extension files have changed but checks are being skipped."
echo "Please run VSCode extension checks manually before submitting your PR."
else
echo "Running VSCode extension checks..."
if [ -d "openhands/integrations/vscode" ]; then
cd openhands/integrations/vscode || exit 1
echo "Running npm lint:fix..."
npm run lint:fix
if [ $? -ne 0 ]; then
echo "VSCode extension linting failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension linting passed!"
fi
echo "Running npm typecheck..."
npm run typecheck
if [ $? -ne 0 ]; then
echo "VSCode extension type checking failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension type checking passed!"
fi
echo "Running npm compile..."
npm run compile
if [ $? -ne 0 ]; then
echo "VSCode extension compilation failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension compilation passed!"
fi
cd ../../..
fi
fi
else
echo "Skipping VSCode extension checks (no VSCode extension changes detected)."
fi
# If no specific code changes detected, run basic checks
if [ "$has_frontend_changes" = false ] && [ "$has_backend_changes" = false ]; then

View File

@@ -1,113 +0,0 @@
import os
import pathlib
import subprocess
# This script is intended to be run by Poetry during the build process.
# Define the expected name of the .vsix file based on the extension's package.json
# This should match the name and version in openhands-vscode/package.json
EXTENSION_NAME = 'openhands-vscode'
EXTENSION_VERSION = '0.0.1'
VSIX_FILENAME = f'{EXTENSION_NAME}-{EXTENSION_VERSION}.vsix'
# Paths
ROOT_DIR = pathlib.Path(__file__).parent.resolve()
VSCODE_EXTENSION_DIR = ROOT_DIR / 'openhands' / 'integrations' / 'vscode'
def check_node_version():
"""Check if Node.js version is sufficient for building the extension."""
try:
result = subprocess.run(
['node', '--version'], capture_output=True, text=True, check=True
)
version_str = result.stdout.strip()
# Extract major version number (e.g., "v12.22.9" -> 12)
major_version = int(version_str.lstrip('v').split('.')[0])
return major_version >= 18 # Align with frontend actual usage (18.20.1)
except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
return False
def build_vscode_extension():
"""Builds the VS Code extension."""
vsix_path = VSCODE_EXTENSION_DIR / VSIX_FILENAME
# Check if VSCode extension build is disabled via environment variable
if os.environ.get('SKIP_VSCODE_BUILD', '').lower() in ('1', 'true', 'yes'):
print('--- Skipping VS Code extension build (SKIP_VSCODE_BUILD is set) ---')
if vsix_path.exists():
print(f'--- Using existing VS Code extension: {vsix_path} ---')
else:
print('--- No pre-built VS Code extension found ---')
return
# Check Node.js version - if insufficient, use pre-built extension as fallback
if not check_node_version():
print('--- Warning: Node.js version < 18 detected or Node.js not found ---')
print('--- Skipping VS Code extension build (requires Node.js >= 18) ---')
print('--- Using pre-built extension if available ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
else:
print(f'--- Using pre-built VS Code extension: {vsix_path} ---')
return
print(f'--- Building VS Code extension in {VSCODE_EXTENSION_DIR} ---')
try:
# Ensure npm dependencies are installed
print('--- Running npm install for VS Code extension ---')
subprocess.run(
['npm', 'install'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Package the extension
print(f'--- Packaging VS Code extension ({VSIX_FILENAME}) ---')
subprocess.run(
['npm', 'run', 'package-vsix'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Verify the generated .vsix file exists
if not vsix_path.exists():
raise FileNotFoundError(
f'VS Code extension package not found after build: {vsix_path}'
)
print(f'--- VS Code extension built successfully: {vsix_path} ---')
except subprocess.CalledProcessError as e:
print(f'--- Warning: Failed to build VS Code extension: {e} ---')
print('--- Continuing without building extension ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
def build(setup_kwargs):
"""This function is called by Poetry during the build process.
`setup_kwargs` is a dictionary that will be passed to `setuptools.setup()`.
"""
print('--- Running custom Poetry build script (build_vscode.py) ---')
# Build the VS Code extension and place the .vsix file
build_vscode_extension()
# Poetry will handle including files based on pyproject.toml `include` patterns.
# Ensure openhands/integrations/vscode/*.vsix is included there.
print('--- Custom Poetry build script (build_vscode.py) finished ---')
if __name__ == '__main__':
print('Running build_vscode.py directly for testing VS Code extension packaging...')
build_vscode_extension()
print('Direct execution of build_vscode.py finished.')

View File

@@ -1,4 +0,0 @@
out/
node_modules/
.vscode-test/
*.vsix

View File

@@ -1,68 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"airbnb-base",
"airbnb-typescript/base",
"prettier",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["prettier", "unused-imports"],
"rules": {
"unused-imports/no-unused-imports": "error",
"prettier/prettier": ["error"],
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871
"import/extensions": [
"error",
"ignorePackages",
{
"": "never",
"ts": "never"
}
],
// Allow state modification in reduce and similar patterns
"no-param-reassign": [
"error",
{
"props": true,
"ignorePropertyModificationsFor": ["acc", "state"]
}
],
// For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra
"no-restricted-syntax": "off",
"import/prefer-default-export": "off",
"no-underscore-dangle": "off",
"import/no-extraneous-dependencies": "off",
// VSCode extension specific - allow console for debugging
"no-console": "warn",
// Allow leading underscores for private variables in VSCode extensions
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "allow"
}
]
},
"env": {
"node": true,
"es6": true
},
"overrides": [
{
"files": ["src/test/**/*.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"no-console": "off",
"@typescript-eslint/no-shadow": "off",
"consistent-return": "off"
}
}
]
}

View File

@@ -1,18 +0,0 @@
# Node modules
node_modules/
# Compiled TypeScript output
out/
# VS Code Extension packaging
*.vsix
# TypeScript build info
*.tsbuildinfo
# Test run output (if any specific folders are generated)
.vscode-test/
# OS-generated files
.DS_Store
Thumbs.db

View File

@@ -1,3 +0,0 @@
{
"trailingComma": "all"
}

View File

@@ -1,11 +0,0 @@
.vscodeignore
.gitignore
*.vsix
node_modules/
out/src/ # We only need out/extension.js and out/extension.js.map
src/
*.tsbuildinfo
tsconfig.json
PLAN.md
README.md
# Add other files/folders to ignore during packaging if needed

View File

@@ -1,97 +0,0 @@
# VSCode Extension Development
This document provides instructions for developing and contributing to the OpenHands VSCode extension.
## Setup
To get started with development, you need to install the dependencies.
```bash
npm install
```
## Building the Extension
The VSCode extension is automatically built during the main OpenHands `pip install` process. However, you can also build it manually.
- **Package the extension:** This creates a `.vsix` file that can be installed in VSCode.
```bash
npm run package-vsix
```
- **Compile TypeScript:** This compiles the source code without creating a package.
```bash
npm run compile
```
## Code Quality and Testing
We use ESLint, Prettier, and TypeScript for code quality.
- **Run linting with auto-fixes:**
```bash
npm run lint:fix
```
- **Run type checking:**
```bash
npm run typecheck
```
- **Run tests:**
```bash
npm run test
```
## Releasing a New Version
The extension has its own version number and is released independently of the main OpenHands application. The release process is automated via the `vscode-extension-build.yml` GitHub Actions workflow and is triggered by pushing a specially formatted Git tag.
### 1. Update the Version Number
Before creating a release, you must first bump the version number in the extension's `package.json` file.
1. Open `openhands/integrations/vscode/package.json`.
2. Find the `"version"` field and update it according to [Semantic Versioning](https://semver.org/) (e.g., from `"0.0.1"` to `"0.0.2"`).
### 2. Commit the Version Bump
Commit the change to `package.json` with a clear commit message.
```bash
git add openhands/integrations/vscode/package.json
git commit -m "chore(vscode): bump version to 0.0.2"
```
### 3. Create and Push the Tag
The release is triggered by a Git tag that **must** match the version in `package.json` and be prefixed with `ext-v`.
1. **Create an annotated tag.** The tag name must be `ext-v` followed by the version number you just set.
```bash
# Example for version 0.0.2
git tag -a ext-v0.0.2 -m "Release VSCode extension v0.0.2"
```
2. **Push the commit and the tag** to the `upstream` remote.
```bash
# Push the branch with the version bump commit
git push upstream <your-branch-name>
# Push the specific tag
git push upstream ext-v0.0.2
```
### 4. Finalize the Release on GitHub
Pushing the tag will automatically trigger the `VSCode Extension CI` workflow. This workflow will:
1. Build the `.vsix` file.
2. Create a new **draft release** on GitHub with the `.vsix` file attached as an asset.
To finalize the release:
1. Go to the "Releases" page of the OpenHands repository on GitHub.
2. Find the new draft release (e.g., `ext-v0.0.2`).
3. Click "Edit" to write the release notes, describing the new features and bug fixes.
4. Click the **"Publish release"** button.
The release is now public and available for users.

View File

@@ -1,25 +0,0 @@
The MIT License (MIT)
=====================
Copyright © 2025
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the “Software”), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,48 +0,0 @@
# OpenHands VS Code Extension
The official OpenHands companion extension for Visual Studio Code.
This extension seamlessly integrates OpenHands into your VSCode workflow, allowing you to start coding sessions with your AI agent directly from your editor.
![OpenHands VSCode Extension Demo](https://raw.githubusercontent.com/OpenHands/OpenHands/main/assets/images/vscode-extension-demo.gif)
## Features
- **Start a New Conversation**: Launch OpenHands in a new terminal with a single command.
- **Use Your Current File**: Automatically send the content of your active file to OpenHands to start a task.
- **Use a Selection**: Send only the highlighted text from your editor to OpenHands for focused tasks.
- **Safe Terminal Management**: The extension intelligently reuses idle terminals or creates new ones, ensuring it never interrupts an active process.
- **Automatic Virtual Environment Detection**: Finds and uses your project's Python virtual environment (`.venv`, `venv`, etc.) automatically.
## How to Use
You can access the extension's commands in two ways:
1. **Command Palette**:
- Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`).
- Type `OpenHands` to see the available commands.
- Select the command you want to run.
2. **Editor Context Menu**:
- Right-click anywhere in your text editor.
- The OpenHands commands will appear in the context menu.
## Installation
For the best experience, the OpenHands CLI will attempt to install the extension for you automatically the first time you run it inside VSCode.
If you need to install it manually:
1. Download the latest `.vsix` file from the [GitHub Releases page](https://github.com/OpenHands/OpenHands/releases).
2. In VSCode, open the Command Palette (`Ctrl+Shift+P`).
3. Run the **"Extensions: Install from VSIX..."** command.
4. Select the `.vsix` file you downloaded.
## Requirements
- **OpenHands CLI**: You must have `openhands` installed and available in your system's PATH.
- **VS Code**: Version 1.98.2 or newer.
- **Shell**: For the best terminal reuse experience, a shell with [Shell Integration](https://code.visualstudio.com/docs/terminal/shell-integration) is recommended (e.g., modern versions of bash, zsh, PowerShell, or fish).
## Contributing
We welcome contributions! If you're interested in developing the extension, please see the `DEVELOPMENT.md` file in our source repository for instructions on how to get started.

File diff suppressed because it is too large Load Diff

View File

@@ -1,110 +0,0 @@
{
"name": "openhands-vscode",
"displayName": "OpenHands Integration",
"description": "Integrates OpenHands with VS Code for easy conversation starting and context passing.",
"version": "0.0.1",
"publisher": "openhands",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/openhands/OpenHands.git"
},
"engines": {
"vscode": "^1.98.2",
"node": ">=18.0.0"
},
"activationEvents": [
"onCommand:openhands.startConversation",
"onCommand:openhands.startConversationWithFileContext",
"onCommand:openhands.startConversationWithSelectionContext"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "openhands.startConversation",
"title": "Start New Conversation",
"category": "OpenHands"
},
{
"command": "openhands.startConversationWithFileContext",
"title": "Start with File Content",
"category": "OpenHands"
},
{
"command": "openhands.startConversationWithSelectionContext",
"title": "Start with Selected Text",
"category": "OpenHands"
}
],
"submenus": [
{
"id": "openhands.contextMenu",
"label": "OpenHands"
}
],
"menus": {
"editor/context": [
{
"submenu": "openhands.contextMenu",
"group": "navigation@1"
}
],
"openhands.contextMenu": [
{
"when": "editorHasSelection",
"command": "openhands.startConversationWithSelectionContext",
"group": "1@1"
},
{
"command": "openhands.startConversationWithFileContext",
"group": "1@2"
}
],
"commandPalette": [
{
"command": "openhands.startConversation",
"when": "true"
},
{
"command": "openhands.startConversationWithFileContext",
"when": "editorIsOpen"
},
{
"command": "openhands.startConversationWithSelectionContext",
"when": "editorHasSelection"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"test": "npm run compile && node ./out/test/runTest.js",
"package-vsix": "npm run compile && npx vsce package --no-dependencies",
"lint": "npm run typecheck && eslint src --ext .ts && prettier --check src/**/*.ts",
"lint:fix": "eslint src --ext .ts --fix && prettier --write src/**/*.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/vscode": "^1.98.2",
"typescript": "^5.0.0",
"@types/mocha": "^10.0.6",
"mocha": "^10.4.0",
"@vscode/test-electron": "^2.3.9",
"@types/node": "^20.12.12",
"@types/glob": "^8.1.0",
"@vscode/vsce": "^3.5.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.5.0",
"eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.5.3"
}
}

View File

@@ -1,380 +0,0 @@
import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
// Create output channel for debug logging
const outputChannel = vscode.window.createOutputChannel("OpenHands Debug");
/**
* This implementation uses VSCode's Shell Integration API.
*
* VSCode API References:
* - Terminal Shell Integration: https://code.visualstudio.com/docs/terminal/shell-integration
* - VSCode Extension API: https://code.visualstudio.com/api/references/vscode-api
* - Terminal API Reference: https://code.visualstudio.com/api/references/vscode-api#Terminal
* - VSCode Source Examples: https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.d.ts
*
* Shell Integration Requirements:
* - Compatible shells: bash, zsh, PowerShell Core, or fish shell
* - Graceful fallback needed for Command Prompt and other shells
*/
// Track terminals that we know are idle (just finished our commands)
const idleTerminals = new Set<string>();
/**
* Marks a terminal as idle after our command completes
* @param terminalName The name of the terminal
*/
function markTerminalAsIdle(terminalName: string): void {
idleTerminals.add(terminalName);
}
/**
* Marks a terminal as busy when we start a command
* @param terminalName The name of the terminal
*/
function markTerminalAsBusy(terminalName: string): void {
idleTerminals.delete(terminalName);
}
/**
* Checks if we know a terminal is idle (safe to reuse)
* @param terminal The terminal to check
* @returns boolean true if we know it's idle, false otherwise
*/
function isKnownIdleTerminal(terminal: vscode.Terminal): boolean {
return idleTerminals.has(terminal.name);
}
/**
* Creates a new OpenHands terminal with timestamp
* @returns vscode.Terminal
*/
function createNewOpenHandsTerminal(): vscode.Terminal {
const timestamp = new Date().toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
});
const terminalName = `OpenHands ${timestamp}`;
return vscode.window.createTerminal(terminalName);
}
/**
* Finds an existing OpenHands terminal or creates a new one using safe detection
* @returns vscode.Terminal
*/
function findOrCreateOpenHandsTerminal(): vscode.Terminal {
const openHandsTerminals = vscode.window.terminals.filter((terminal) =>
terminal.name.startsWith("OpenHands"),
);
if (openHandsTerminals.length > 0) {
// Use the most recent terminal, but only if we know it's idle
const terminal = openHandsTerminals[openHandsTerminals.length - 1];
// Only reuse terminals that we know are idle (safe to reuse)
if (isKnownIdleTerminal(terminal)) {
return terminal;
}
// If we don't know the terminal is idle, create a new one to avoid interrupting running processes
return createNewOpenHandsTerminal();
}
// No existing terminals, create new one
return createNewOpenHandsTerminal();
}
/**
* Executes an OpenHands command using Shell Integration when available
* @param terminal The terminal to execute the command in
* @param command The command to execute
*/
function executeOpenHandsCommand(
terminal: vscode.Terminal,
command: string,
): void {
// Mark terminal as busy when we start a command
markTerminalAsBusy(terminal.name);
if (terminal.shellIntegration) {
// Use Shell Integration for better control
const execution = terminal.shellIntegration.executeCommand(command);
// Monitor execution completion
const disposable = vscode.window.onDidEndTerminalShellExecution((event) => {
if (event.execution === execution) {
if (event.exitCode === 0) {
outputChannel.appendLine(
"DEBUG: OpenHands command completed successfully",
);
// Mark terminal as idle when command completes successfully
markTerminalAsIdle(terminal.name);
} else if (event.exitCode !== undefined) {
outputChannel.appendLine(
`DEBUG: OpenHands command exited with code ${event.exitCode}`,
);
// Mark terminal as idle even if command failed (user can reuse it)
markTerminalAsIdle(terminal.name);
}
disposable.dispose(); // Clean up the event listener
}
});
} else {
// Fallback to traditional sendText
terminal.sendText(command, true);
// For traditional sendText, we can't track completion, so don't mark as idle
// This means terminals without Shell Integration won't be reused, which is safer
}
}
/**
* Detects and builds virtual environment activation command
* @returns string The activation command prefix (empty if no venv found)
*/
function detectVirtualEnvironment(): string {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
outputChannel.appendLine("DEBUG: No workspace folder found");
return "";
}
const venvPaths = [".venv", "venv", ".virtualenv"];
for (const venvPath of venvPaths) {
const venvFullPath = path.join(workspaceFolder.uri.fsPath, venvPath);
if (fs.existsSync(venvFullPath)) {
outputChannel.appendLine(`DEBUG: Found venv at ${venvFullPath}`);
if (process.platform === "win32") {
// For Windows, the activation command is different and typically doesn't use 'source'
// It's often a script that needs to be executed.
// This is a simplified version. A more robust solution might need to check for PowerShell, cmd, etc.
return `& "${path.join(venvFullPath, "Scripts", "Activate.ps1")}" && `;
}
// For POSIX-like shells
return `source "${path.join(venvFullPath, "bin", "activate")}" && `;
}
}
outputChannel.appendLine(
`DEBUG: No venv found in workspace ${workspaceFolder.uri.fsPath}`,
);
return "";
}
/**
* Creates a contextual task message for file content
* @param filePath The file path (or "Untitled" for unsaved files)
* @param content The file content
* @param languageId The programming language ID
* @returns string A descriptive task message
*/
function createFileContextMessage(
filePath: string,
content: string,
languageId?: string,
): string {
const fileName =
filePath === "Untitled" ? "an untitled file" : `file ${filePath}`;
const langInfo = languageId ? ` (${languageId})` : "";
return `User opened ${fileName}${langInfo}. Here's the content:
\`\`\`${languageId || ""}
${content}
\`\`\`
Please ask the user what they want to do with this file.`;
}
/**
* Creates a contextual task message for selected text
* @param filePath The file path (or "Untitled" for unsaved files)
* @param content The selected content
* @param startLine 1-based start line number
* @param endLine 1-based end line number
* @param languageId The programming language ID
* @returns string A descriptive task message
*/
function createSelectionContextMessage(
filePath: string,
content: string,
startLine: number,
endLine: number,
languageId?: string,
): string {
const fileName =
filePath === "Untitled" ? "an untitled file" : `file ${filePath}`;
const langInfo = languageId ? ` (${languageId})` : "";
const lineInfo =
startLine === endLine
? `line ${startLine}`
: `lines ${startLine}-${endLine}`;
return `User selected ${lineInfo} in ${fileName}${langInfo}. Here's the selected content:
\`\`\`${languageId || ""}
${content}
\`\`\`
Please ask the user what they want to do with this selection.`;
}
/**
* Builds the OpenHands command with proper sanitization
* @param options Command options
* @param activationCommand Virtual environment activation prefix
* @returns string The complete command to execute
*/
function buildOpenHandsCommand(
options: { task?: string; filePath?: string },
activationCommand: string,
): string {
let commandToSend = `${activationCommand}openhands`;
if (options.filePath) {
// Ensure filePath is properly quoted if it contains spaces or special characters
const safeFilePath = options.filePath.includes(" ")
? `"${options.filePath}"`
: options.filePath;
commandToSend = `${activationCommand}openhands --file ${safeFilePath}`;
} else if (options.task) {
// Sanitize task string for command line (basic sanitization)
// Replace backticks and double quotes that might break the command
const sanitizedTask = options.task
.replace(/`/g, "\\`")
.replace(/"/g, '\\"');
commandToSend = `${activationCommand}openhands --task "${sanitizedTask}"`;
}
return commandToSend;
}
/**
* Main function to start OpenHands in terminal with safe terminal reuse
* @param options Command options
*/
function startOpenHandsInTerminal(options: {
task?: string;
filePath?: string;
}): void {
try {
// Find or create terminal using safe detection
const terminal = findOrCreateOpenHandsTerminal();
terminal.show(true); // true to preserve focus on the editor
// Detect virtual environment
const activationCommand = detectVirtualEnvironment();
// Build command
const commandToSend = buildOpenHandsCommand(options, activationCommand);
// Debug: show the actual command being sent
outputChannel.appendLine(`DEBUG: Sending command: ${commandToSend}`);
// Execute command using Shell Integration when available
executeOpenHandsCommand(terminal, commandToSend);
} catch (error) {
vscode.window.showErrorMessage(`Error starting OpenHands: ${error}`);
}
}
export function activate(context: vscode.ExtensionContext) {
// Clean up terminal tracking when terminals are closed
const terminalCloseDisposable = vscode.window.onDidCloseTerminal(
(terminal) => {
idleTerminals.delete(terminal.name);
},
);
context.subscriptions.push(terminalCloseDisposable);
// Command: Start New Conversation
const startConversationDisposable = vscode.commands.registerCommand(
"openhands.startConversation",
() => {
startOpenHandsInTerminal({});
},
);
context.subscriptions.push(startConversationDisposable);
// Command: Start Conversation with Active File Content
const startWithFileContextDisposable = vscode.commands.registerCommand(
"openhands.startConversationWithFileContext",
() => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
// No active editor, start conversation without task
startOpenHandsInTerminal({});
return;
}
if (editor.document.isUntitled) {
const fileContent = editor.document.getText();
if (!fileContent.trim()) {
// Empty untitled file, start conversation without task
startOpenHandsInTerminal({});
return;
}
// Create contextual message for untitled file
const contextualTask = createFileContextMessage(
"Untitled",
fileContent,
editor.document.languageId,
);
startOpenHandsInTerminal({ task: contextualTask });
} else {
const filePath = editor.document.uri.fsPath;
// For saved files, we can still use --file flag for better performance,
// but we could also create a contextual message if preferred
startOpenHandsInTerminal({ filePath });
}
},
);
context.subscriptions.push(startWithFileContextDisposable);
// Command: Start Conversation with Selected Text
const startWithSelectionContextDisposable = vscode.commands.registerCommand(
"openhands.startConversationWithSelectionContext",
() => {
outputChannel.appendLine(
"DEBUG: startConversationWithSelectionContext command triggered!",
);
const editor = vscode.window.activeTextEditor;
if (!editor) {
// No active editor, start conversation without task
startOpenHandsInTerminal({});
return;
}
if (editor.selection.isEmpty) {
// No text selected, start conversation without task
startOpenHandsInTerminal({});
return;
}
const selectedText = editor.document.getText(editor.selection);
const startLine = editor.selection.start.line + 1; // Convert to 1-based
const endLine = editor.selection.end.line + 1; // Convert to 1-based
const filePath = editor.document.isUntitled
? "Untitled"
: editor.document.uri.fsPath;
// Create contextual message with line numbers and file info
const contextualTask = createSelectionContextMessage(
filePath,
selectedText,
startLine,
endLine,
editor.document.languageId,
);
startOpenHandsInTerminal({ task: contextualTask });
},
);
context.subscriptions.push(startWithSelectionContextDisposable);
}
export function deactivate() {
// Clean up resources if needed, though for this simple extension,
// VS Code handles terminal disposal.
}

View File

@@ -1,22 +0,0 @@
import * as path from "path";
import { runTests } from "@vscode/test-electron";
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, "../../../");
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, "./suite/index"); // Points to the compiled version of suite/index.ts
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error("Failed to run tests");
process.exit(1);
}
}
main();

View File

@@ -1,848 +0,0 @@
import * as assert from "assert";
import * as vscode from "vscode";
suite("Extension Test Suite", () => {
let mockTerminal: vscode.Terminal;
let sendTextSpy: any; // Manual spy, using 'any' type
let showSpy: any; // Manual spy
let createTerminalStub: any; // Manual stub
let findTerminalStub: any; // Manual spy
let showErrorMessageSpy: any; // Manual spy
// It's better to use a proper mocking library like Sinon.JS for spies and stubs.
// For now, we'll use a simplified manual approach for spies.
const createManualSpy = () => {
const spy: any = (...args: any[]) => {
// eslint-disable-line @typescript-eslint/no-explicit-any
spy.called = true;
spy.callCount = (spy.callCount || 0) + 1;
spy.lastArgs = args;
spy.argsHistory = spy.argsHistory || [];
spy.argsHistory.push(args);
};
spy.called = false;
spy.callCount = 0;
spy.lastArgs = null;
spy.argsHistory = [];
spy.resetHistory = () => {
spy.called = false;
spy.callCount = 0;
spy.lastArgs = null;
spy.argsHistory = [];
};
return spy;
};
setup(() => {
// Reset spies and stubs before each test
sendTextSpy = createManualSpy();
showSpy = createManualSpy();
showErrorMessageSpy = createManualSpy();
mockTerminal = {
name: "OpenHands",
processId: Promise.resolve(123),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined, // Added to satisfy Terminal interface
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
}, // Added shell property
shellIntegration: undefined, // No Shell Integration in tests by default
};
// Store original functions
const _originalCreateTerminal = vscode.window.createTerminal;
const _originalTerminalsDescriptor = Object.getOwnPropertyDescriptor(
vscode.window,
"terminals",
);
const _originalShowErrorMessage = vscode.window.showErrorMessage;
// Stub vscode.window.createTerminal
createTerminalStub = createManualSpy();
vscode.window.createTerminal = (...args: any[]): vscode.Terminal => {
createTerminalStub(...args); // Call the spy with whatever arguments it received
return mockTerminal; // Return the mock terminal
};
// Stub vscode.window.terminals
findTerminalStub = createManualSpy(); // To track if vscode.window.terminals getter is accessed
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
// Default to returning the mockTerminal, can be overridden in specific tests
return [mockTerminal];
},
configurable: true,
});
vscode.window.showErrorMessage = showErrorMessageSpy as any;
// Restore default mock behavior before each test
setup(() => {
// Reset spies
createTerminalStub.resetHistory();
sendTextSpy.resetHistory();
showSpy.resetHistory();
findTerminalStub.resetHistory();
showErrorMessageSpy.resetHistory();
// Restore default createTerminal mock
vscode.window.createTerminal = (...args: any[]): vscode.Terminal => {
createTerminalStub(...args);
return mockTerminal; // Return the default mock terminal (no Shell Integration)
};
// Restore default terminals mock
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [mockTerminal]; // Default to returning the mockTerminal
},
configurable: true,
});
});
// Teardown logic to restore original functions
teardown(() => {
vscode.window.createTerminal = _originalCreateTerminal;
if (_originalTerminalsDescriptor) {
Object.defineProperty(
vscode.window,
"terminals",
_originalTerminalsDescriptor,
);
} else {
// If it wasn't originally defined, delete it to restore to that state
delete (vscode.window as any).terminals;
}
vscode.window.showErrorMessage = _originalShowErrorMessage;
});
});
test("Extension should be present and activate", async () => {
const extension = vscode.extensions.getExtension(
"openhands.openhands-vscode",
);
assert.ok(
extension,
"Extension should be found (check publisher.name in package.json)",
);
if (!extension.isActive) {
await extension.activate();
}
assert.ok(extension.isActive, "Extension should be active");
});
test("Commands should be registered", async () => {
const extension = vscode.extensions.getExtension(
"openhands.openhands-vscode",
);
if (extension && !extension.isActive) {
await extension.activate();
}
const commands = await vscode.commands.getCommands(true);
const expectedCommands = [
"openhands.startConversation",
"openhands.startConversationWithFileContext",
"openhands.startConversationWithSelectionContext",
];
for (const cmd of expectedCommands) {
assert.ok(
commands.includes(cmd),
`Command '${cmd}' should be registered`,
);
}
});
test("openhands.startConversation should send correct command to terminal", async () => {
findTerminalStub.resetHistory(); // Reset for this specific test path if needed
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [];
},
configurable: true,
}); // Simulate no existing terminal
await vscode.commands.executeCommand("openhands.startConversation");
assert.ok(
createTerminalStub.called,
"vscode.window.createTerminal should be called",
);
assert.ok(showSpy.called, "terminal.show should be called");
assert.deepStrictEqual(
sendTextSpy.lastArgs,
["openhands", true],
"Correct command sent to terminal",
);
});
test("openhands.startConversationWithFileContext (saved file) should send --file command", async () => {
const testFilePath = "/test/file.py";
// Mock activeTextEditor for a saved file
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => ({
document: {
isUntitled: false,
uri: vscode.Uri.file(testFilePath),
fsPath: testFilePath, // fsPath is often used
getText: () => "file content", // Not used for saved files but good to have
},
}),
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithFileContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
assert.deepStrictEqual(sendTextSpy.lastArgs, [
`openhands --file ${testFilePath.includes(" ") ? `"${testFilePath}"` : testFilePath}`,
true,
]);
// Restore activeTextEditor
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("openhands.startConversationWithFileContext (untitled file) should send contextual --task command", async () => {
const untitledFileContent = "untitled content";
const languageId = "javascript";
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => ({
document: {
isUntitled: true,
uri: vscode.Uri.parse("untitled:Untitled-1"),
getText: () => untitledFileContent,
languageId,
},
}),
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithFileContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
// Check that the command contains the contextual message
const expectedMessage = `User opened an untitled file (${languageId}). Here's the content:
\`\`\`${languageId}
${untitledFileContent}
\`\`\`
Please ask the user what they want to do with this file.`;
// Apply the same sanitization as the actual implementation
const sanitizedMessage = expectedMessage
.replace(/`/g, "\\`")
.replace(/"/g, '\\"');
assert.deepStrictEqual(sendTextSpy.lastArgs, [
`openhands --task "${sanitizedMessage}"`,
true,
]);
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("openhands.startConversationWithFileContext (no editor) should start conversation without context", async () => {
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => undefined,
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithFileContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
assert.deepStrictEqual(sendTextSpy.lastArgs, ["openhands", true]);
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("openhands.startConversationWithSelectionContext should send contextual --task with selection", async () => {
const selectedText = "selected text for openhands";
const filePath = "/test/file.py";
const languageId = "python";
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => ({
document: {
isUntitled: false,
uri: vscode.Uri.file(filePath),
fsPath: filePath,
languageId,
getText: (selection?: vscode.Selection) =>
selection ? selectedText : "full content",
},
selection: {
isEmpty: false,
active: new vscode.Position(0, 0),
anchor: new vscode.Position(0, 0),
start: new vscode.Position(0, 0), // Line 0 (0-based)
end: new vscode.Position(0, 10), // Line 0 (0-based)
} as vscode.Selection, // Mock non-empty selection on line 1
}),
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithSelectionContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
// Check that the command contains the contextual message with line numbers
const expectedMessage = `User selected line 1 in file ${filePath} (${languageId}). Here's the selected content:
\`\`\`${languageId}
${selectedText}
\`\`\`
Please ask the user what they want to do with this selection.`;
// Apply the same sanitization as the actual implementation
const sanitizedMessage = expectedMessage
.replace(/`/g, "\\`")
.replace(/"/g, '\\"');
assert.deepStrictEqual(sendTextSpy.lastArgs, [
`openhands --task "${sanitizedMessage}"`,
true,
]);
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("openhands.startConversationWithSelectionContext (no selection) should start conversation without context", async () => {
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => ({
document: {
isUntitled: false,
uri: vscode.Uri.file("/test/file.py"),
getText: () => "full content",
},
selection: { isEmpty: true } as vscode.Selection, // Mock empty selection
}),
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithSelectionContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
assert.deepStrictEqual(sendTextSpy.lastArgs, ["openhands", true]);
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("openhands.startConversationWithSelectionContext should handle multi-line selections", async () => {
const selectedText = "line 1\nline 2\nline 3";
const filePath = "/test/multiline.js";
const languageId = "javascript";
const originalActiveTextEditor = Object.getOwnPropertyDescriptor(
vscode.window,
"activeTextEditor",
);
Object.defineProperty(vscode.window, "activeTextEditor", {
get: () => ({
document: {
isUntitled: false,
uri: vscode.Uri.file(filePath),
fsPath: filePath,
languageId,
getText: (selection?: vscode.Selection) =>
selection ? selectedText : "full content",
},
selection: {
isEmpty: false,
active: new vscode.Position(4, 0),
anchor: new vscode.Position(4, 0),
start: new vscode.Position(4, 0), // Line 4 (0-based) = Line 5 (1-based)
end: new vscode.Position(6, 10), // Line 6 (0-based) = Line 7 (1-based)
} as vscode.Selection, // Mock multi-line selection from line 5 to 7
}),
configurable: true,
});
await vscode.commands.executeCommand(
"openhands.startConversationWithSelectionContext",
);
assert.ok(sendTextSpy.called, "terminal.sendText should be called");
// Check that the command contains the contextual message with line range
const expectedMessage = `User selected lines 5-7 in file ${filePath} (${languageId}). Here's the selected content:
\`\`\`${languageId}
${selectedText}
\`\`\`
Please ask the user what they want to do with this selection.`;
// Apply the same sanitization as the actual implementation
const sanitizedMessage = expectedMessage
.replace(/`/g, "\\`")
.replace(/"/g, '\\"');
assert.deepStrictEqual(sendTextSpy.lastArgs, [
`openhands --task "${sanitizedMessage}"`,
true,
]);
if (originalActiveTextEditor) {
Object.defineProperty(
vscode.window,
"activeTextEditor",
originalActiveTextEditor,
);
}
});
test("Terminal reuse should work when existing OpenHands terminal exists", async () => {
// Create a mock existing terminal
const existingTerminal = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: undefined, // No Shell Integration, should create new terminal
};
// Mock terminals array to return existing terminal
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [existingTerminal];
},
configurable: true,
});
await vscode.commands.executeCommand("openhands.startConversation");
// Should create new terminal since no Shell Integration
assert.ok(
createTerminalStub.called,
"Should create new terminal when no Shell Integration available",
);
});
test("Terminal reuse with Shell Integration should reuse existing terminal", async () => {
// Create mock Shell Integration
const mockExecution = {
read: () => ({
async *[Symbol.asyncIterator]() {
yield "OPENHANDS_PROBE_123456789";
},
}),
exitCode: Promise.resolve(0),
};
const mockShellIntegration = {
executeCommand: () => mockExecution,
};
// Create a mock existing terminal with Shell Integration
const existingTerminalWithShell = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: mockShellIntegration,
};
// Mock terminals array to return existing terminal with Shell Integration
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [existingTerminalWithShell];
},
configurable: true,
});
// Reset create terminal stub to track if new terminal is created
createTerminalStub.resetHistory();
await vscode.commands.executeCommand("openhands.startConversation");
// Should reuse existing terminal since Shell Integration is available
// Note: The probe might timeout in test environment, but it should still reuse the terminal
assert.ok(showSpy.called, "terminal.show should be called");
});
test("Shell Integration should use executeCommand for OpenHands commands", async () => {
const executeCommandSpy = createManualSpy();
// Mock execution for OpenHands command
const mockExecution = {
read: () => ({
async *[Symbol.asyncIterator]() {
yield "OpenHands started successfully";
},
}),
exitCode: Promise.resolve(0),
commandLine: {
value: "openhands",
isTrusted: true,
confidence: 2,
},
cwd: vscode.Uri.file("/test/directory"),
};
const mockShellIntegration = {
executeCommand: (command: string) => {
executeCommandSpy(command);
return mockExecution;
},
cwd: vscode.Uri.file("/test/directory"),
};
// Create a terminal with Shell Integration that will be created by createTerminal
const terminalWithShell = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: mockShellIntegration,
};
// Mock createTerminal to return a terminal with Shell Integration
createTerminalStub.resetHistory();
vscode.window.createTerminal = (...args: any[]): vscode.Terminal => {
createTerminalStub(...args);
return terminalWithShell; // Return terminal with Shell Integration
};
// Mock empty terminals array so we create a new one
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return []; // No existing terminals
},
configurable: true,
});
await vscode.commands.executeCommand("openhands.startConversation");
// Should have called executeCommand for OpenHands command
assert.ok(
executeCommandSpy.called,
"Shell Integration executeCommand should be called for OpenHands command",
);
// Check that the command was an OpenHands command
const openhandsCall = executeCommandSpy.argsHistory.find(
(args: any[]) => args[0] && args[0].includes("openhands"),
);
assert.ok(
openhandsCall,
`Should execute OpenHands command. Actual calls: ${JSON.stringify(executeCommandSpy.argsHistory)}`,
);
// Should create new terminal since none exist
assert.ok(
createTerminalStub.called,
"Should create new terminal when none exist",
);
});
test("Idle terminal tracking should reuse known idle terminals", async () => {
const executeCommandSpy = createManualSpy();
// Mock execution for OpenHands command
const mockExecution = {
read: () => ({
async *[Symbol.asyncIterator]() {
yield "OpenHands started successfully";
},
}),
exitCode: Promise.resolve(0),
commandLine: {
value: "openhands",
isTrusted: true,
confidence: 2,
},
cwd: vscode.Uri.file("/test/directory"),
};
const mockShellIntegration = {
executeCommand: (command: string) => {
executeCommandSpy(command);
return mockExecution;
},
cwd: vscode.Uri.file("/test/directory"),
};
const terminalWithShell = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: mockShellIntegration,
};
// First, manually mark the terminal as idle (simulating a previous successful command)
// We need to access the extension's internal idle tracking
// For testing, we'll simulate this by running a command first, then another
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [terminalWithShell];
},
configurable: true,
});
createTerminalStub.resetHistory();
// First command to establish the terminal as idle
await vscode.commands.executeCommand("openhands.startConversation");
// Simulate command completion to mark terminal as idle
// This would normally happen via the onDidEndTerminalShellExecution event
createTerminalStub.resetHistory();
executeCommandSpy.resetHistory();
// Second command should reuse the terminal if it's marked as idle
await vscode.commands.executeCommand("openhands.startConversation");
// Should show terminal
assert.ok(showSpy.called, "Should show terminal");
});
test("Shell Integration should use executeCommand when available", async () => {
const executeCommandSpy = createManualSpy();
const mockExecution = {
read: () => ({
async *[Symbol.asyncIterator]() {
yield "OpenHands started successfully";
},
}),
exitCode: Promise.resolve(0),
commandLine: {
value: "openhands",
isTrusted: true,
confidence: 2,
},
cwd: vscode.Uri.file("/test/directory"),
};
const mockShellIntegration = {
executeCommand: (command: string) => {
executeCommandSpy(command);
return mockExecution;
},
cwd: vscode.Uri.file("/test/directory"),
};
const terminalWithShell = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: mockShellIntegration,
};
// Mock createTerminal to return a terminal with Shell Integration
createTerminalStub.resetHistory();
vscode.window.createTerminal = (...args: any[]): vscode.Terminal => {
createTerminalStub(...args);
return terminalWithShell; // Return terminal with Shell Integration
};
// Mock empty terminals array so we create a new one
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return []; // No existing terminals
},
configurable: true,
});
sendTextSpy.resetHistory();
executeCommandSpy.resetHistory();
await vscode.commands.executeCommand("openhands.startConversation");
// Should use Shell Integration executeCommand, not sendText
assert.ok(
executeCommandSpy.called,
"Should use Shell Integration executeCommand",
);
// The OpenHands command should be executed via Shell Integration
const openhandsCommand = executeCommandSpy.argsHistory.find(
(args: any[]) => args[0] && args[0].includes("openhands"),
);
assert.ok(
openhandsCommand,
"Should execute OpenHands command via Shell Integration",
);
});
test("Terminal creation should work when no existing terminals", async () => {
// Mock empty terminals array
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return []; // No existing terminals
},
configurable: true,
});
createTerminalStub.resetHistory();
await vscode.commands.executeCommand("openhands.startConversation");
// Should create new terminal when none exist
assert.ok(
createTerminalStub.called,
"Should create new terminal when none exist",
);
// Should show the new terminal
assert.ok(showSpy.called, "Should show the new terminal");
});
test("Shell Integration fallback should work when Shell Integration unavailable", async () => {
// Create terminal without Shell Integration
const terminalWithoutShell = {
name: "OpenHands 10:30:15",
processId: Promise.resolve(456),
sendText: sendTextSpy as any,
show: showSpy as any,
hide: () => {},
dispose: () => {},
creationOptions: {},
exitStatus: undefined,
state: {
isInteractedWith: false,
shell: undefined as string | undefined,
},
shellIntegration: undefined, // No Shell Integration
};
Object.defineProperty(vscode.window, "terminals", {
get: () => {
findTerminalStub();
return [terminalWithoutShell];
},
configurable: true,
});
createTerminalStub.resetHistory();
sendTextSpy.resetHistory();
await vscode.commands.executeCommand("openhands.startConversation");
// Should create new terminal when no Shell Integration available
assert.ok(
createTerminalStub.called,
"Should create new terminal when Shell Integration unavailable",
);
// Should use sendText fallback for the new terminal
assert.ok(sendTextSpy.called, "Should use sendText fallback");
assert.ok(
sendTextSpy.lastArgs[0].includes("openhands"),
"Should send OpenHands command",
);
});
});

View File

@@ -1,37 +0,0 @@
import * as path from "path";
import Mocha = require("mocha");
import { glob } from "glob"; // Updated for glob v9+ API
export async function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
// This should now work with the changed import
ui: "tdd", // Use TDD interface
color: true, // Colored output
timeout: 15000, // Increased timeout for extension tests
});
const testsRoot = path.resolve(__dirname, ".."); // Root of the /src/test folder (compiled to /out/test)
try {
// Use glob to find all test files (ending with .test.js in the compiled output)
const files = await glob("**/**.test.js", { cwd: testsRoot });
// Add files to the test suite
files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f)));
// Run the mocha test
return await new Promise<void>((resolve, reject) => {
mocha.run((failures: number) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
});
} catch (err) {
console.error(err);
throw err;
}
}

View File

@@ -1,23 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"outDir": "out",
"lib": [
"es2020"
],
"sourceMap": true,
"strict": true,
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"exclude": [
"node_modules",
".vscode-test"
],
"include": [
"src"
]
}

View File

@@ -19,10 +19,8 @@ packages = [
{ include = "poetry.lock", to = "openhands" },
]
include = [
"openhands/integrations/vscode/openhands-vscode-0.0.1.vsix",
"skills/**/*",
]
build = "build_vscode.py" # Build VSCode extension during Poetry build
[tool.poetry.dependencies]
python = "^3.12,<3.14"