WIP: JetBrains integration

This commit is contained in:
daniel-lxs 2025-09-11 18:30:12 -05:00
parent 8fee3127ff
commit b91425889f
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
210 changed files with 2076707 additions and 73 deletions

192
.github/workflows/jetbrains-build.yml vendored Normal file
View file

@ -0,0 +1,192 @@
name: JetBrains Integration Build
on:
push:
paths:
- 'jetbrains/**'
- 'deps/patches/vscode/jetbrains.patch'
- '.github/workflows/jetbrains-build.yml'
pull_request:
paths:
- 'jetbrains/**'
- 'deps/patches/vscode/jetbrains.patch'
- '.github/workflows/jetbrains-build.yml'
workflow_dispatch:
jobs:
check-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.19.2'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Check JetBrains dependencies
run: node jetbrains/scripts/check-dependencies.js
build-host:
runs-on: ubuntu-latest
needs: check-dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.19.2'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.8.1
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-jetbrains-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-jetbrains-
- name: Install host dependencies
working-directory: jetbrains/host
run: npm install
- name: Apply VSCode patches
working-directory: jetbrains/host
run: npm run deps:patch
continue-on-error: true
- name: Build host
working-directory: jetbrains/host
run: npm run build
- name: Upload host artifacts
uses: actions/upload-artifact@v4
with:
name: jetbrains-host-build
path: jetbrains/host/dist/
build-plugin:
runs-on: ubuntu-latest
needs: check-dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build plugin
working-directory: jetbrains/plugin
run: ./gradlew buildPlugin
- name: Run plugin tests
working-directory: jetbrains/plugin
run: ./gradlew test
- name: Upload plugin artifacts
uses: actions/upload-artifact@v4
with:
name: jetbrains-plugin-build
path: jetbrains/plugin/build/distributions/*.zip
test-integration:
runs-on: ubuntu-latest
needs: [build-host, build-plugin]
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download host artifacts
uses: actions/download-artifact@v4
with:
name: jetbrains-host-build
path: jetbrains/host/dist/
- name: Download plugin artifacts
uses: actions/download-artifact@v4
with:
name: jetbrains-plugin-build
path: jetbrains/plugin/build/distributions/
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.19.2'
- name: Run integration tests
run: |
echo "Integration tests would run here"
# Add actual integration test commands when available
create-release:
runs-on: ubuntu-latest
needs: [build-host, build-plugin]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download host artifacts
uses: actions/download-artifact@v4
with:
name: jetbrains-host-build
path: jetbrains/host/dist/
- name: Download plugin artifacts
uses: actions/download-artifact@v4
with:
name: jetbrains-plugin-build
path: jetbrains/plugin/build/distributions/
- name: Create platform.zip
run: |
cd jetbrains
zip -r platform.zip host/dist plugin/build/distributions
echo "Platform package created"
- name: Upload platform package
uses: actions/upload-artifact@v4
with:
name: jetbrains-platform-package
path: jetbrains/platform.zip
retention-days: 30

4
.gitmodules vendored Normal file
View file

@ -0,0 +1,4 @@
[submodule "deps/vscode"]
path = deps/vscode
url = https://github.com/microsoft/vscode.git
ignore = dirty

1832
deps/patches/vscode/jetbrains.patch vendored Normal file

File diff suppressed because it is too large Load diff

1
deps/vscode vendored Submodule

@ -0,0 +1 @@
Subproject commit 174af221c9ea2ccdb64abe4aab8e1a805e77beae

43
jetbrains/.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# JetBrains integration specific ignores
# Host build outputs
host/dist/
host/deps/vscode/
host/*.log
host/logs/
host/.turbo/
host/node_modules/
# Plugin build outputs
plugin/build/
plugin/.gradle/
plugin/out/
plugin/.idea/
plugin/*.iml
plugin/*.ipr
plugin/*.iws
plugin/local.properties
# Platform files
platform.zip
*.vsix
# Debug and temporary files
*.tmp
*.bak
*.swp
.DS_Store
Thumbs.db
# Environment files
.env
.env.local
.env.*.local
# Test coverage
coverage/
.nyc_output/
# IDE specific
.vscode/
*.code-workspace

View file

@ -0,0 +1 @@
catrielmuller/jetbrains

197
jetbrains/README.md Normal file
View file

@ -0,0 +1,197 @@
# JetBrains Integration for Roo-Code
This directory contains the JetBrains integration layer that allows Roo-Code to run as a plugin within JetBrains IDEs (IntelliJ IDEA, WebStorm, PyCharm, etc.).
## Architecture Overview
The JetBrains integration consists of two main components:
### 1. Host Bridge (`/jetbrains/host/`)
A Node.js application that:
- Starts the VSCode extension host process
- Provides socket-based IPC communication between JetBrains and the extension
- Translates between JetBrains and VSCode APIs via RPC
- Manages the extension lifecycle
### 2. JetBrains Plugin (`/jetbrains/plugin/`)
A Kotlin/Java plugin that:
- Integrates with JetBrains IDE APIs
- Spawns the Node.js host process
- Handles UI integration within the IDE
- Maps IDE actions to extension commands
## Setup Instructions
### Prerequisites
- Node.js 18.x or higher
- Java 17 or higher
- JetBrains IDE (IntelliJ IDEA, WebStorm, etc.)
- Git with submodule support
### Building the Host Bridge
1. Install dependencies:
```bash
cd jetbrains/host
npm install
```
2. Apply VSCode patches:
```bash
npm run deps:patch
```
3. Build the host:
```bash
npm run build
```
### Building the JetBrains Plugin
1. Navigate to the plugin directory:
```bash
cd jetbrains/plugin
```
2. Build the plugin:
```bash
./gradlew buildPlugin
```
3. The plugin will be available in `build/distributions/`
## Development
### Running in Development Mode
1. Start the host in development mode:
```bash
cd jetbrains/host
npm run dev
```
2. Open the plugin project in IntelliJ IDEA:
```bash
cd jetbrains/plugin
idea .
```
3. Run the plugin using the "Run Plugin" configuration
### Debugging
Enable debug logging by setting environment variables:
- `JETBRAINS_DEBUG_IPC=true` - Logs IPC messages
- `JETBRAINS_RPC_DEBUG=true` - Logs RPC protocol messages
### Testing
Run tests for the host:
```bash
cd jetbrains/host
npm test
```
Run tests for the plugin:
```bash
cd jetbrains/plugin
./gradlew test
```
## Configuration
### Host Configuration
The host can be configured via `jetbrains/host/src/config.ts`:
- `DEFAULT_PORT`: Default socket port (51234)
- `SOCKET_TIMEOUT`: Connection timeout in milliseconds
- `MAX_RECONNECT_ATTEMPTS`: Maximum reconnection attempts
### Plugin Configuration
Plugin settings are in `jetbrains/plugin/gradle.properties`:
- `pluginVersion`: Plugin version
- `platformVersion`: Target IDE version
- `platformType`: IDE type (IC for IntelliJ Community)
## Architecture Details
### Communication Flow
```
JetBrains IDE <-> Kotlin Plugin <-> Socket (TCP) <-> Node.js Host <-> VSCode Extension API <-> Roo-Code Extension
```
### RPC Protocol
The RPC manager (`host/src/rpcManager.ts`) handles bidirectional communication:
- Incoming calls from JetBrains to VSCode APIs
- Outgoing calls from VSCode to JetBrains APIs
- Event subscriptions and notifications
### API Translation
Main thread actors in the plugin map JetBrains APIs to VSCode equivalents:
- `MainThreadCommandsShape`: Command execution
- `MainThreadDocumentsShape`: Document management
- `MainThreadTextEditorsShape`: Editor operations
- `MainThreadTerminalServiceShape`: Terminal integration
- And many more...
## Troubleshooting
### Common Issues
1. **Port already in use**: Change the socket port in the configuration
2. **VSCode patch fails**: Ensure the VSCode submodule is at the correct version
3. **Plugin doesn't load**: Check IDE compatibility in gradle.properties
4. **Extension not found**: Verify the extension is built and in the correct location
### Logs Location
- Host logs: `jetbrains/host/logs/`
- Plugin logs: Check IDE's log directory
## Contributing
When contributing to the JetBrains integration:
1. Follow the existing code style
2. Add tests for new functionality
3. Update documentation as needed
4. Test in multiple JetBrains IDEs if possible
## License
This JetBrains integration follows the same license as Roo-Code.
## Support
For issues specific to JetBrains integration:
1. Check this README's troubleshooting section
2. Search existing issues on GitHub
3. Create a new issue with the "jetbrains" label
## Credits
This integration is adapted from similar VSCode-to-JetBrains bridge implementations and modified for Roo-Code's specific requirements.

5
jetbrains/host/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
### Dependencies
deps/*
### Build
dist

View file

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Delete `VSCODE_CWD` very early. We have seen
// reports where `code .` would use the wrong
// current working directory due to our variable
// somehow escaping to the parent shell
// (https://github.com/microsoft/vscode/issues/126399)
delete process.env["VSCODE_CWD"]

View file

@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from "path"
import * as fs from "fs"
import { fileURLToPath } from "url"
import { createRequire, register } from "node:module"
import { product, pkg } from "./bootstrap-meta.js"
import "./bootstrap-node.js"
import * as performance from "./deps/vscode/vs/base/common/performance.js"
import { INLSConfiguration } from "./deps/vscode/vs/nls.js"
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// Install a hook to module resolution to map 'fs' to 'original-fs'
if (process.env["ELECTRON_RUN_AS_NODE"] || process.versions["electron"]) {
const jsCode = `
export async function resolve(specifier, context, nextResolve) {
if (specifier === 'fs') {
return {
format: 'builtin',
shortCircuit: true,
url: 'node:original-fs'
};
}
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier, context);
}`
register(`data:text/javascript;base64,${Buffer.from(jsCode).toString("base64")}`, import.meta.url)
}
// Prepare globals that are needed for running
globalThis._VSCODE_PRODUCT_JSON = { ...product }
if (process.env["VSCODE_DEV"]) {
try {
const overrides: unknown = require("../product.overrides.json")
globalThis._VSCODE_PRODUCT_JSON = Object.assign(globalThis._VSCODE_PRODUCT_JSON, overrides)
} catch (error) {
/* ignore */
}
}
globalThis._VSCODE_PACKAGE_JSON = { ...pkg }
globalThis._VSCODE_FILE_ROOT = __dirname
//#region NLS helpers
let setupNLSResult: Promise<INLSConfiguration | undefined> | undefined = undefined
function setupNLS(): Promise<INLSConfiguration | undefined> {
if (!setupNLSResult) {
setupNLSResult = doSetupNLS()
}
return setupNLSResult
}
async function doSetupNLS(): Promise<INLSConfiguration | undefined> {
performance.mark("code/willLoadNls")
let nlsConfig: INLSConfiguration | undefined = undefined
let messagesFile: string | undefined
if (process.env["VSCODE_NLS_CONFIG"]) {
try {
nlsConfig = JSON.parse(process.env["VSCODE_NLS_CONFIG"])
if (nlsConfig?.languagePack?.messagesFile) {
messagesFile = nlsConfig.languagePack.messagesFile
} else if (nlsConfig?.defaultMessagesFile) {
messagesFile = nlsConfig.defaultMessagesFile
}
globalThis._VSCODE_NLS_LANGUAGE = nlsConfig?.resolvedLanguage
} catch (e) {
console.error(`Error reading VSCODE_NLS_CONFIG from environment: ${e}`)
}
}
if (
process.env["VSCODE_DEV"] || // no NLS support in dev mode
!messagesFile // no NLS messages file
) {
return undefined
}
try {
globalThis._VSCODE_NLS_MESSAGES = JSON.parse((await fs.promises.readFile(messagesFile)).toString())
} catch (error) {
console.error(`Error reading NLS messages file ${messagesFile}: ${error}`)
// Mark as corrupt: this will re-create the language pack cache next startup
if (nlsConfig?.languagePack?.corruptMarkerFile) {
try {
await fs.promises.writeFile(nlsConfig.languagePack.corruptMarkerFile, "corrupted")
} catch (error) {
console.error(`Error writing corrupted NLS marker file: ${error}`)
}
}
// Fallback to the default message file to ensure english translation at least
if (nlsConfig?.defaultMessagesFile && nlsConfig.defaultMessagesFile !== messagesFile) {
try {
globalThis._VSCODE_NLS_MESSAGES = JSON.parse(
(await fs.promises.readFile(nlsConfig.defaultMessagesFile)).toString(),
)
} catch (error) {
console.error(`Error reading default NLS messages file ${nlsConfig.defaultMessagesFile}: ${error}`)
}
}
}
performance.mark("code/didLoadNls")
return nlsConfig
}
//#endregion
export async function bootstrapESM(): Promise<void> {
// NLS
await setupNLS()
}

View file

@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as performance from "./deps/vscode/vs/base/common/performance.js"
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from "./bootstrap-node.js"
import { bootstrapESM } from "./bootstrap-esm.js"
performance.mark("code/fork/start")
//#region Helpers
function pipeLoggingToParent(): void {
const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024
const MAX_LENGTH = 100000
/**
* Prevent circular stringify and convert arguments to real array
*/
function safeToString(args: ArrayLike<unknown>): string {
const seen: unknown[] = []
const argsArray: unknown[] = []
// Massage some arguments with special treatment
if (args.length) {
for (let i = 0; i < args.length; i++) {
let arg = args[i]
// Any argument of type 'undefined' needs to be specially treated because
// JSON.stringify will simply ignore those. We replace them with the string
// 'undefined' which is not 100% right, but good enough to be logged to console
if (typeof arg === "undefined") {
arg = "undefined"
}
// Any argument that is an Error will be changed to be just the error stack/message
// itself because currently cannot serialize the error over entirely.
else if (arg instanceof Error) {
const errorObj = arg
if (errorObj.stack) {
arg = errorObj.stack
} else {
arg = errorObj.toString()
}
}
argsArray.push(arg)
}
}
try {
const res = JSON.stringify(argsArray, function (key, value: unknown) {
// Objects get special treatment to prevent circles
if (isObject(value) || Array.isArray(value)) {
if (seen.indexOf(value) !== -1) {
return "[Circular]"
}
seen.push(value)
}
return value
})
if (res.length > MAX_LENGTH) {
return "Output omitted for a large object that exceeds the limits"
}
return res
} catch (error) {
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`
}
}
function safeSend(arg: { type: string; severity: string; arguments: string }): void {
try {
if (process.send) {
process.send(arg)
}
} catch (error) {
// Can happen if the parent channel is closed meanwhile
}
}
function isObject(obj: unknown): boolean {
return (
typeof obj === "object" &&
obj !== null &&
!Array.isArray(obj) &&
!(obj instanceof RegExp) &&
!(obj instanceof Date)
)
}
function safeSendConsoleMessage(severity: "log" | "warn" | "error", args: string): void {
safeSend({ type: "__$console", severity, arguments: args })
}
/**
* Wraps a console message so that it is transmitted to the renderer.
*
* The wrapped property is not defined with `writable: false` to avoid
* throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88
*/
function wrapConsoleMethod(method: "log" | "info" | "warn" | "error", severity: "log" | "warn" | "error"): void {
Object.defineProperty(console, method, {
set: () => {},
get: () =>
function () {
safeSendConsoleMessage(severity, safeToString(arguments))
},
})
}
/**
* Wraps process.stderr/stdout.write() so that it is transmitted to the
* renderer or CLI. It both calls through to the original method as well
* as to console.log with complete lines so that they're made available
* to the debugger/CLI.
*/
function wrapStream(streamName: "stdout" | "stderr", severity: "log" | "warn" | "error"): void {
const stream = process[streamName]
const original = stream.write
let buf = ""
Object.defineProperty(stream, "write", {
set: () => {},
get:
() =>
(
chunk: string | Buffer | Uint8Array,
encoding: BufferEncoding | undefined,
callback: ((err?: Error | undefined) => void) | undefined,
) => {
buf += chunk.toString(encoding)
const eol = buf.length > MAX_STREAM_BUFFER_LENGTH ? buf.length : buf.lastIndexOf("\n")
if (eol !== -1) {
console[severity](buf.slice(0, eol))
buf = buf.slice(eol + 1)
}
original.call(stream, chunk, encoding, callback)
},
})
}
// Pass console logging to the outside so that we have it in the main side if told so
if (process.env["VSCODE_VERBOSE_LOGGING"] === "true") {
wrapConsoleMethod("info", "log")
wrapConsoleMethod("log", "log")
wrapConsoleMethod("warn", "warn")
wrapConsoleMethod("error", "error")
} else {
console.log = function () {
/* ignore */
}
console.warn = function () {
/* ignore */
}
console.info = function () {
/* ignore */
}
wrapConsoleMethod("error", "error")
}
wrapStream("stderr", "error")
wrapStream("stdout", "log")
}
function handleExceptions(): void {
// Handle uncaught exceptions
process.on("uncaughtException", function (err) {
console.error("Uncaught Exception: ", err)
})
// Handle unhandled promise rejections
process.on("unhandledRejection", function (reason) {
console.error("Unhandled Promise Rejection: ", reason)
})
}
function terminateWhenParentTerminates(): void {
const parentPid = Number(process.env["VSCODE_PARENT_PID"])
if (typeof parentPid === "number" && !isNaN(parentPid)) {
setInterval(function () {
try {
process.kill(parentPid, 0) // throws an exception if the main process doesn't exist anymore.
} catch (e) {
process.exit()
}
}, 5000)
}
}
function configureCrashReporter(): void {
const crashReporterProcessType = process.env["VSCODE_CRASH_REPORTER_PROCESS_TYPE"]
if (crashReporterProcessType) {
try {
//@ts-ignore
if (
process["crashReporter"] &&
typeof process["crashReporter"].addExtraParameter === "function" /* Electron only */
) {
//@ts-ignore
process["crashReporter"].addExtraParameter("processType", crashReporterProcessType)
}
} catch (error) {
console.error(error)
}
}
}
//#endregion
// Crash reporter
configureCrashReporter()
// Remove global paths from the node module lookup (node.js only)
removeGlobalNodeJsModuleLookupPaths()
if (process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]) {
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
}
// Configure: pipe logging to parent process
if (!!process.send && process.env["VSCODE_PIPE_LOGGING"] === "true") {
pipeLoggingToParent()
}
// Handle Exceptions
if (!process.env["VSCODE_HANDLES_UNCAUGHT_ERRORS"]) {
handleExceptions()
}
// Terminate when parent terminates
if (process.env["VSCODE_PARENT_PID"]) {
terminateWhenParentTerminates()
}
// Bootstrap ESM
await bootstrapESM()
// Load ESM entry point
await import(
[`./${process.env["VSCODE_ESM_ENTRYPOINT"]}.js`].join(
"/",
) /* workaround: esbuild prints some strange warnings when trying to inline? */
)

View file

@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// *********************************************************************
// * *
// * We need this to redirect to node_modules from the remote-folder. *
// * This ONLY applies when running out of source. *
// * *
// *********************************************************************
import { fileURLToPath, pathToFileURL } from "node:url"
import { promises } from "node:fs"
import { join } from "node:path"
// SEE https://nodejs.org/docs/latest/api/module.html#initialize
const _specifierToUrl: Record<string, string> = {}
export async function initialize(injectPath: string): Promise<void> {
// populate mappings
const injectPackageJSONPath = fileURLToPath(new URL("../package.json", pathToFileURL(injectPath)))
const packageJSON = JSON.parse(String(await promises.readFile(injectPackageJSONPath)))
for (const [name] of Object.entries(packageJSON.dependencies)) {
try {
const path = join(injectPackageJSONPath, `../node_modules/${name}/package.json`)
let { main } = JSON.parse(String(await promises.readFile(path)))
if (!main) {
main = "index.js"
}
if (!main.endsWith(".js")) {
main += ".js"
}
const mainPath = join(injectPackageJSONPath, `../node_modules/${name}/${main}`)
_specifierToUrl[name] = pathToFileURL(mainPath).href
} catch (err) {
console.error(name)
console.error(err)
}
}
console.log(`[bootstrap-import] Initialized node_modules redirector for: ${injectPath}`)
}
export async function resolve(specifier: string | number, context: any, nextResolve: (arg0: any, arg1: any) => any) {
const newSpecifier = _specifierToUrl[specifier]
if (newSpecifier !== undefined) {
return {
format: "commonjs",
shortCircuit: true,
url: newSpecifier,
}
}
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier, context)
}

View file

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createRequire } from "node:module"
import type { IProductConfiguration } from "./deps/vscode/vs/base/common/product.js"
const require = createRequire(import.meta.url)
let productObj: Partial<IProductConfiguration> & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } = {
BUILD_INSERT_PRODUCT_CONFIGURATION: "BUILD_INSERT_PRODUCT_CONFIGURATION",
} // DO NOT MODIFY, PATCHED DURING BUILD
if (productObj["BUILD_INSERT_PRODUCT_CONFIGURATION"]) {
productObj = require("../product.json") // Running out of sources
}
let pkgObj = { BUILD_INSERT_PACKAGE_CONFIGURATION: "BUILD_INSERT_PACKAGE_CONFIGURATION" } // DO NOT MODIFY, PATCHED DURING BUILD
if (pkgObj["BUILD_INSERT_PACKAGE_CONFIGURATION"]) {
pkgObj = require("../package.json") // Running out of sources
}
export const product = productObj
export const pkg = pkgObj

View file

@ -0,0 +1,193 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from "path"
import * as fs from "fs"
import { fileURLToPath } from "url"
import { createRequire } from "node:module"
import type { IProductConfiguration } from "./deps/vscode/vs/base/common/product.js"
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isWindows = process.platform === "win32"
// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
Error.stackTraceLimit = 100
if (!process.env["VSCODE_HANDLES_SIGPIPE"]) {
// Workaround for Electron not installing a handler to ignore SIGPIPE
// (https://github.com/electron/electron/issues/13254)
let didLogAboutSIGPIPE = false
process.on("SIGPIPE", () => {
// See https://github.com/microsoft/vscode-remote-release/issues/6543
// In certain situations, the console itself can be in a broken pipe state
// so logging SIGPIPE to the console will cause an infinite async loop
if (!didLogAboutSIGPIPE) {
didLogAboutSIGPIPE = true
console.error(new Error(`Unexpected SIGPIPE`))
}
})
}
// Setup current working directory in all our node & electron processes
// - Windows: call `process.chdir()` to always set application folder as cwd
// - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups
function setupCurrentWorkingDirectory(): void {
try {
// Store the `process.cwd()` inside `VSCODE_CWD`
// for consistent lookups, but make sure to only
// do this once unless defined already from e.g.
// a parent process.
if (typeof process.env["VSCODE_CWD"] !== "string") {
process.env["VSCODE_CWD"] = process.cwd()
}
// Windows: always set application folder as current working dir
if (process.platform === "win32") {
process.chdir(path.dirname(process.execPath))
}
} catch (err) {
console.error(err)
}
}
setupCurrentWorkingDirectory()
/**
* Add support for redirecting the loading of node modules
*
* Note: only applies when running out of sources.
*/
export function devInjectNodeModuleLookupPath(injectPath: string): void {
if (!process.env["VSCODE_DEV"]) {
return // only applies running out of sources
}
if (!injectPath) {
throw new Error("Missing injectPath")
}
// register a loader hook
const Module = require("node:module")
Module.register("./bootstrap-import.js", { parentURL: import.meta.url, data: injectPath })
}
export function removeGlobalNodeJsModuleLookupPaths(): void {
if (typeof process?.versions?.electron === "string") {
return // Electron disables global search paths in https://github.com/electron/electron/blob/3186c2f0efa92d275dc3d57b5a14a60ed3846b0e/shell/common/node_bindings.cc#L653
}
const Module = require("module")
const globalPaths = Module.globalPaths
const originalResolveLookupPaths = Module._resolveLookupPaths
Module._resolveLookupPaths = function (moduleName: string, parent: any): string[] {
const paths = originalResolveLookupPaths(moduleName, parent)
if (Array.isArray(paths)) {
let commonSuffixLength = 0
while (
commonSuffixLength < paths.length &&
paths[paths.length - 1 - commonSuffixLength] ===
globalPaths[globalPaths.length - 1 - commonSuffixLength]
) {
commonSuffixLength++
}
return paths.slice(0, paths.length - commonSuffixLength)
}
return paths
}
const originalNodeModulePaths = Module._nodeModulePaths
Module._nodeModulePaths = function (from: string): string[] {
let paths: string[] = originalNodeModulePaths(from)
if (!isWindows) {
return paths
}
// On Windows, remove drive(s) and users' home directory from search paths,
// UNLESS 'from' is explicitly set to one of those.
const isDrive = (p: string) => p.length >= 3 && p.endsWith(":\\")
if (!isDrive(from)) {
paths = paths.filter((p) => !isDrive(path.dirname(p)))
}
if (process.env.HOMEDRIVE && process.env.HOMEPATH) {
const userDir = path.dirname(path.join(process.env.HOMEDRIVE, process.env.HOMEPATH))
const isUsersDir = (p: string) => path.relative(p, userDir).length === 0
// Check if 'from' is the same as 'userDir'
if (!isUsersDir(from)) {
paths = paths.filter((p) => !isUsersDir(path.dirname(p)))
}
}
return paths
}
}
/**
* Helper to enable portable mode.
*/
export function configurePortable(product: Partial<IProductConfiguration>): {
portableDataPath: string
isPortable: boolean
} {
const appRoot = path.dirname(__dirname)
function getApplicationPath(): string {
if (process.env["VSCODE_DEV"]) {
return appRoot
}
if (process.platform === "darwin") {
return path.dirname(path.dirname(path.dirname(appRoot)))
}
return path.dirname(path.dirname(appRoot))
}
function getPortableDataPath(): string {
if (process.env["VSCODE_PORTABLE"]) {
return process.env["VSCODE_PORTABLE"]
}
if (process.platform === "win32" || process.platform === "linux") {
return path.join(getApplicationPath(), "data")
}
const portableDataName = product.portable || `${product.applicationName}-portable-data`
return path.join(path.dirname(getApplicationPath()), portableDataName)
}
const portableDataPath = getPortableDataPath()
const isPortable = !("target" in product) && fs.existsSync(portableDataPath)
const portableTempPath = path.join(portableDataPath, "tmp")
const isTempPortable = isPortable && fs.existsSync(portableTempPath)
if (isPortable) {
process.env["VSCODE_PORTABLE"] = portableDataPath
} else {
delete process.env["VSCODE_PORTABLE"]
}
if (isTempPortable) {
if (process.platform === "win32") {
process.env["TMP"] = portableTempPath
process.env["TEMP"] = portableTempPath
} else {
process.env["TMPDIR"] = portableTempPath
}
}
return {
portableDataPath,
isPortable,
}
}

View file

@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Keep bootstrap-esm.js from redefining 'fs'.
delete process.env["ELECTRON_RUN_AS_NODE"]

View file

@ -0,0 +1,252 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
;(function () {
type ISandboxConfiguration =
import("./deps/vscode/vs/base/parts/sandbox/common/sandboxTypes.js").ISandboxConfiguration
type ILoadResult<
M,
T extends ISandboxConfiguration,
> = import("./deps/vscode/vs/platform/window/electron-sandbox/window.js").ILoadResult<M, T>
type ILoadOptions<T extends ISandboxConfiguration> =
import("./deps/vscode/vs/platform/window/electron-sandbox/window.js").ILoadOptions<T>
type IMainWindowSandboxGlobals =
import("./deps/vscode/vs/base/parts/sandbox/electron-sandbox/globals.js").IMainWindowSandboxGlobals
const preloadGlobals: IMainWindowSandboxGlobals = (window as any).vscode // defined by preload.ts
const safeProcess = preloadGlobals.process
async function load<M, T extends ISandboxConfiguration>(
esModule: string,
options: ILoadOptions<T>,
): Promise<ILoadResult<M, T>> {
// Window Configuration from Preload Script
const configuration = await resolveWindowConfiguration<T>()
// Signal before import()
options?.beforeImport?.(configuration)
// Developer settings
const {
enableDeveloperKeybindings,
removeDeveloperKeybindingsAfterLoad,
developerDeveloperKeybindingsDisposable,
forceDisableShowDevtoolsOnError,
} = setupDeveloperKeybindings(configuration, options)
// NLS
setupNLS<T>(configuration)
// Compute base URL and set as global
const baseUrl = new URL(
`${fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === "win32", scheme: "vscode-file", fallbackAuthority: "vscode-app" })}/out/`,
)
globalThis._VSCODE_FILE_ROOT = baseUrl.toString()
// Dev only: CSS import map tricks
setupCSSImportMaps<T>(configuration, baseUrl)
// ESM Import
try {
const result = await import(new URL(`${esModule}.js`, baseUrl).href)
if (developerDeveloperKeybindingsDisposable && removeDeveloperKeybindingsAfterLoad) {
developerDeveloperKeybindingsDisposable()
}
return { result, configuration }
} catch (error) {
onUnexpectedError(error, enableDeveloperKeybindings && !forceDisableShowDevtoolsOnError)
throw error
}
}
async function resolveWindowConfiguration<T extends ISandboxConfiguration>() {
const timeout = setTimeout(() => {
console.error(
`[resolve window config] Could not resolve window configuration within 10 seconds, but will continue to wait...`,
)
}, 10000)
performance.mark("code/willWaitForWindowConfig")
const configuration = (await preloadGlobals.context.resolveConfiguration()) as T
performance.mark("code/didWaitForWindowConfig")
clearTimeout(timeout)
return configuration
}
function setupDeveloperKeybindings<T extends ISandboxConfiguration>(configuration: T, options: ILoadOptions<T>) {
const {
forceEnableDeveloperKeybindings,
disallowReloadKeybinding,
removeDeveloperKeybindingsAfterLoad,
forceDisableShowDevtoolsOnError,
} =
typeof options?.configureDeveloperSettings === "function"
? options.configureDeveloperSettings(configuration)
: {
forceEnableDeveloperKeybindings: false,
disallowReloadKeybinding: false,
removeDeveloperKeybindingsAfterLoad: false,
forceDisableShowDevtoolsOnError: false,
}
const isDev = !!safeProcess.env["VSCODE_DEV"]
const enableDeveloperKeybindings = Boolean(isDev || forceEnableDeveloperKeybindings)
let developerDeveloperKeybindingsDisposable: Function | undefined = undefined
if (enableDeveloperKeybindings) {
developerDeveloperKeybindingsDisposable = registerDeveloperKeybindings(disallowReloadKeybinding)
}
return {
enableDeveloperKeybindings,
removeDeveloperKeybindingsAfterLoad,
developerDeveloperKeybindingsDisposable,
forceDisableShowDevtoolsOnError,
}
}
function registerDeveloperKeybindings(disallowReloadKeybinding: boolean | undefined): Function {
const ipcRenderer = preloadGlobals.ipcRenderer
const extractKey = function (e: KeyboardEvent) {
return [
e.ctrlKey ? "ctrl-" : "",
e.metaKey ? "meta-" : "",
e.altKey ? "alt-" : "",
e.shiftKey ? "shift-" : "",
e.keyCode,
].join("")
}
// Devtools & reload support
const TOGGLE_DEV_TOOLS_KB = safeProcess.platform === "darwin" ? "meta-alt-73" : "ctrl-shift-73" // mac: Cmd-Alt-I, rest: Ctrl-Shift-I
const TOGGLE_DEV_TOOLS_KB_ALT = "123" // F12
const RELOAD_KB = safeProcess.platform === "darwin" ? "meta-82" : "ctrl-82" // mac: Cmd-R, rest: Ctrl-R
let listener: ((e: KeyboardEvent) => void) | undefined = function (e) {
const key = extractKey(e)
if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) {
ipcRenderer.send("vscode:toggleDevTools")
} else if (key === RELOAD_KB && !disallowReloadKeybinding) {
ipcRenderer.send("vscode:reloadWindow")
}
}
window.addEventListener("keydown", listener)
return function () {
if (listener) {
window.removeEventListener("keydown", listener)
listener = undefined
}
}
}
function setupNLS<T extends ISandboxConfiguration>(configuration: T): void {
globalThis._VSCODE_NLS_MESSAGES = configuration.nls.messages
globalThis._VSCODE_NLS_LANGUAGE = configuration.nls.language
let language = configuration.nls.language || "en"
if (language === "zh-tw") {
language = "zh-Hant"
} else if (language === "zh-cn") {
language = "zh-Hans"
}
window.document.documentElement.setAttribute("lang", language)
}
function onUnexpectedError(error: string | Error, showDevtoolsOnError: boolean): void {
if (showDevtoolsOnError) {
const ipcRenderer = preloadGlobals.ipcRenderer
ipcRenderer.send("vscode:openDevTools")
}
console.error(`[uncaught exception]: ${error}`)
if (error && typeof error !== "string" && error.stack) {
console.error(error.stack)
}
}
function fileUriFromPath(
path: string,
config: { isWindows?: boolean; scheme?: string; fallbackAuthority?: string },
): string {
// Since we are building a URI, we normalize any backslash
// to slashes and we ensure that the path begins with a '/'.
let pathName = path.replace(/\\/g, "/")
if (pathName.length > 0 && pathName.charAt(0) !== "/") {
pathName = `/${pathName}`
}
let uri: string
// Windows: in order to support UNC paths (which start with '//')
// that have their own authority, we do not use the provided authority
// but rather preserve it.
if (config.isWindows && pathName.startsWith("//")) {
uri = encodeURI(`${config.scheme || "file"}:${pathName}`)
}
// Otherwise we optionally add the provided authority if specified
else {
uri = encodeURI(`${config.scheme || "file"}://${config.fallbackAuthority || ""}${pathName}`)
}
return uri.replace(/#/g, "%23")
}
function setupCSSImportMaps<T extends ISandboxConfiguration>(configuration: T, baseUrl: URL) {
// DEV ---------------------------------------------------------------------------------------
// DEV: This is for development and enables loading CSS via import-statements via import-maps.
// DEV: For each CSS modules that we have we defined an entry in the import map that maps to
// DEV: a blob URL that loads the CSS via a dynamic @import-rule.
// DEV ---------------------------------------------------------------------------------------
if (Array.isArray(configuration.cssModules) && configuration.cssModules.length > 0) {
performance.mark("code/willAddCssLoader")
const style = document.createElement("style")
style.type = "text/css"
style.media = "screen"
style.id = "vscode-css-loading"
document.head.appendChild(style)
globalThis._VSCODE_CSS_LOAD = function (url) {
style.textContent += `@import url(${url});\n`
}
const importMap: { imports: Record<string, string> } = { imports: {} }
for (const cssModule of configuration.cssModules) {
const cssUrl = new URL(cssModule, baseUrl).href
const jsSrc = `globalThis._VSCODE_CSS_LOAD('${cssUrl}');\n`
const blob = new Blob([jsSrc], { type: "application/javascript" })
importMap.imports[cssUrl] = URL.createObjectURL(blob)
}
const ttp = window.trustedTypes?.createPolicy("vscode-bootstrapImportMap", {
createScript(value) {
return value
},
})
const importMapSrc = JSON.stringify(importMap, undefined, 2)
const importMapScript = document.createElement("script")
importMapScript.type = "importmap"
importMapScript.setAttribute("nonce", "0c6a828f1297")
// @ts-ignore
importMapScript.textContent = ttp?.createScript(importMapSrc) ?? importMapSrc
document.head.appendChild(importMapScript)
performance.mark("code/didAddCssLoader")
}
}
;(globalThis as any).MonacoBootstrapWindow = { load }
})()

36
jetbrains/host/cli.ts Normal file
View file

@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import "./bootstrap-cli.js" // this MUST come before other imports as it changes global state
import { dirname } from "path"
import { fileURLToPath } from "url"
import { configurePortable } from "./bootstrap-node.js"
import { bootstrapESM } from "./bootstrap-esm.js"
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
import { product } from "./bootstrap-meta.js"
const __dirname = dirname(fileURLToPath(import.meta.url))
// NLS
const nlsConfiguration = await resolveNLSConfiguration({
userLocale: "en",
osLocale: "en",
commit: product.commit,
userDataPath: "",
nlsMetadataPath: __dirname,
})
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration) // required for `bootstrap-esm` to pick up NLS messages
// Enable portable support
configurePortable(product)
// Signal processes that we got launched as CLI
process.env["VSCODE_CLI"] = "1"
// Bootstrap ESM
await bootstrapESM()
// Load Server
await import("./deps/vscode/vs/code/node/cli.js")

38
jetbrains/host/electron.d.ts vendored Normal file
View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { MessageEvent } from "electron"
declare global {
interface Process {
/**
* Electron's process.crash() method
* This method is used to crash the process
*/
crash(): void
/**
* Electron's IPC (Inter-Process Communication) port
* Used for communication between renderer and main processes
*/
parentPort: {
/**
* Register a listener for a specific channel
*/
on(channel: string, listener: (event: MessageEvent) => void): void
/**
* Register a one-time listener for a specific channel
*/
once(channel: string, listener: (event: MessageEvent) => void): void
/**
* Send a message to the parent process
*/
postMessage(message: any): void
}
}
}
export {}

View file

@ -0,0 +1,42 @@
import { config } from "@roo-code/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [
...config,
{
rules: {
// TODO: These should be fixed and the rules re-enabled.
"no-regex-spaces": "off",
"no-useless-escape": "off",
"no-empty": "off",
"prefer-const": "off",
"prefer-rest-params": "off",
"no-var": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/no-misused-new": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
{
files: ["__mocks__/**/*.js"],
rules: {
"no-undef": "off",
},
},
{
files: ["**/__test_cases__/**/*"],
rules: {
"no-undef": "off",
"no-const-assign": "off",
},
},
{
ignores: ["dist", "deps"],
},
]

142
jetbrains/host/extension.ts Normal file
View file

@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import net from "net"
// Save original process methods
const originalProcessOn = process.on
const originalProcessSend = process.send || (() => false)
// Store message event handlers
const messageHandlers: ((message: any, socket?: net.Socket) => void)[] = []
// Reconnection related variables
let isReconnecting = false
let reconnectAttempts = 0
const MAX_RECONNECT_ATTEMPTS = 5
const RECONNECT_DELAY = 1000 // 1 second
// Override process.on
process.on = function (event: string, listener: (...args: any[]) => void): any {
if (event === "message") {
messageHandlers.push((message: any, socket?: net.Socket) => {
// Check the number of parameters for listener
const paramCount = listener.length
if (paramCount === 1) {
// If only one parameter, pass only message
listener(message)
} else {
// If multiple parameters, pass message and socket
listener(message, socket)
}
})
}
return originalProcessOn.call(process, event, listener)
}
// Override process.send
process.send = function (message: any): boolean {
if (message?.type === "VSCODE_EXTHOST_IPC_READY") {
console.log("Extension host process is ready to receive socket")
connect()
}
// Call original process.send
return originalProcessSend.call(process, message)
}
// Establish socket connection
function connect() {
if (isReconnecting) {
console.log("Already in reconnection process, skipping")
return
}
try {
// Get socket server information from environment variables
const host = process.env.VSCODE_EXTHOST_SOCKET_HOST || "127.0.0.1"
const port = parseInt(process.env.VSCODE_EXTHOST_SOCKET_PORT || "0", 10)
if (!port) {
throw new Error("Invalid socket port")
}
console.log(`Attempting to connect to ${host}:${port}`)
// Establish socket connection
const socket = net.createConnection(port, host)
// Set socket noDelay option
socket.setNoDelay(true)
socket.on("connect", () => {
console.log("Connected to main server")
isReconnecting = false
reconnectAttempts = 0
// Prepare message to send to VSCode module
const socketMessage = {
type: "VSCODE_EXTHOST_IPC_SOCKET",
initialDataChunk: "",
skipWebSocketFrames: true,
permessageDeflate: false,
inflateBytes: "",
}
// Call all saved message handlers
messageHandlers.forEach((handler) => {
try {
handler(socketMessage, socket)
} catch (error) {
console.error("Error in message handler:", error)
}
})
})
socket.on("error", (error: Error) => {
console.error("Socket connection error:", error)
handleDisconnect()
})
socket.on("close", () => {
console.log("Socket connection closed")
handleDisconnect()
})
} catch (error) {
console.error("Connection error:", error)
handleDisconnect()
}
}
// Handle disconnection
async function handleDisconnect() {
if (isReconnecting) {
console.log("Already in reconnection process, skipping")
return
}
isReconnecting = true
reconnectAttempts++
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
console.error("Max reconnection attempts reached. Giving up.")
isReconnecting = false
return
}
console.log(`Attempting to reconnect (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
// Wait for a while before retrying
console.log(`Waiting ${RECONNECT_DELAY}ms before reconnecting...`)
await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY))
console.log("Reconnection delay finished, attempting to connect...")
connect()
}
console.log("Starting extension host process...")
import start from "./deps/vscode/vs/workbench/api/node/extensionHostProcess.js"
// This line will trigger extension host related logic startup, actual logic is in extensionHostProcess,
// Do not handle specific plugin business logic in subsequent content of this file
start()

178
jetbrains/host/package.json Normal file
View file

@ -0,0 +1,178 @@
{
"name": "@roo-code/jetbrains-host",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"deps:check": "node ../../jetbrains/scripts/check-dependencies.js",
"deps:patch": "npm run deps:check && cd ../../deps/vscode && git reset --hard HEAD && git clean -fd && git apply ../patches/vscode/jetbrains.patch",
"deps:clean": "rm -rf ./deps/vscode/* || true",
"deps:copy": "npm run deps:check && npx cpy '../../deps/vscode/src/**' './deps/vscode' --parents",
"clean": "del-cli ./dist",
"build": "tsc",
"build:clean": "npm run clean && npm run build",
"start": "node ./dist/src/main.js",
"dev": "tsc && node ./dist/src/main.js",
"watch:tsc": "tsc --watch",
"watch": "tsc --watch",
"bundle:package": "cp ./package.json ./dist/package.json",
"bundle:build": "tsc --noEmit && tsup",
"lint": "eslint . --ext=ts --max-warnings=0"
},
"dependencies": {
"@c4312/eventsource-umd": "^3.0.5",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "2.5.1",
"@types/semver": "^7.5.8",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
"@vscode/policy-watcher": "^1.3.2",
"@vscode/proxy-agent": "^0.32.0",
"@vscode/ripgrep": "^1.15.11",
"@vscode/spdlog": "^0.15.0",
"@vscode/sqlite3": "5.1.8-vscode",
"@vscode/sudo-prompt": "9.3.1",
"@vscode/tree-sitter-wasm": "^0.1.4",
"@vscode/vscode-languagedetection": "1.0.21",
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.2.0-beta.82",
"@xterm/addon-image": "^0.9.0-beta.99",
"@xterm/addon-ligatures": "^0.10.0-beta.99",
"@xterm/addon-progress": "^0.2.0-beta.5",
"@xterm/addon-search": "^0.16.0-beta.99",
"@xterm/addon-serialize": "^0.14.0-beta.99",
"@xterm/addon-unicode11": "^0.9.0-beta.99",
"@xterm/addon-webgl": "^0.19.0-beta.99",
"@xterm/headless": "^5.6.0-beta.99",
"@xterm/xterm": "^5.6.0-beta.99",
"all": "^0.0.0",
"debug": "^4.4.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.4",
"kerberos": "2.1.1",
"minimist": "^1.2.6",
"native-is-elevated": "0.7.0",
"native-keymap": "^3.3.5",
"native-watchdog": "^1.4.1",
"node-pty": "^1.1.0-beta33",
"open": "^8.4.2",
"tas-client-umd": "0.2.0",
"undici": "^7.13.0",
"undici-types": "^7.15.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "9.2.0",
"yauzl": "^3.0.0",
"yazl": "^2.4.3"
},
"devDependencies": {
"@roo-code/config-eslint": "file:../../packages/config-eslint",
"@playwright/test": "^1.50.0",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/gulp-svgmin": "^1.2.1",
"@types/http-proxy-agent": "^2.0.1",
"@types/kerberos": "^1.1.2",
"@types/minimist": "^1.2.1",
"@types/mocha": "^9.1.1",
"@types/node": "20.x",
"@types/sinon": "^10.0.2",
"@types/sinon-test": "^2.4.2",
"@types/trusted-types": "^1.0.6",
"@types/vscode-notebook-renderer": "^1.72.0",
"@types/webpack": "^5.28.5",
"@types/wicg-file-system-access": "^2020.9.6",
"@types/windows-foreground-love": "^0.3.0",
"@types/winreg": "^1.2.30",
"@types/yauzl": "^2.10.0",
"@types/yazl": "^2.4.2",
"@vscode/gulp-electron": "^1.36.0",
"@vscode/l10n-dev": "0.0.35",
"@vscode/telemetry-extractor": "^1.10.2",
"@vscode/test-cli": "^0.0.6",
"@vscode/test-electron": "^2.4.0",
"@vscode/test-web": "^0.0.62",
"@vscode/v8-heap-parser": "^0.1.0",
"@vscode/vscode-perf": "^0.0.19",
"@webgpu/types": "^0.1.44",
"ansi-colors": "^3.2.3",
"asar": "^3.0.3",
"chromium-pickle-js": "^0.2.0",
"cookie": "^0.7.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.9.1",
"cssnano": "^6.0.3",
"debounce": "^1.0.0",
"deemon": "^1.11.0",
"electron": "34.4.1",
"event-stream": "3.3.4",
"fancy-log": "^1.3.3",
"file-loader": "^6.2.0",
"cpy-cli": "^5.0.0",
"glob": "^7.2.3",
"gulp": "^4.0.0",
"gulp-azure-storage": "^0.12.1",
"gulp-bom": "^3.0.0",
"gulp-buffer": "0.0.2",
"gulp-filter": "^5.1.0",
"gulp-flatmap": "^1.0.2",
"gulp-gunzip": "^1.0.0",
"gulp-gzip": "^1.4.2",
"gulp-json-editor": "^2.5.0",
"gulp-plumber": "^1.2.0",
"gulp-rename": "^1.2.0",
"gulp-replace": "^0.5.4",
"gulp-sourcemaps": "^3.0.0",
"gulp-svgmin": "^4.1.0",
"gulp-untar": "^0.0.7",
"husky": "^0.13.1",
"innosetup": "^6.4.1",
"istanbul-lib-coverage": "^3.2.0",
"istanbul-lib-instrument": "^6.0.1",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.1.5",
"lazy.js": "^0.4.2",
"merge-options": "^1.0.1",
"mime": "^1.4.1",
"minimatch": "^3.0.4",
"minimist": "^1.2.6",
"mocha": "^10.8.2",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5",
"os-browserify": "^0.3.0",
"p-all": "^1.0.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.33",
"postcss-nesting": "^12.0.2",
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"del-cli": "^5.1.0",
"sinon": "^12.0.1",
"sinon-test": "^3.1.3",
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"tslib": "^2.6.3",
"tsup": "^8.5.0",
"util": "^0.12.4",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-stream": "^7.0.0",
"xml2js": "^0.5.0",
"yaserver": "^0.4.0"
},
"overrides": {
"node-gyp-build": "4.8.1",
"kerberos@2.1.1": {
"node-addon-api": "7.1.0"
}
}
}

View file

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import "./bootstrap-server.js" // this MUST come before other imports as it changes global state
import { dirname, join } from "path"
import { fileURLToPath } from "url"
import { devInjectNodeModuleLookupPath } from "./bootstrap-node.js"
import { bootstrapESM } from "./bootstrap-esm.js"
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
import { product } from "./bootstrap-meta.js"
const __dirname = dirname(fileURLToPath(import.meta.url))
// NLS
const nlsConfiguration = await resolveNLSConfiguration({
userLocale: "en",
osLocale: "en",
commit: product.commit,
userDataPath: "",
nlsMetadataPath: __dirname,
})
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration) // required for `bootstrap-esm` to pick up NLS messages
if (process.env["VSCODE_DEV"]) {
// When running out of sources, we need to load node modules from remote/node_modules,
// which are compiled against nodejs, not electron
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] =
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] || join(__dirname, "..", "remote", "node_modules")
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
} else {
delete process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]
}
// Bootstrap ESM
await bootstrapESM()
// Load Server
await import("./deps/vscode/vs/server/node/server.cli.js")

View file

@ -0,0 +1,328 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import "./bootstrap-server.js" // this MUST come before other imports as it changes global state
import * as path from "path"
import * as http from "http"
import { AddressInfo } from "net"
import * as os from "os"
import * as readline from "readline"
import { performance } from "perf_hooks"
import { fileURLToPath } from "url"
import minimist from "minimist"
import { devInjectNodeModuleLookupPath, removeGlobalNodeJsModuleLookupPaths } from "./bootstrap-node.js"
import { bootstrapESM } from "./bootstrap-esm.js"
import { resolveNLSConfiguration } from "./deps/vscode/vs/base/node/nls.js"
import { product } from "./bootstrap-meta.js"
import * as perf from "./deps/vscode/vs/base/common/performance.js"
import { INLSConfiguration } from "./deps/vscode/vs/nls.js"
import { IServerAPI } from "./deps/vscode/vs/server/node/remoteExtensionHostAgentServer.js"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
perf.mark("code/server/start")
;(globalThis as any).vscodeServerStartTime = performance.now()
// Do a quick parse to determine if a server or the cli needs to be started
const parsedArgs = minimist(process.argv.slice(2), {
boolean: [
"start-server",
"list-extensions",
"print-ip-address",
"help",
"version",
"accept-server-license-terms",
"update-extensions",
],
string: [
"install-extension",
"install-builtin-extension",
"uninstall-extension",
"locate-extension",
"socket-path",
"host",
"port",
"compatibility",
],
alias: { help: "h", version: "v" },
})
;["host", "port", "accept-server-license-terms"].forEach((e) => {
if (!parsedArgs[e]) {
const envValue = process.env[`VSCODE_SERVER_${e.toUpperCase().replace("-", "_")}`]
if (envValue) {
parsedArgs[e] = envValue
}
}
})
const extensionLookupArgs = ["list-extensions", "locate-extension"]
const extensionInstallArgs = [
"install-extension",
"install-builtin-extension",
"uninstall-extension",
"update-extensions",
]
const shouldSpawnCli =
parsedArgs.help ||
parsedArgs.version ||
extensionLookupArgs.some((a) => !!parsedArgs[a]) ||
(extensionInstallArgs.some((a) => !!parsedArgs[a]) && !parsedArgs["start-server"])
const nlsConfiguration = await resolveNLSConfiguration({
userLocale: "en",
osLocale: "en",
commit: product.commit,
userDataPath: "",
nlsMetadataPath: __dirname,
})
if (shouldSpawnCli) {
loadCode(nlsConfiguration).then((mod) => {
mod.spawnCli()
})
} else {
let _remoteExtensionHostAgentServer: IServerAPI | null = null
let _remoteExtensionHostAgentServerPromise: Promise<IServerAPI> | null = null
const getRemoteExtensionHostAgentServer = () => {
if (!_remoteExtensionHostAgentServerPromise) {
_remoteExtensionHostAgentServerPromise = loadCode(nlsConfiguration).then(async (mod) => {
const server = await mod.createServer(address)
_remoteExtensionHostAgentServer = server
return server
})
}
return _remoteExtensionHostAgentServerPromise
}
if (Array.isArray(product.serverLicense) && product.serverLicense.length) {
console.log(product.serverLicense.join("\n"))
if (product.serverLicensePrompt && parsedArgs["accept-server-license-terms"] !== true) {
if (hasStdinWithoutTty()) {
console.log("To accept the license terms, start the server with --accept-server-license-terms")
process.exit(1)
}
try {
const accept = await prompt(product.serverLicensePrompt)
if (!accept) {
process.exit(1)
}
} catch (e) {
console.log(e)
process.exit(1)
}
}
}
let firstRequest = true
let firstWebSocket = true
let address: string | AddressInfo | null = null
const server = http.createServer(async (req, res) => {
if (firstRequest) {
firstRequest = false
perf.mark("code/server/firstRequest")
}
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
return remoteExtensionHostAgentServer.handleRequest(req, res)
})
server.on("upgrade", async (req, socket) => {
if (firstWebSocket) {
firstWebSocket = false
perf.mark("code/server/firstWebSocket")
}
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
// @ts-ignore
return remoteExtensionHostAgentServer.handleUpgrade(req, socket)
})
server.on("error", async (err) => {
const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer()
return remoteExtensionHostAgentServer.handleServerError(err)
})
const host =
sanitizeStringArg(parsedArgs["host"]) || (parsedArgs["compatibility"] !== "1.63" ? "localhost" : undefined)
const nodeListenOptions = parsedArgs["socket-path"]
? { path: sanitizeStringArg(parsedArgs["socket-path"]) }
: { host, port: await parsePort(host, sanitizeStringArg(parsedArgs["port"])) }
server.listen(nodeListenOptions, async () => {
let output =
Array.isArray(product.serverGreeting) && product.serverGreeting.length
? `\n\n${product.serverGreeting.join("\n")}\n\n`
: ``
if (typeof nodeListenOptions.port === "number" && parsedArgs["print-ip-address"]) {
const ifaces = os.networkInterfaces()
Object.keys(ifaces).forEach(function (ifname) {
ifaces[ifname]?.forEach(function (iface) {
if (!iface.internal && iface.family === "IPv4") {
output += `IP Address: ${iface.address}\n`
}
})
})
}
address = server.address()
if (address === null) {
throw new Error("Unexpected server address")
}
output += `Server bound to ${typeof address === "string" ? address : `${address.address}:${address.port} (${address.family})`}\n`
// Do not change this line. VS Code looks for this in the output.
output += `Extension host agent listening on ${typeof address === "string" ? address : address.port}\n`
console.log(output)
perf.mark("code/server/started")
;(globalThis as any).vscodeServerListenTime = performance.now()
await getRemoteExtensionHostAgentServer()
})
process.on("exit", () => {
server.close()
if (_remoteExtensionHostAgentServer) {
_remoteExtensionHostAgentServer.dispose()
}
})
}
function sanitizeStringArg(val: any): string | undefined {
if (Array.isArray(val)) {
// if an argument is passed multiple times, minimist creates an array
val = val.pop() // take the last item
}
return typeof val === "string" ? val : undefined
}
/**
* If `--port` is specified and describes a single port, connect to that port.
*
* If `--port`describes a port range
* then find a free port in that range. Throw error if no
* free port available in range.
*
* In absence of specified ports, connect to port 8000.
*/
async function parsePort(host: string | undefined, strPort: string | undefined): Promise<number> {
if (strPort) {
let range: { start: number; end: number } | undefined
if (strPort.match(/^\d+$/)) {
return parseInt(strPort, 10)
} else if ((range = parseRange(strPort))) {
const port = await findFreePort(host, range.start, range.end)
if (port !== undefined) {
return port
}
// Remote-SSH extension relies on this exact port error message, treat as an API
console.warn(`--port: Could not find free port in range: ${range.start} - ${range.end} (inclusive).`)
process.exit(1)
} else {
console.warn(
`--port "${strPort}" is not a valid number or range. Ranges must be in the form 'from-to' with 'from' an integer larger than 0 and not larger than 'end'.`,
)
process.exit(1)
}
}
return 8000
}
function parseRange(strRange: string): { start: number; end: number } | undefined {
const match = strRange.match(/^(\d+)-(\d+)$/)
if (match) {
const start = parseInt(match[1], 10),
end = parseInt(match[2], 10)
if (start > 0 && start <= end && end <= 65535) {
return { start, end }
}
}
return undefined
}
/**
* Starting at the `start` port, look for a free port incrementing
* by 1 until `end` inclusive. If no free port is found, undefined is returned.
*/
async function findFreePort(host: string | undefined, start: number, end: number): Promise<number | undefined> {
const testPort = (port: number) => {
return new Promise((resolve) => {
const server = http.createServer()
server
.listen(port, host, () => {
server.close()
resolve(true)
})
.on("error", () => {
resolve(false)
})
})
}
for (let port = start; port <= end; port++) {
if (await testPort(port)) {
return port
}
}
return undefined
}
async function loadCode(nlsConfiguration: INLSConfiguration) {
// required for `bootstrap-esm` to pick up NLS messages
process.env["VSCODE_NLS_CONFIG"] = JSON.stringify(nlsConfiguration)
// See https://github.com/microsoft/vscode-remote-release/issues/6543
// We would normally install a SIGPIPE listener in bootstrap-node.js
// But in certain situations, the console itself can be in a broken pipe state
// so logging SIGPIPE to the console will cause an infinite async loop
process.env["VSCODE_HANDLES_SIGPIPE"] = "true"
if (process.env["VSCODE_DEV"]) {
// When running out of sources, we need to load node modules from remote/node_modules,
// which are compiled against nodejs, not electron
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] =
process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"] ||
path.join(__dirname, "..", "remote", "node_modules")
devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
} else {
delete process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]
}
// Remove global paths from the node module lookup (node.js only)
removeGlobalNodeJsModuleLookupPaths()
// Bootstrap ESM
await bootstrapESM()
// Load Server
return import("./deps/vscode/vs/server/node/server.main.js")
}
function hasStdinWithoutTty(): boolean {
try {
return !process.stdin.isTTY // Via https://twitter.com/MylesBorins/status/782009479382626304
} catch (error) {
// Windows workaround for https://github.com/nodejs/node/issues/11656
}
return false
}
function prompt(question: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise((resolve, reject) => {
rl.question(question + " ", async function (data) {
rl.close()
const str = data.toString().trim().toLowerCase()
if (str === "" || str === "y" || str === "yes") {
resolve(true)
} else if (str === "n" || str === "no") {
resolve(false)
} else {
process.stdout.write("\nInvalid Response. Answer either yes (y, yes) or no (n, no)\n")
resolve(await prompt(question))
}
})
})
}

View file

@ -0,0 +1,6 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
export const DEBUG_PORT = 9229

View file

@ -0,0 +1,284 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import net from "net"
import start from "../deps/vscode/vs/workbench/api/node/extensionHostProcess.js"
import { FileRPCProtocolLogger } from "../deps/vscode/vs/workbench/services/extensions/common/fileRPCProtocolLogger.js"
import { RequestInitiator } from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
// Create global logger instance and export for use by other modules
export const fileLoggerGlobal = new FileRPCProtocolLogger("extension")
// Command line argument parsing
const args = process.argv.slice(2)
console.log("args:", args)
const LISTEN_MODE = args.includes("--listen") || process.env.VSCODE_EXTHOST_LISTEN === "true"
const PORT = parseInt(
args.find((arg) => arg.startsWith("--vscode-socket-port="))?.substring(21) ||
process.env.VSCODE_EXTHOST_DEBUG_PORT ||
"51234",
10,
)
const SOCKET_HOST =
args.find((arg) => arg.startsWith("--vscode-socket-host="))?.substring(21) ||
process.env.VSCODE_EXTHOST_SOCKET_HOST ||
"127.0.0.1"
const WILL_SEND_SOCKET =
args.find((arg) => arg.startsWith("--vscode-will-send-socket="))?.substring(26) ||
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET ||
"0"
const pipeName = process.env.VSCODE_EXTHOST_IPC_HOOK
console.log(`Extension host starting in ${LISTEN_MODE ? "LISTEN" : "CONNECT"} mode`)
console.log("PORT:", PORT)
console.log("SOCKET_HOST:", SOCKET_HOST)
console.log("WILL_SEND_SOCKET:", WILL_SEND_SOCKET)
console.log("pipeName:", pipeName)
if (pipeName) {
console.log("Using pipeName, connection will be handled by VSCode IPC")
} else {
// Reset parameter values back to environment variables
process.env.VSCODE_EXTHOST_SOCKET_PORT = PORT.toString()
process.env.VSCODE_EXTHOST_SOCKET_HOST = SOCKET_HOST
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET = WILL_SEND_SOCKET
console.log("set send socket:", process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET)
// Save original process methods
const originalProcessOn = process.on
const originalProcessSend = process.send || (() => false)
// Store message event handlers
const messageHandlers: ((message: any, socket?: net.Socket) => void)[] = []
// Reconnection related variables
let isReconnecting = false
let reconnectAttempts = 0
const MAX_RECONNECT_ATTEMPTS = 5
const RECONNECT_DELAY = 1000 // 1 second
// Override process.on
process.on = function (event: string, listener: (...args: any[]) => void): any {
if (event === "message") {
messageHandlers.push((message: any, socket?: net.Socket) => {
// Check listener parameter count
const paramCount = listener.length
if (paramCount === 1) {
// If only one parameter, pass only message
listener(message)
} else {
// If multiple parameters, pass message and socket
listener(message, socket)
}
})
}
return originalProcessOn.call(process, event, listener)
}
// Override process.send
process.send = function (message: any): boolean {
if (message?.type === "VSCODE_EXTHOST_IPC_READY") {
console.log("Extension host process is ready to receive socket")
if (LISTEN_MODE) {
startServer()
} else {
connect()
}
}
// Call original process.send
return originalProcessSend.call(process, message)
}
// Start server mode (for debugging)
function startServer() {
const server = net.createServer((socket) => {
console.log("Main process connected to extension host")
socket.setNoDelay(true)
// Prepare message to send to VSCode module
const socketMessage = {
type: "VSCODE_EXTHOST_IPC_SOCKET",
initialDataChunk: "",
skipWebSocketFrames: true,
permessageDeflate: false,
inflateBytes: "",
}
// Call all saved message handlers
messageHandlers.forEach((handler) => {
try {
handler(socketMessage, socket)
} catch (error) {
console.error("Error in message handler:", error)
}
})
socket.on("error", (error) => {
console.error("Socket error:", error)
// Don't close server, wait for reconnection
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Socket error:", error)
})
socket.on("close", () => {
console.log("Client connection closed, waiting for new connections...")
fileLoggerGlobal.logOutgoing(
0,
0,
RequestInitiator.LocalSide,
"Client connection closed, waiting for new connections...",
)
})
})
// Prevent server timeout closure, keep process active
const keepAliveInterval = setInterval(() => {
if (server.listening) {
console.log("Server still waiting for connections...")
}
}, 60000) // Print a log every minute to keep the process alive
// Ensure timer cleanup on process exit
process.on("exit", () => {
clearInterval(keepAliveInterval)
})
server.listen(PORT, "127.0.0.1", () => {
console.log(`Extension host server listening on 127.0.0.1:${PORT}`)
console.log("Waiting for main process to connect...")
})
server.on("error", (error) => {
console.error("Server error:", error)
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Server error:", error)
// No longer exit process, only log error
// Try to restart server
setTimeout(() => {
if (!server.listening) {
console.log("Attempting to restart server after error...")
try {
server.listen(PORT, "127.0.0.1")
} catch (e) {
console.error("Failed to restart server:", e)
}
}
}, 5000)
})
}
// Client mode (original behavior)
function connect() {
if (isReconnecting) {
console.log("Already in reconnection process, skipping")
return
}
try {
// Check connection method
// console.log("get send socket:", process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET);
// const useSocket = process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET === "1";
// if (!useSocket) {
// throw new Error('No connection method specified. Please set either VSCODE_EXTHOST_IPC_HOOK or VSCODE_EXTHOST_WILL_SEND_SOCKET');
// }
// Use regular TCP Socket
const host = process.env.VSCODE_EXTHOST_SOCKET_HOST || "127.0.0.1"
const port = parseInt(process.env.VSCODE_EXTHOST_SOCKET_PORT || "0", 10)
if (!port) {
throw new Error("Invalid socket port")
}
console.log(`Attempting to connect to ${host}:${port}`)
// Establish socket connection
const socket = net.createConnection(port, host)
// Set the noDelay option for the socket
socket.setNoDelay(true)
socket.on("connect", () => {
console.log("Connected to main server")
isReconnecting = false
reconnectAttempts = 0
// Prepare the message to be sent to the VSCode module
const socketMessage = {
type: "VSCODE_EXTHOST_IPC_SOCKET",
initialDataChunk: "",
skipWebSocketFrames: true,
permessageDeflate: false,
inflateBytes: "",
}
// Call all saved message handler functions
messageHandlers.forEach((handler) => {
try {
handler(socketMessage, socket)
} catch (error) {
console.error("Error in message handler:", error)
}
})
})
socket.on("error", (error: Error) => {
console.error("Socket connection error:", error)
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Socket connection error:", error)
handleDisconnect()
})
socket.on("close", () => {
console.log("Socket connection closed")
handleDisconnect()
})
} catch (error) {
console.error("Connection error:", error)
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Connection error:", error)
handleDisconnect()
}
}
// Handle disconnection
async function handleDisconnect() {
if (isReconnecting) {
console.log("Already in reconnection process, skipping")
fileLoggerGlobal.logOutgoing(0, 0, RequestInitiator.LocalSide, "Already in reconnection process, skipping")
return
}
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error("Max reconnection attempts reached. Giving up.")
fileLoggerGlobal.logOutgoing(
0,
0,
RequestInitiator.LocalSide,
"Max reconnection attempts reached. Giving up.",
)
return
}
isReconnecting = true
reconnectAttempts++
console.log(`Attempting to reconnect (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
// Retry after waiting for a period of time
console.log(`Waiting ${RECONNECT_DELAY}ms before reconnecting...`)
await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY))
console.log("Reconnection delay finished, attempting to connect...")
// Reset reconnection state to allow new reconnection attempts
isReconnecting = false
connect()
}
}
console.log("Starting extension host process...")
// Adjust logic: only start directly in non-LISTEN mode
if (LISTEN_MODE) {
process.env.VSCODE_EXTHOST_WILL_SEND_SOCKET = "1"
}
start()

View file

@ -0,0 +1,101 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
ExtensionIdentifier,
IExtensionDescription,
TargetPlatform,
} from "../deps/vscode/vs/platform/extensions/common/extensions.js"
import { URI } from "../deps/vscode/vs/base/common/uri.js"
import { ExtHostContext } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
import * as fs from "fs"
import * as path from "path"
export class ExtensionManager {
private extensionDescriptions: Map<string, IExtensionDescription> = new Map()
/**
* Parse extension description information
* @param extensionPath Extension path
* @returns Extension description object
*/
private parseExtensionDescription(extensionPath: string): IExtensionDescription {
const packageJsonPath = path.join(extensionPath, "extension.package.json")
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
return {
identifier: new ExtensionIdentifier(packageJson.name),
name: packageJson.name,
displayName: packageJson.displayName,
description: packageJson.description,
version: packageJson.version,
publisher: packageJson.publisher,
main: "./extension.cjs",
activationEvents: packageJson.activationEvents || ["onStartupFinished"],
extensionLocation: URI.file(path.resolve(extensionPath)),
targetPlatform: TargetPlatform.UNIVERSAL,
isBuiltin: false,
isUserBuiltin: false,
isUnderDevelopment: false,
engines: packageJson.engines || { vscode: "^1.0.0" },
preRelease: false,
capabilities: {},
extensionDependencies: packageJson.extensionDependencies || [],
}
}
/**
* Get all parsed extension descriptions
* @returns Extension description array
*/
public getAllExtensionDescriptions(): IExtensionDescription[] {
return Array.from(this.extensionDescriptions.values())
}
/**
* Get description information for specified extension
* @param extensionId Extension ID
* @returns Extension description object, or undefined if not exists
*/
public getExtensionDescription(extensionId: string): IExtensionDescription | undefined {
return this.extensionDescriptions.get(extensionId)
}
/**
* Register an extension
* @param extensionPath Extension path
* @returns Extension description object
*/
public registerExtension(extensionPath: string): IExtensionDescription {
const extensionDescription = this.parseExtensionDescription(extensionPath)
this.extensionDescriptions.set(extensionDescription.identifier.value, extensionDescription)
return extensionDescription
}
/**
* Activate a registered extension
* @param extensionId Extension ID
* @param protocol RPC protocol
*/
public async activateExtension(extensionId: string, protocol: IRPCProtocol): Promise<void> {
const extensionDescription = this.extensionDescriptions.get(extensionId)
if (!extensionDescription) {
throw new Error(`Extension ${extensionId} is not registered`)
}
try {
const extensionService = protocol.getProxy(ExtHostContext.ExtHostExtensionService)
await extensionService.$activate(extensionDescription.identifier, {
startup: true,
extensionId: extensionDescription.identifier,
activationEvent: "api",
})
} catch (error) {
console.error(`Failed to activate extension ${extensionId}:`, error)
throw error
}
}
}

202
jetbrains/host/src/main.ts Normal file
View file

@ -0,0 +1,202 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { fork } from "child_process"
import * as path from "path"
import { fileURLToPath } from "url"
import * as net from "net"
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
import { NodeSocket } from "../deps/vscode/vs/base/parts/ipc/node/ipc.net.js"
import { PersistentProtocol } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
import { DEBUG_PORT } from "./config.js"
import {
MessageType,
createMessageOfType,
isMessageOfType,
UIKind,
IExtensionHostInitData,
} from "../deps/vscode/vs/workbench/services/extensions/common/extensionHostProtocol.js"
import { SocketCloseEvent, SocketCloseEventType } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
import { IDisposable } from "../deps/vscode/vs/base/common/lifecycle.js"
import { URI } from "../deps/vscode/vs/base/common/uri.js"
import { RPCManager } from "./rpcManager.js"
import { ExtensionManager } from "./extensionManager.js"
// Get current file directory path
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Create ExtensionManager instance and register extension
const extensionManager = new ExtensionManager()
const rooCodeIdentifier = extensionManager.registerExtension("roo-code").identifier
// Declare extension host process variables
let extHostProcess: ReturnType<typeof fork>
let protocol: PersistentProtocol | null = null
let rpcManager: RPCManager | null = null
// Create socket server
const server = net.createServer((socket) => {
console.log("Someone connected to main server")
// Set socket noDelay option
socket.setNoDelay(true)
// Wrap socket with NodeSocket
const nodeSocket = new NodeSocket(socket)
// Listen for NodeSocket close events
const closeDisposable: IDisposable = nodeSocket.onClose((event: SocketCloseEvent | undefined) => {
console.log("NodeSocket close event received")
if (event?.type === SocketCloseEventType.NodeSocketCloseEvent) {
if (event.hadError) {
console.error("Socket closed with error:", event.error)
} else {
console.log("Socket closed normally")
}
}
closeDisposable.dispose()
})
// Create PersistentProtocol instance
protocol = new PersistentProtocol({
socket: nodeSocket,
initialChunk: null,
})
// Set protocol message handler
protocol.onMessage((message) => {
if (isMessageOfType(message, MessageType.Ready)) {
console.log("Extension host is ready")
// Send initialization data
const initData: IExtensionHostInitData = {
commit: "development",
version: "1.0.0",
quality: undefined,
parentPid: process.pid,
environment: {
isExtensionDevelopmentDebug: false,
appName: "VSCodeAPIHook",
appHost: "node",
appLanguage: "en",
appUriScheme: "vscode",
appRoot: URI.file(__dirname),
globalStorageHome: URI.file(path.join(__dirname, "globalStorage")),
workspaceStorageHome: URI.file(path.join(__dirname, "workspaceStorage")),
extensionDevelopmentLocationURI: undefined,
extensionTestsLocationURI: undefined,
useHostProxy: false,
skipWorkspaceStorageLock: false,
isExtensionTelemetryLoggingOnly: false,
},
workspace: {
id: "development-workspace",
name: "Development Workspace",
transient: false,
configuration: null,
isUntitled: false,
},
remote: {
authority: undefined,
connectionData: null,
isRemote: false,
},
extensions: {
versionId: 1,
allExtensions: extensionManager.getAllExtensionDescriptions(),
myExtensions: extensionManager.getAllExtensionDescriptions().map((ext) => ext.identifier),
activationEvents: extensionManager.getAllExtensionDescriptions().reduce(
(events, ext) => {
if (ext.activationEvents) {
events[ext.identifier.value] = ext.activationEvents
}
return events
},
{} as { [extensionId: string]: string[] },
),
},
telemetryInfo: {
sessionId: "development-session",
machineId: "development-machine",
sqmId: "",
devDeviceId: "",
firstSessionDate: new Date().toISOString(),
msftInternal: false,
},
logLevel: 0, // Info level
loggers: [],
logsLocation: URI.file(path.join(__dirname, "logs")),
autoStart: true,
consoleForward: {
includeStack: false,
logNative: false,
},
uiKind: UIKind.Desktop,
}
protocol?.send(VSBuffer.fromString(JSON.stringify(initData)))
} else if (isMessageOfType(message, MessageType.Initialized)) {
console.log("Extension host initialized")
// Create RPCManager instance
rpcManager = new RPCManager(protocol!, extensionManager)
rpcManager.startInitialize()
// Activate rooCode plugin
const rpcProtocol = rpcManager.getRPCProtocol()
if (rpcProtocol) {
extensionManager.activateExtension(rooCodeIdentifier.value, rpcProtocol).catch((error: Error) => {
console.error("Failed to load rooCode plugin:", error)
})
} else {
console.error("Failed to get RPCProtocol from RPCManager")
}
}
})
})
function startExtensionHostProcess() {
process.env.VSCODE_DEBUG = "true"
let nodeOptions = process.env.VSCODE_DEBUG ? `--inspect-brk=9229` : `--inspect=${DEBUG_PORT}`
console.log("will start extension host process with options:", nodeOptions)
// Create extension host process and pass environment variables
extHostProcess = fork(path.join(__dirname, "extension.js"), [], {
env: {
...process.env,
VSCODE_EXTHOST_WILL_SEND_SOCKET: "1",
VSCODE_EXTHOST_SOCKET_HOST: "127.0.0.1",
VSCODE_EXTHOST_SOCKET_PORT: (server.address() as net.AddressInfo)?.port?.toString() || "0",
NODE_OPTIONS: nodeOptions,
},
})
// Handle extension host process exit
extHostProcess.on("exit", (code: number | null, signal: string | null) => {
console.log(`Extension host process exited with code ${code} and signal ${signal}`)
server.close()
})
}
// Listen on random port
server.listen(0, "127.0.0.1", () => {
const address = server.address()
if (address && typeof address !== "string") {
console.log(`Server listening on port ${address.port}`)
startExtensionHostProcess()
}
})
// Handle process exit
process.on("SIGINT", () => {
console.log("Cleaning up...")
if (protocol) {
protocol.send(createMessageOfType(MessageType.Terminate))
}
server.close()
if (extHostProcess) {
extHostProcess.kill()
}
process.exit(0)
})

View file

@ -0,0 +1,997 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { RPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
import { PersistentProtocol } from "../deps/vscode/vs/base/parts/ipc/common/ipc.net.js"
import { MainContext, ExtHostContext } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
import {
IRPCProtocolLogger,
RequestInitiator,
} from "../deps/vscode/vs/workbench/services/extensions/common/rpcProtocol.js"
import { UriComponents, UriDto } from "../deps/vscode/vs/base/common/uri.js"
import { LogLevel } from "../deps/vscode/vs/platform/log/common/log.js"
import { ILoggerResource } from "../deps/vscode/vs/platform/log/common/log.js"
import { TerminalLaunchConfig } from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
import { IRawFileMatch2 } from "../deps/vscode/vs/workbench/services/search/common/search.js"
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
import { SerializedError, transformErrorFromSerialization } from "../deps/vscode/vs/base/common/errors.js"
import { IRemoteConsoleLog } from "../deps/vscode/vs/base/common/console.js"
import { FileType, FilePermission, FileSystemProviderErrorCode } from "../deps/vscode/vs/platform/files/common/files.js"
import * as fs from "fs"
import { promisify } from "util"
import { ConfigurationModel } from "../deps/vscode/vs/platform/configuration/common/configurationModels.js"
import { NullLogService } from "../deps/vscode/vs/platform/log/common/log.js"
import { ExtensionIdentifier } from "../deps/vscode/vs/platform/extensions/common/extensions.js"
import { ExtensionActivationReason } from "../deps/vscode/vs/workbench/services/extensions/common/extensions.js"
import { IExtensionDescription } from "../deps/vscode/vs/platform/extensions/common/extensions.js"
import { Dto } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
import { ExtensionManager } from "./extensionManager.js"
import { WebViewManager } from "./webViewManager.js"
// Promisify Node.js fs functions
const fsStat = promisify(fs.stat)
const fsReadDir = promisify(fs.readdir)
const fsReadFile = promisify(fs.readFile)
const fsWriteFile = promisify(fs.writeFile)
const fsRename = promisify(fs.rename)
const fsCopyFile = promisify(fs.copyFile)
const fsUnlink = promisify(fs.unlink)
const fsLstat = promisify(fs.lstat)
const fsMkdir = promisify(fs.mkdir)
class RPCLogger implements IRPCProtocolLogger {
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, msg: string, data?: any): void {
if (msg == "ack") {
return
}
console.log(`[RPC] ExtHost: ${msg}`)
}
logOutgoing(msgLength: number, req: number, initiator: RequestInitiator, msg: string, data?: any): void {
if (msg == "ack" || msg == "reply:") {
return
}
console.log(`[RPC] Main: ${msg}`)
}
}
export class RPCManager {
private rpcProtocol: IRPCProtocol
private logger: RPCLogger
private extensionManager: ExtensionManager
constructor(
private protocol: PersistentProtocol,
extensionManager: ExtensionManager,
) {
this.logger = new RPCLogger()
this.rpcProtocol = new RPCProtocol(this.protocol, this.logger)
this.extensionManager = extensionManager
this.setupDefaultProtocols()
this.setupExtensionRequiredProtocols()
this.setupRooCodeRequiredProtocols()
}
public startInitialize(): void {
// ExtHostConfiguration
const extHostConfiguration = this.rpcProtocol.getProxy(ExtHostContext.ExtHostConfiguration)
// Send initialization configuration message
extHostConfiguration.$initializeConfiguration({
defaults: ConfigurationModel.createEmptyModel(new NullLogService()),
policy: ConfigurationModel.createEmptyModel(new NullLogService()),
application: ConfigurationModel.createEmptyModel(new NullLogService()),
userLocal: ConfigurationModel.createEmptyModel(new NullLogService()),
userRemote: ConfigurationModel.createEmptyModel(new NullLogService()),
workspace: ConfigurationModel.createEmptyModel(new NullLogService()),
folders: [],
configurationScopes: [],
})
const extHostWorkspace = this.rpcProtocol.getProxy(ExtHostContext.ExtHostWorkspace)
// Initialize workspace
extHostWorkspace.$initializeWorkspace(null, true)
}
// Protocols needed for extHost process startup and initialization
public setupDefaultProtocols(): void {
if (!this.rpcProtocol) {
throw new Error("RPCProtocol not initialized")
}
// MainThreadErrors
this.rpcProtocol.set(MainContext.MainThreadErrors, {
dispose(): void {
// Nothing to do
},
$onUnexpectedError(err: any | SerializedError): void {
if (err && err.$isError) {
err = transformErrorFromSerialization(err)
}
console.error("Unexpected error:", err)
/*
if (err instanceof Error && err.stack) {
console.error('Stack trace:', err.stack);
}
*/
},
})
// MainThreadConsole
this.rpcProtocol.set(MainContext.MainThreadConsole, {
dispose(): void {
// Nothing to do
},
$logExtensionHostMessage(entry: IRemoteConsoleLog): void {
// Parse the entry
const args = this.parseRemoteConsoleLog(entry)
// Log based on severity
switch (entry.severity) {
case "log":
case "info":
console.log("[Extension Host]", ...args)
break
case "warn":
console.warn("[Extension Host]", ...args)
break
case "error":
console.error("[Extension Host]", ...args)
break
case "debug":
console.debug("[Extension Host]", ...args)
break
default:
console.log("[Extension Host]", ...args)
}
},
parseRemoteConsoleLog(entry: IRemoteConsoleLog): any[] {
const args: any[] = []
try {
// Parse the arguments string as JSON
const parsedArguments = JSON.parse(entry.arguments)
args.push(...parsedArguments)
} catch (error) {
// If parsing fails, just log the raw arguments string
args.push("Unable to log remote console arguments", entry.arguments)
}
return args
},
})
// MainThreadLogger
this.rpcProtocol.set(MainContext.MainThreadLogger, {
$log(file: UriComponents, messages: [LogLevel, string][]): void {
console.log("Logger message:", { file, messages })
},
$flush(file: UriComponents): void {
console.log("Flush logger:", file)
},
$createLogger(file: UriComponents, options?: any): Promise<void> {
console.log("Create logger:", { file, options })
return Promise.resolve()
},
$registerLogger(logger: UriDto<ILoggerResource>): Promise<void> {
console.log("Register logger (id: ", logger.id, ", name: ", logger.name, ")")
return Promise.resolve()
},
$deregisterLogger(resource: UriComponents): Promise<void> {
console.log("Deregister logger:", resource)
return Promise.resolve()
},
$setVisibility(resource: UriComponents, visible: boolean): Promise<void> {
console.log("Set logger visibility:", { resource, visible })
return Promise.resolve()
},
})
// MainThreadCommands
this.rpcProtocol.set(MainContext.MainThreadCommands, {
$registerCommand(id: string): void {
console.log("Register command:", id)
},
$unregisterCommand(id: string): void {
console.log("Unregister command:", id)
},
$executeCommand<T>(id: string, ...args: any[]): Promise<T> {
console.log("Execute command:", id, args)
return Promise.resolve(null as T)
},
$fireCommandActivationEvent(id: string): void {
console.log("Fire command activation event:", id)
},
$getCommands(): Promise<string[]> {
return Promise.resolve([])
},
dispose(): void {
console.log("Dispose MainThreadCommands")
},
})
// MainThreadTerminalService
this.rpcProtocol.set(MainContext.MainThreadTerminalService, {
$registerProcessSupport(isSupported: boolean): void {
console.log("Register process support:", isSupported)
},
$createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise<void> {
console.log("Create terminal:", { extHostTerminalId, config })
return Promise.resolve()
},
$dispose(id: string): void {
console.log("Dispose terminal:", id)
},
$hide(id: string): void {
console.log("Hide terminal:", id)
},
$sendText(id: string, text: string, shouldExecute: boolean): void {
console.log("Send text to terminal:", { id, text, shouldExecute })
},
$show(id: string, preserveFocus: boolean): void {
console.log("Show terminal:", { id, preserveFocus })
},
$registerProfileProvider(id: string, extensionIdentifier: string): void {
console.log("Register profile provider:", { id, extensionIdentifier })
},
$unregisterProfileProvider(id: string): void {
console.log("Unregister profile provider:", id)
},
$registerCompletionProvider(id: string, extensionIdentifier: string, ...triggerCharacters: string[]): void {
console.log("Register completion provider:", { id, extensionIdentifier, triggerCharacters })
},
$unregisterCompletionProvider(id: string): void {
console.log("Unregister completion provider:", id)
},
$registerQuickFixProvider(id: string, extensionIdentifier: string): void {
console.log("Register quick fix provider:", { id, extensionIdentifier })
},
$unregisterQuickFixProvider(id: string): void {
console.log("Unregister quick fix provider:", id)
},
$setEnvironmentVariableCollection(
extensionIdentifier: string,
persistent: boolean,
collection: any,
descriptionMap: any,
): void {
console.log("Set environment variable collection:", {
extensionIdentifier,
persistent,
collection,
descriptionMap,
})
},
$startSendingDataEvents(): void {
console.log("Start sending data events")
},
$stopSendingDataEvents(): void {
console.log("Stop sending data events")
},
$startSendingCommandEvents(): void {
console.log("Start sending command events")
},
$stopSendingCommandEvents(): void {
console.log("Stop sending command events")
},
$startLinkProvider(): void {
console.log("Start link provider")
},
$stopLinkProvider(): void {
console.log("Stop link provider")
},
$sendProcessData(terminalId: number, data: string): void {
console.log("Send process data:", { terminalId, data })
},
$sendProcessReady(terminalId: number, pid: number, cwd: string, windowsPty: any): void {
console.log("Send process ready:", { terminalId, pid, cwd, windowsPty })
},
$sendProcessProperty(terminalId: number, property: any): void {
console.log("Send process property:", { terminalId, property })
},
$sendProcessExit(terminalId: number, exitCode: number | undefined): void {
console.log("Send process exit:", { terminalId, exitCode })
},
dispose(): void {
console.log("Dispose MainThreadTerminalService")
},
})
// MainThreadWindow
this.rpcProtocol.set(MainContext.MainThreadWindow, {
$getInitialState(): Promise<{ isFocused: boolean; isActive: boolean }> {
console.log("Get initial state")
return Promise.resolve({ isFocused: false, isActive: false })
},
$openUri(uri: UriComponents, uriString: string | undefined, options: any): Promise<boolean> {
console.log("Open URI:", { uri, uriString, options })
return Promise.resolve(true)
},
$asExternalUri(uri: UriComponents, options: any): Promise<UriComponents> {
console.log("As external URI:", { uri, options })
return Promise.resolve(uri)
},
dispose(): void {
console.log("Dispose MainThreadWindow")
},
})
// MainThreadSearch
this.rpcProtocol.set(MainContext.MainThreadSearch, {
$registerFileSearchProvider(handle: number, scheme: string): void {
console.log("Register file search provider:", { handle, scheme })
},
$registerAITextSearchProvider(handle: number, scheme: string): void {
console.log("Register AI text search provider:", { handle, scheme })
},
$registerTextSearchProvider(handle: number, scheme: string): void {
console.log("Register text search provider:", { handle, scheme })
},
$unregisterProvider(handle: number): void {
console.log("Unregister provider:", handle)
},
$handleFileMatch(handle: number, session: number, data: UriComponents[]): void {
console.log("Handle file match:", { handle, session, data })
},
$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void {
console.log("Handle text match:", { handle, session, data })
},
$handleTelemetry(eventName: string, data: any): void {
console.log("Handle telemetry:", { eventName, data })
},
dispose(): void {
console.log("Dispose MainThreadSearch")
},
})
// MainThreadTask
this.rpcProtocol.set(MainContext.MainThreadTask, {
$createTaskId(task: any): Promise<string> {
console.log("Create task ID:", task)
return Promise.resolve("task-id")
},
$registerTaskProvider(handle: number, type: string): Promise<void> {
console.log("Register task provider:", { handle, type })
return Promise.resolve()
},
$unregisterTaskProvider(handle: number): Promise<void> {
console.log("Unregister task provider:", handle)
return Promise.resolve()
},
$fetchTasks(filter?: any): Promise<any[]> {
console.log("Fetch tasks:", filter)
return Promise.resolve([])
},
$getTaskExecution(value: any): Promise<any> {
console.log("Get task execution:", value)
return Promise.resolve(null)
},
$executeTask(task: any): Promise<any> {
console.log("Execute task:", task)
return Promise.resolve(null)
},
$terminateTask(id: string): Promise<void> {
console.log("Terminate task:", id)
return Promise.resolve()
},
$registerTaskSystem(scheme: string, info: any): void {
console.log("Register task system:", { scheme, info })
},
$customExecutionComplete(id: string, result?: number): Promise<void> {
console.log("Custom execution complete:", { id, result })
return Promise.resolve()
},
$registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise<void> {
console.log("Register supported executions:", { custom, shell, process })
return Promise.resolve()
},
dispose(): void {
console.log("Dispose MainThreadTask")
},
})
// MainThreadConfiguration
this.rpcProtocol.set(MainContext.MainThreadConfiguration, {
$updateConfigurationOption(
target: any,
key: string,
value: any,
overrides: any,
scopeToLanguage: boolean | undefined,
): Promise<void> {
console.log("Update configuration option:", { target, key, value, overrides, scopeToLanguage })
return Promise.resolve()
},
$removeConfigurationOption(
target: any,
key: string,
overrides: any,
scopeToLanguage: boolean | undefined,
): Promise<void> {
console.log("Remove configuration option:", { target, key, overrides, scopeToLanguage })
return Promise.resolve()
},
dispose(): void {
console.log("Dispose MainThreadConfiguration")
},
})
// MainThreadFileSystem
this.rpcProtocol.set(MainContext.MainThreadFileSystem, {
async $registerFileSystemProvider(
handle: number,
scheme: string,
capabilities: any,
readonlyMessage?: any,
): Promise<void> {
console.log("Register file system provider:", { handle, scheme, capabilities, readonlyMessage })
},
$unregisterProvider(handle: number): void {
console.log("Unregister provider:", handle)
},
$onFileSystemChange(handle: number, resource: any[]): void {
console.log("File system change:", { handle, resource })
},
async $stat(resource: UriComponents): Promise<any> {
console.log("Stat:", resource)
try {
const filePath = this.uriToPath(resource)
const stats = await fsStat(filePath)
return {
type: this.getFileType(stats),
ctime: stats.birthtimeMs,
mtime: stats.mtimeMs,
size: stats.size,
permissions: stats.mode & 0o444 ? FilePermission.Readonly : undefined,
}
} catch (error) {
console.error("Error in $stat:", error)
throw this.handleFileSystemError(error)
}
},
async $readdir(resource: UriComponents): Promise<[string, FileType][]> {
console.log("Read directory:", resource)
try {
const filePath = this.uriToPath(resource)
const entries = await fsReadDir(filePath, { withFileTypes: true })
return entries.map((entry) => {
let type = FileType.Unknown
if (entry.isFile()) {
type = FileType.File
} else if (entry.isDirectory()) {
type = FileType.Directory
}
// Check if it's a symbolic link
if (entry.isSymbolicLink()) {
type |= FileType.SymbolicLink
}
return [entry.name, type] as [string, FileType]
})
} catch (error) {
console.error("Error in $readdir:", error)
throw this.handleFileSystemError(error)
}
},
async $readFile(resource: UriComponents): Promise<any> {
console.log("Read file:", resource)
try {
const filePath = this.uriToPath(resource)
const buffer = await fsReadFile(filePath)
return VSBuffer.wrap(buffer)
} catch (error) {
console.error("Error in $readFile:", error)
throw this.handleFileSystemError(error)
}
},
async $writeFile(resource: UriComponents, content: any): Promise<void> {
console.log("Write file:", { resource, content })
try {
const filePath = this.uriToPath(resource)
const buffer = content instanceof VSBuffer ? content.buffer : content
await fsWriteFile(filePath, buffer)
} catch (error) {
console.error("Error in $writeFile:", error)
throw this.handleFileSystemError(error)
}
},
async $rename(resource: UriComponents, target: UriComponents, opts: any): Promise<void> {
console.log("Rename:", { resource, target, opts })
try {
const sourcePath = this.uriToPath(resource)
const targetPath = this.uriToPath(target)
// Check if target exists and handle overwrite option
if (opts.overwrite) {
try {
await fsUnlink(targetPath)
} catch (error) {
// Ignore error if file doesn't exist
}
}
await fsRename(sourcePath, targetPath)
} catch (error) {
console.error("Error in $rename:", error)
throw this.handleFileSystemError(error)
}
},
async $copy(resource: UriComponents, target: UriComponents, opts: any): Promise<void> {
console.log("Copy:", { resource, target, opts })
try {
const sourcePath = this.uriToPath(resource)
const targetPath = this.uriToPath(target)
// Check if target exists and handle overwrite option
if (opts.overwrite) {
try {
await fsUnlink(targetPath)
} catch (error) {
// Ignore error if file doesn't exist
}
}
await fsCopyFile(sourcePath, targetPath)
} catch (error) {
console.error("Error in $copy:", error)
throw this.handleFileSystemError(error)
}
},
async $mkdir(resource: UriComponents): Promise<void> {
console.log("Make directory:", resource)
try {
const dirPath = this.uriToPath(resource)
await fsMkdir(dirPath, { recursive: true })
} catch (error) {
console.error("Error in $mkdir:", error)
throw this.handleFileSystemError(error)
}
},
async $delete(resource: UriComponents, opts: any): Promise<void> {
console.log("Delete:", { resource, opts })
try {
const filePath = this.uriToPath(resource)
// Check if it's a directory
const stats = await fsLstat(filePath)
if (stats.isDirectory()) {
// For directories, we need to implement recursive deletion
// This is a simplified version
await fs.promises.rm(filePath, { recursive: true })
} else {
await fsUnlink(filePath)
}
} catch (error) {
console.error("Error in $delete:", error)
throw this.handleFileSystemError(error)
}
},
async $ensureActivation(scheme: string): Promise<void> {
console.log("Ensure activation:", scheme)
// No-op implementation
return Promise.resolve()
},
dispose(): void {
console.log("Dispose MainThreadFileSystem")
},
// Helper methods
uriToPath(uri: UriComponents): string {
// Convert URI to file path
// This is a simplified implementation
if (uri.scheme !== "file") {
throw new Error(`Unsupported URI scheme: ${uri.scheme}`)
}
// Handle Windows paths
let filePath = uri.path || ""
if (process.platform === "win32" && filePath.startsWith("/")) {
filePath = filePath.substring(1)
}
return filePath
},
getFileType(stats: fs.Stats): FileType {
let type = FileType.Unknown
if (stats.isFile()) {
type = FileType.File
} else if (stats.isDirectory()) {
type = FileType.Directory
}
// Check if it's a symbolic link
if (stats.isSymbolicLink()) {
type |= FileType.SymbolicLink
}
return type
},
handleFileSystemError(error: any): Error {
// Map Node.js errors to VSCode file system errors
if (error.code === "ENOENT") {
const err = new Error(error.message)
err.name = FileSystemProviderErrorCode.FileNotFound
return err
} else if (error.code === "EACCES" || error.code === "EPERM") {
const err = new Error(error.message)
err.name = FileSystemProviderErrorCode.NoPermissions
return err
} else if (error.code === "EEXIST") {
const err = new Error(error.message)
err.name = FileSystemProviderErrorCode.FileExists
return err
} else if (error.code === "EISDIR") {
const err = new Error(error.message)
err.name = FileSystemProviderErrorCode.FileIsADirectory
return err
} else if (error.code === "ENOTDIR") {
const err = new Error(error.message)
err.name = FileSystemProviderErrorCode.FileNotADirectory
return err
}
// Default error
return error
},
})
// MainThreadLanguageModelTools
this.rpcProtocol.set(MainContext.MainThreadLanguageModelTools, {
$getTools(): Promise<any[]> {
console.log("Getting language model tools")
return Promise.resolve([])
},
$invokeTool(dto: any, token: any): Promise<any> {
console.log("Invoking language model tool:", dto)
return Promise.resolve({})
},
$countTokensForInvocation(callId: string, input: string, token: any): Promise<number> {
console.log("Counting tokens for invocation:", { callId, input })
return Promise.resolve(0)
},
$registerTool(id: string): void {
console.log("Registering language model tool:", id)
},
$unregisterTool(name: string): void {
console.log("Unregistering language model tool:", name)
},
dispose(): void {
console.log("Disposing MainThreadLanguageModelTools")
},
})
}
// Protocols needed for general extension loading process
public setupExtensionRequiredProtocols(): void {
if (!this.rpcProtocol) {
return
}
this.rpcProtocol.set(MainContext.MainThreadExtensionService, {
$getExtension: async (extensionId: string): Promise<Dto<IExtensionDescription> | undefined> => {
console.log(`Getting extension: ${extensionId}`)
return this.extensionManager.getExtensionDescription(extensionId)
},
$activateExtension: async (
extensionId: ExtensionIdentifier,
reason: ExtensionActivationReason,
): Promise<void> => {
console.log(`Activating extension ${extensionId.value} with reason:`, reason)
await this.extensionManager.activateExtension(extensionId.value, this.rpcProtocol)
},
$onWillActivateExtension: async (extensionId: ExtensionIdentifier): Promise<void> => {
console.log(`Extension ${extensionId.value} will be activated`)
},
$onDidActivateExtension: (
extensionId: ExtensionIdentifier,
codeLoadingTime: number,
activateCallTime: number,
activateResolvedTime: number,
activationReason: ExtensionActivationReason,
): void => {
console.log(`Extension ${extensionId.value} was activated with reason:`, activationReason)
},
$onExtensionActivationError: async (
extensionId: ExtensionIdentifier,
error: any,
missingExtensionDependency: any | null,
): Promise<void> => {
console.error(`Extension ${extensionId.value} activation error:`, error)
},
$onExtensionRuntimeError: (extensionId: ExtensionIdentifier, error: any): void => {
console.error(`Extension ${extensionId.value} runtime error:`, error)
},
$setPerformanceMarks: async (marks: { name: string; startTime: number }[]): Promise<void> => {
console.log("Setting performance marks:", marks)
},
$asBrowserUri: async (uri: any): Promise<any> => {
console.log("Converting to browser URI:", uri)
return uri
},
dispose: () => {
console.log("Disposing MainThreadExtensionService")
},
})
this.rpcProtocol.set(MainContext.MainThreadTelemetry, {
$publicLog(eventName: string, data?: any): void {
console.log(`[Telemetry] ${eventName}`, data)
},
$publicLog2<E extends any = never, T extends any = never>(eventName: string, data?: any): void {
console.log(`[Telemetry] ${eventName}`, data)
},
dispose(): void {
console.log("Disposing MainThreadTelemetry")
},
})
this.rpcProtocol.set(MainContext.MainThreadDebugService, {
$registerDebugTypes(debugTypes: string[]): void {
console.log("Register debug types:", debugTypes)
},
$sessionCached(sessionID: string): void {
console.log("Session cached:", sessionID)
},
$acceptDAMessage(handle: number, message: any): void {
console.log("Accept debug adapter message:", { handle, message })
},
$acceptDAError(handle: number, name: string, message: string, stack: string | undefined): void {
console.error("Debug adapter error:", { handle, name, message, stack })
},
$acceptDAExit(handle: number, code: number | undefined, signal: string | undefined): void {
console.log("Debug adapter exit:", { handle, code, signal })
},
async $registerDebugConfigurationProvider(
type: string,
triggerKind: any,
hasProvideMethod: boolean,
hasResolveMethod: boolean,
hasResolve2Method: boolean,
handle: number,
): Promise<void> {
console.log("Register debug configuration provider:", {
type,
triggerKind,
hasProvideMethod,
hasResolveMethod,
hasResolve2Method,
handle,
})
},
async $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise<void> {
console.log("Register debug adapter descriptor factory:", { type, handle })
},
$unregisterDebugConfigurationProvider(handle: number): void {
console.log("Unregister debug configuration provider:", handle)
},
$unregisterDebugAdapterDescriptorFactory(handle: number): void {
console.log("Unregister debug adapter descriptor factory:", handle)
},
async $startDebugging(folder: any, nameOrConfig: string | any, options: any): Promise<boolean> {
console.log("Start debugging:", { folder, nameOrConfig, options })
return true
},
async $stopDebugging(sessionId: string | undefined): Promise<void> {
console.log("Stop debugging:", sessionId)
},
$setDebugSessionName(id: string, name: string): void {
console.log("Set debug session name:", { id, name })
},
async $customDebugAdapterRequest(id: string, command: string, args: any): Promise<any> {
console.log("Custom debug adapter request:", { id, command, args })
return null
},
async $getDebugProtocolBreakpoint(id: string, breakpoinId: string): Promise<any> {
console.log("Get debug protocol breakpoint:", { id, breakpoinId })
return undefined
},
$appendDebugConsole(value: string): void {
console.log("Debug console:", value)
},
async $registerBreakpoints(breakpoints: any[]): Promise<void> {
console.log("Register breakpoints:", breakpoints)
},
async $unregisterBreakpoints(
breakpointIds: string[],
functionBreakpointIds: string[],
dataBreakpointIds: string[],
): Promise<void> {
console.log("Unregister breakpoints:", { breakpointIds, functionBreakpointIds, dataBreakpointIds })
},
$registerDebugVisualizer(extensionId: string, id: string): void {
console.log("Register debug visualizer:", { extensionId, id })
},
$unregisterDebugVisualizer(extensionId: string, id: string): void {
console.log("Unregister debug visualizer:", { extensionId, id })
},
$registerDebugVisualizerTree(treeId: string, canEdit: boolean): void {
console.log("Register debug visualizer tree:", { treeId, canEdit })
},
$unregisterDebugVisualizerTree(treeId: string): void {
console.log("Unregister debug visualizer tree:", treeId)
},
$registerCallHierarchyProvider(handle: number, supportsResolve: boolean): void {
console.log("Register call hierarchy provider:", { handle, supportsResolve })
},
dispose(): void {
console.log("Disposing MainThreadDebugService")
},
})
}
public setupRooCodeRequiredProtocols(): void {
if (!this.rpcProtocol) {
return
}
// MainThreadTextEditors
this.rpcProtocol.set(MainContext.MainThreadTextEditors, {
$tryShowTextDocument(resource: UriComponents, options: any): Promise<string | undefined> {
console.log("Try show text document:", { resource, options })
return Promise.resolve(undefined)
},
$tryShowEditor(id: string, position?: any): Promise<void> {
console.log("Try show editor:", { id, position })
return Promise.resolve()
},
$tryHideEditor(id: string): Promise<void> {
console.log("Try hide editor:", id)
return Promise.resolve()
},
$trySetSelections(id: string, selections: any[]): Promise<void> {
console.log("Try set selections:", { id, selections })
return Promise.resolve()
},
$tryRevealRange(id: string, range: any, revealType: any): Promise<void> {
console.log("Try reveal range:", { id, range, revealType })
return Promise.resolve()
},
$trySetOptions(id: string, options: any): Promise<void> {
console.log("Try set options:", { id, options })
return Promise.resolve()
},
$tryApplyEdits(id: string, modelVersionId: number, edits: any[], opts: any): Promise<boolean> {
console.log("Try apply edits:", { id, modelVersionId, edits, opts })
return Promise.resolve(true)
},
$registerTextEditorDecorationType(extensionId: ExtensionIdentifier, key: string, options: any): void {
console.log("Register text editor decoration type:", { extensionId, key, options })
},
$removeTextEditorDecorationType(key: string): void {
console.log("Remove text editor decoration type:", key)
},
$trySetDecorations(id: string, key: string, ranges: any[]): Promise<void> {
console.log("Try set decorations:", { id, key, ranges })
return Promise.resolve()
},
$trySetDecorationsFast(id: string, key: string, ranges: any[]): Promise<void> {
console.log("Try set decorations fast:", { id, key, ranges })
return Promise.resolve()
},
$tryInsertSnippet(id: string, snippet: any, location: any, options: any): Promise<boolean> {
console.log("Try insert snippet:", { id, snippet, location, options })
return Promise.resolve(true)
},
$getDiffInformation(id: string): Promise<any> {
console.log("Get diff information:", id)
return Promise.resolve(null)
},
dispose(): void {
console.log("Dispose MainThreadTextEditors")
},
})
// MainThreadStorage
this.rpcProtocol.set(MainContext.MainThreadStorage, {
$initializeExtensionStorage(shared: boolean, extensionId: string): Promise<string | undefined> {
console.log("Initialize extension storage:", { shared, extensionId })
return Promise.resolve(undefined)
},
$setValue(shared: boolean, extensionId: string, value: object): Promise<void> {
console.log("Set value:", { shared, extensionId, value })
return Promise.resolve()
},
$registerExtensionStorageKeysToSync(extension: any, keys: string[]): void {
console.log("Register extension storage keys to sync:", { extension, keys })
},
dispose(): void {
console.log("Dispose MainThreadStorage")
},
})
// MainThreadOutputService
this.rpcProtocol.set(MainContext.MainThreadOutputService, {
$register(
label: string,
file: UriComponents,
languageId: string | undefined,
extensionId: string,
): Promise<string> {
console.log("Register output channel:", { label, file, languageId, extensionId })
return Promise.resolve(`output-${extensionId}-${label}`)
},
$update(channelId: string, mode: any, till?: number): Promise<void> {
console.log("Update output channel:", { channelId, mode, till })
return Promise.resolve()
},
$reveal(channelId: string, preserveFocus: boolean): Promise<void> {
console.log("Reveal output channel:", { channelId, preserveFocus })
return Promise.resolve()
},
$close(channelId: string): Promise<void> {
console.log("Close output channel:", channelId)
return Promise.resolve()
},
$dispose(channelId: string): Promise<void> {
console.log("Dispose output channel:", channelId)
return Promise.resolve()
},
dispose(): void {
console.log("Dispose MainThreadOutputService")
},
})
// Create a single WebViewManager instance
const webViewManager = new WebViewManager(this.rpcProtocol)
// MainThreadWebviewViews
this.rpcProtocol.set(MainContext.MainThreadWebviewViews, webViewManager)
// MainThreadDocumentContentProviders
this.rpcProtocol.set(MainContext.MainThreadDocumentContentProviders, {
$registerTextContentProvider(handle: number, scheme: string): void {
console.log("Register text content provider:", { handle, scheme })
},
$unregisterTextContentProvider(handle: number): void {
console.log("Unregister text content provider:", handle)
},
$onVirtualDocumentChange(uri: UriComponents, value: string): Promise<void> {
console.log("Virtual document change:", { uri, value })
return Promise.resolve()
},
dispose(): void {
console.log("Dispose MainThreadDocumentContentProviders")
},
})
// MainThreadUrls
this.rpcProtocol.set(MainContext.MainThreadUrls, {
$registerUriHandler(
handle: number,
extensionId: ExtensionIdentifier,
extensionDisplayName: string,
): Promise<void> {
console.log("Register URI handler:", { handle, extensionId, extensionDisplayName })
return Promise.resolve()
},
$unregisterUriHandler(handle: number): Promise<void> {
console.log("Unregister URI handler:", handle)
return Promise.resolve()
},
$createAppUri(uri: UriComponents): Promise<UriComponents> {
console.log("Create app URI:", uri)
return Promise.resolve(uri)
},
dispose(): void {
console.log("Dispose MainThreadUrls")
},
})
// MainThreadWebviews
this.rpcProtocol.set(MainContext.MainThreadWebviews, webViewManager)
}
public getRPCProtocol(): IRPCProtocol | null {
return this.rpcProtocol
}
}

View file

@ -0,0 +1,159 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
ExtHostContext,
ExtHostWebviewViewsShape,
MainThreadWebviewViewsShape,
WebviewExtensionDescription as ExtHostWebviewExtensionDescription,
MainThreadWebviewsShape,
IWebviewContentOptions,
} from "../deps/vscode/vs/workbench/api/common/extHost.protocol.js"
import { IRPCProtocol } from "../deps/vscode/vs/workbench/services/extensions/common/proxyIdentifier.js"
import { WebviewContentOptions } from "../deps/vscode/vs/workbench/contrib/webview/browser/webview.js"
import { URI } from "../deps/vscode/vs/base/common/uri.js"
import { CancellationToken } from "../deps/vscode/vs/base/common/cancellation.js"
import { VSBuffer } from "../deps/vscode/vs/base/common/buffer.js"
/**
* A simplified webview implementation that only includes methods used by WebViewManager
*/
class SimpleWebview {
contentOptions: WebviewContentOptions = {}
setHtml(html: string): void {
console.log("[SimpleWebview] Set HTML:", html)
}
setTitle(title: string): void {
console.log("[SimpleWebview] Set title:", title)
}
postMessage(message: any, transfer?: readonly VSBuffer[]): Promise<boolean> {
console.log("[SimpleWebview] Post message:", message)
return Promise.resolve(true)
}
focus(): void {
console.log("[SimpleWebview] Focus")
}
dispose(): void {
console.log("[SimpleWebview] Dispose")
}
}
export class WebViewManager implements MainThreadWebviewViewsShape, MainThreadWebviewsShape {
private readonly _proxy: ExtHostWebviewViewsShape
private readonly _webviews = new Map<string, SimpleWebview>()
constructor(private readonly rpcProtocol: IRPCProtocol) {
this._proxy = this.rpcProtocol.getProxy(ExtHostContext.ExtHostWebviewViews)
}
// MainThreadWebviewViewsShape implementation
$registerWebviewViewProvider(
extension: ExtHostWebviewExtensionDescription,
viewType: string,
options: { retainContextWhenHidden?: boolean; serializeBuffersForPostMessage: boolean },
): void {
console.log("Register webview view provider:", { extension, viewType, options })
// Create a new webview instance
const webview = new SimpleWebview()
// Store the webview instance
this._webviews.set(viewType, webview)
// Generate a unique handle for this webview
const webviewHandle = `webview-${viewType}-${Date.now()}`
// Notify the extension host that the webview is ready
this._proxy.$resolveWebviewView(
webviewHandle,
viewType,
undefined, // title
undefined, // state
CancellationToken.None, // cancellation
)
}
$unregisterWebviewViewProvider(viewType: string): void {
console.log("Unregister webview view provider:", viewType)
// Remove the webview instance
const webview = this._webviews.get(viewType)
if (webview) {
webview.dispose()
this._webviews.delete(viewType)
}
}
$setWebviewViewTitle(handle: string, value: string | undefined): void {
console.log("Set webview view title:", { handle, value })
const webview = this._webviews.get(handle)
if (webview) {
webview.setTitle(value || "")
}
}
$setWebviewViewDescription(handle: string, value: string | undefined): void {
console.log("Set webview view description:", { handle, value })
}
$setWebviewViewBadge(handle: string, badge: any | undefined): void {
console.log("Set webview view badge:", { handle, badge })
}
$show(handle: string, preserveFocus: boolean): void {
console.log("Show webview view:", { handle, preserveFocus })
const webview = this._webviews.get(handle)
if (webview) {
webview.focus()
}
}
// MainThreadWebviewsShape implementation
$setHtml(handle: string, value: string): void {
console.log("Set webview HTML:", { handle, value })
const webview = this._webviews.get(handle)
if (webview) {
webview.setHtml(value)
}
}
$setOptions(handle: string, options: IWebviewContentOptions): void {
console.log("Set webview panel options:", { handle, options })
const webview = this._webviews.get(handle)
if (webview) {
// Convert IWebviewContentOptions to WebviewContentOptions
const contentOptions: WebviewContentOptions = {
allowScripts: options.enableScripts,
allowForms: options.enableForms,
localResourceRoots: options.localResourceRoots?.map((uri) => URI.revive(uri)),
portMapping: options.portMapping,
}
webview.contentOptions = contentOptions
}
}
$postMessage(handle: string, value: string, ...buffers: VSBuffer[]): Promise<boolean> {
console.log("Post message to webview:", { handle, value, buffers })
const webview = this._webviews.get(handle)
if (webview) {
return webview.postMessage(value, buffers)
}
return Promise.resolve(false)
}
dispose(): void {
console.log("Dispose WebViewManager")
// Dispose all webviews
for (const webview of this._webviews.values()) {
webview.dispose()
}
this._webviews.clear()
}
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"moduleDetection": "legacy",
"experimentalDecorators": true,
"noImplicitReturns": false,
"noImplicitOverride": false,
"noUnusedLocals": false,
"noUncheckedSideEffectImports": false,
"allowUnreachableCode": false,
"strict": false,
"exactOptionalPropertyTypes": false,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"target": "es2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker.ImportScripts"],
"allowSyntheticDefaultImports": true
}
}

View file

@ -0,0 +1,51 @@
{
// This is the configuration for plugins/base independent project, compiled separately from the outer project
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "NodeNext",
"esModuleInterop": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"inlineSources": true,
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": false,
"outDir": "./dist",
"skipLibCheck": true, // Skip library file checking to avoid type conflicts
"declaration": true,
"declarationMap": true,
"types": [
"@types/mocha",
"@types/semver",
"@types/sinon",
"@types/trusted-types",
"@types/winreg",
"@types/wicg-file-system-access"
]
},
"include": [
"src/**/*.ts", // Include all TS files under src
"src/**/*.js",
"electron.d.ts",
"deps/vscode/typings",
"deps/vscode/vs/workbench/api/node",
"deps/vscode/vs/workbench/contrib/debug/common/debugProtocol.d.ts",
"deps/vscode/vscode-dts/vscode.d.ts",
"deps/vscode/vscode-dts/vscode.proposed.*.d.ts",
"deps/vscode/vs/base/common/marked",
"deps/vscode/vs/base/common/semver"
],
"exclude": [
"node_modules",
".vscode-test",
"webview-ui",
"deps/vscode/**/test/**/*.ts",
"deps/vscode/**/test/**/*.js",
"deps/vscode/**/fixtures/**/*.js",
"deps/vscode/**/fixtures/**/*.ts",
"node_modules/@types/vscode",
"node_modules/@types/electron",
"dist/**/*"
]
}

View file

@ -0,0 +1,37 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"types": ["@webgpu/types", "trusted-types", "wicg-file-system-access"],
"paths": {},
"module": "amd",
"moduleResolution": "node",
"removeComments": false,
"preserveConstEnums": true,
"target": "ES2022",
"sourceMap": false,
"declaration": true
},
"include": [
"typings/css.d.ts",
"typings/thenable.d.ts",
"typings/vscode-globals-product.d.ts",
"typings/vscode-globals-nls.d.ts",
"typings/editContext.d.ts",
"vs/monaco.d.ts",
"vs/editor/*",
"vs/base/common/*",
"vs/base/browser/*",
"vs/platform/*/common/*",
"vs/platform/*/browser/*"
],
"exclude": [
"node_modules/*",
"vs/platform/files/browser/htmlFileSystemProvider.ts",
"vs/platform/files/browser/webFileSystemAccess.ts",
"vs/platform/telemetry/*",
"vs/platform/assignment/*",
"vs/platform/terminal/*",
"vs/platform/externalTerminal/*"
]
}

View file

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true,
"plugins": [
{
"name": "tsec",
"exemptionConfig": "./tsec.exemptions.json"
}
]
},
"exclude": ["./vs/workbench/contrib/webview/browser/pre/service-worker.js", "*/test/*", "**/*.test.ts"]
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"noEmit": true,
"module": "None",
"experimentalDecorators": false,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"allowUnreachableCode": false,
"strict": true,
"exactOptionalPropertyTypes": false,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"types": [],
"lib": ["ES2022"]
},
"include": ["vscode-dts/vscode.d.ts"]
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.vscode-dts.json",
"include": ["vscode-dts/vscode.d.ts", "vscode-dts/vscode.proposed.*.d.ts"]
}

View file

@ -0,0 +1,34 @@
{
"ban-document-execcommand": [
"vs/workbench/contrib/codeEditor/electron-sandbox/inputClipboardActions.ts",
"vs/editor/contrib/clipboard/browser/clipboard.ts"
],
"ban-eval-calls": ["vs/workbench/api/worker/extHostExtensionService.ts"],
"ban-function-calls": [
"vs/workbench/api/worker/extHostExtensionService.ts",
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts",
"vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts"
],
"ban-trustedtypes-createpolicy": [
"bootstrap-window.ts",
"vs/amdX.ts",
"vs/base/browser/trustedTypes.ts",
"vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts"
],
"ban-worker-calls": [
"vs/base/browser/webWorkerFactory.ts",
"vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts"
],
"ban-worker-importscripts": [
"vs/amdX.ts",
"vs/workbench/services/extensions/worker/polyfillNestedWorker.ts",
"vs/workbench/api/worker/extensionHostWorker.ts"
],
"ban-domparser-parsefromstring": [
"vs/base/browser/markdownRenderer.ts",
"vs/base/test/browser/markdownRenderer.test.ts"
],
"ban-element-setattribute": ["**/*.ts"],
"ban-element-insertadjacenthtml": ["**/*.ts"],
"ban-script-content-assignments": ["bootstrap-window.ts"]
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
// tsup.config.ts
import { defineConfig } from "tsup"
import { dependencies } from "./package.json"
export default defineConfig({
entry: {
"src/main": "src/main.ts",
"src/extension": "src/extension.ts",
}, // Build both server and ext host
format: ["esm"], // Output format, e.g., ES Module and CommonJS
minify: true, // Minify code
clean: true, // Clean output directory
splitting: false,
platform: "node", // Target platform, e.g., Node.js
target: "node18", // Target environment, e.g., latest ECMAScript standard
skipNodeModulesBundle: false, // Don't bundle dependencies in node_modules
// noExternal: Object.keys(dependencies),
// external:[/^@vscode\/.*$/], // Don't bundle vscode-related dependencies
dts: false, // Don't generate type declaration files, as we usually handle type declarations separately
})

44
jetbrains/host/turbo.json Normal file
View file

@ -0,0 +1,44 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"deps:check": {
"cache": false,
"env": ["DEVENV"]
},
"deps:clean": {
"cache": false
},
"deps:patch": {
"cache": false,
"dependsOn": ["deps:check", "deps:clean"],
"env": ["DEVENV"]
},
"deps:copy": {
"cache": false,
"dependsOn": ["deps:check", "deps:patch"],
"env": ["DEVENV"]
},
"prop:deps": {
"cache": false
},
"bundle:build": {
"outputs": ["dist/**"],
"inputs": ["package.json", "tsconfig.json", "src/**", "deps/vscode/**"],
"dependsOn": ["clean", "deps:clean", "deps:patch", "deps:copy"]
},
"bundle:package": {
"cache": false,
"dependsOn": ["bundle:build"]
},
"bundle": {
"cache": false,
"dependsOn": ["bundle:package"]
},
"build": {
"outputs": ["dist/**"],
"inputs": ["package.json", "tsconfig.json", "src/**", "deps/vscode/**"],
"dependsOn": ["clean", "deps:clean", "deps:patch", "deps:copy"]
}
}
}

18
jetbrains/host/typings/base-common.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Declare types that we probe for to implement util and/or polyfill functions
declare global {
interface IdleDeadline {
readonly didTimeout: boolean
timeRemaining(): number
}
function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number
function cancelIdleCallback(handle: number): void
}
export {}

81
jetbrains/host/typings/crypto.d.ts vendored Normal file
View file

@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// NOTE that this is a partial copy from lib.dom.d.ts which is NEEDED because these utils are used in the /common/
// layer which has no dependency on the DOM/browser-context. However, `crypto` is available as global in all browsers and
// in nodejs. Therefore it's OK to spell out its typings here
declare global {
/**
* This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto).
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)
*/
interface SubtleCrypto {
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */
// decrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */
// deriveBits(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, length?: number | null): Promise<ArrayBuffer>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */
// deriveKey(algorithm: AlgorithmIdentifier | EcdhKeyDeriveParams | HkdfParams | Pbkdf2Params, baseKey: CryptoKey, derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams | HkdfParams | Pbkdf2Params, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */
digest(algorithm: { name: string } | string, data: ArrayBufferView | ArrayBuffer): Promise<ArrayBuffer>
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */
// encrypt(algorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */
// exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>;
// exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>;
// exportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */
// generateKey(algorithm: "Ed25519", extractable: boolean, keyUsages: ReadonlyArray<"sign" | "verify">): Promise<CryptoKeyPair>;
// generateKey(algorithm: RsaHashedKeyGenParams | EcKeyGenParams, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKeyPair>;
// generateKey(algorithm: AesKeyGenParams | HmacKeyGenParams | Pbkdf2Params, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKey>;
// generateKey(algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair | CryptoKey>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */
// importKey(format: "jwk", keyData: JsonWebKey, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: ReadonlyArray<KeyUsage>): Promise<CryptoKey>;
// importKey(format: Exclude<KeyFormat, "jwk">, keyData: BufferSource, algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */
// sign(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */
// unwrapKey(format: KeyFormat, wrappedKey: BufferSource, unwrappingKey: CryptoKey, unwrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams, unwrappedKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */
// verify(algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams, key: CryptoKey, signature: BufferSource, data: BufferSource): Promise<boolean>;
// /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */
// wrapKey(format: KeyFormat, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: AlgorithmIdentifier | RsaOaepParams | AesCtrParams | AesCbcParams | AesGcmParams): Promise<ArrayBuffer>;
}
/**
* Basic cryptography features available in the current context. It allows access to a cryptographically strong random number generator and to cryptographic primitives.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto)
*/
interface Crypto {
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)
*/
readonly subtle: SubtleCrypto
/**
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)
*/
getRandomValues<T extends ArrayBufferView | null>(array: T): T
/**
* Available only in secure contexts.
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
*/
randomUUID(): `${string}-${string}-${string}-${string}-${string}`
}
var Crypto: {
prototype: Crypto
new (): Crypto
}
var crypto: Crypto
}
export {}

8
jetbrains/host/typings/css.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Recognize all CSS files as valid module imports
declare module "vs/css!*" {}
declare module "*.css" {}

138
jetbrains/host/typings/editContext.d.ts vendored Normal file
View file

@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
type DOMString = string
interface EditContext extends EventTarget {
updateText(rangeStart: number, rangeEnd: number, text: DOMString): void
updateSelection(start: number, end: number): void
updateControlBounds(controlBounds: DOMRect): void
updateSelectionBounds(selectionBounds: DOMRect): void
updateCharacterBounds(rangeStart: number, characterBounds: DOMRect[]): void
attachedElements(): HTMLElement[]
get text(): DOMString
get selectionStart(): number
get selectionEnd(): number
get characterBoundsRangeStart(): number
characterBounds(): DOMRect[]
get ontextupdate(): EventHandler<TextUpdateEvent> | null
set ontextupdate(value: EventHandler | null)
get ontextformatupdate(): EventHandler | null
set ontextformatupdate(value: EventHandler | null)
get oncharacterboundsupdate(): EventHandler | null
set oncharacterboundsupdate(value: EventHandler | null)
get oncompositionstart(): EventHandler | null
set oncompositionstart(value: EventHandler | null)
get oncompositionend(): EventHandler | null
set oncompositionend(value: EventHandler | null)
addEventListener<K extends keyof EditContextEventHandlersEventMap>(
type: K,
listener: (this: GlobalEventHandlers, ev: EditContextEventHandlersEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): void
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void
removeEventListener<K extends keyof EditContextEventHandlersEventMap>(
type: K,
listener: (this: GlobalEventHandlers, ev: EditContextEventHandlersEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void
}
interface EditContextInit {
text: DOMString
selectionStart: number
selectionEnd: number
}
interface EditContextEventHandlersEventMap {
textupdate: TextUpdateEvent
textformatupdate: TextFormatUpdateEvent
characterboundsupdate: CharacterBoundsUpdateEvent
compositionstart: Event
compositionend: Event
}
type EventHandler<TEvent extends Event = Event> = (event: TEvent) => void
declare class TextUpdateEvent extends Event {
constructor(type: DOMString, options?: TextUpdateEventInit)
readonly updateRangeStart: number
readonly updateRangeEnd: number
readonly text: DOMString
readonly selectionStart: number
readonly selectionEnd: number
}
interface TextUpdateEventInit extends EventInit {
updateRangeStart: number
updateRangeEnd: number
text: DOMString
selectionStart: number
selectionEnd: number
compositionStart: number
compositionEnd: number
}
interface TextFormat {
new (options?: TextFormatInit): TextFormat
readonly rangeStart: number
readonly rangeEnd: number
readonly underlineStyle: UnderlineStyle
readonly underlineThickness: UnderlineThickness
}
interface TextFormatInit {
rangeStart: number
rangeEnd: number
underlineStyle: UnderlineStyle
underlineThickness: UnderlineThickness
}
type UnderlineStyle = "none" | "solid" | "dotted" | "dashed" | "wavy"
type UnderlineThickness = "none" | "thin" | "thick"
interface TextFormatUpdateEvent extends Event {
new (type: DOMString, options?: TextFormatUpdateEventInit): TextFormatUpdateEvent
getTextFormats(): TextFormat[]
}
interface TextFormatUpdateEventInit extends EventInit {
textFormats: TextFormat[]
}
interface CharacterBoundsUpdateEvent extends Event {
new (type: DOMString, options?: CharacterBoundsUpdateEventInit): CharacterBoundsUpdateEvent
readonly rangeStart: number
readonly rangeEnd: number
}
interface CharacterBoundsUpdateEventInit extends EventInit {
rangeStart: number
rangeEnd: number
}
interface HTMLElement {
editContext?: EditContext
}

12
jetbrains/host/typings/thenable.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise library is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommend the use of native promises which are available in VS Code.
*/
interface Thenable<T> extends PromiseLike<T> {}

View file

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// AMD2ESM migration relevant
/**
* NLS Globals: these need to be defined in all contexts that make
* use of our `nls.localize` and `nls.localize2` functions. This includes:
* - Electron main process
* - Electron window (renderer) process
* - Utility Process
* - Node.js
* - Browser
* - Web worker
*
* That is because during build time we strip out all english strings from
* the resulting JS code and replace it with a <number> that is then looked
* up from the `_VSCODE_NLS_MESSAGES` array.
*/
declare global {
/**
* All NLS messages produced by `localize` and `localize2` calls
* under `src/vs` translated to the language as indicated by
* `_VSCODE_NLS_LANGUAGE`.
*
* Instead of accessing this global variable directly, use function getNLSMessages.
*/
var _VSCODE_NLS_MESSAGES: string[]
/**
* The actual language of the NLS messages (e.g. 'en', de' or 'pt-br').
*
* Instead of accessing this global variable directly, use function getNLSLanguage.
*/
var _VSCODE_NLS_LANGUAGE: string | undefined
}
// fake export to make global work
export {}

View file

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// AMD2ESM migration relevant
declare global {
/**
* Holds the file root for resources.
*/
var _VSCODE_FILE_ROOT: string
/**
* CSS loader that's available during development time.
* DO NOT call directly, instead just import css modules, like `import 'some.css'`
*/
var _VSCODE_CSS_LOAD: (module: string) => void
/**
* @deprecated You MUST use `IProductService` whenever possible.
*/
var _VSCODE_PRODUCT_JSON: Record<string, any>
/**
* @deprecated You MUST use `IProductService` whenever possible.
*/
var _VSCODE_PACKAGE_JSON: Record<string, any>
}
// fake export to make global work
export {}

View file

@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// AMD2ESM migration relevant
declare global {
var _VSCODE_WEB_PACKAGE_TTP:
| Pick<
TrustedTypePolicy<{
createScriptURL(value: string): string
}>,
"name" | "createScriptURL"
>
| undefined
}
// fake export to make global work
export {}

50
jetbrains/plugin/.gitignore vendored Normal file
View file

@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
src/main/resources/ai/roocode/jetbrains/plugin/config/plugin.properties
### Deps ###
prodDep.txt
plugins

View file

@ -0,0 +1,31 @@
<!--
SPDX-FileCopyrightText: 2025 Weibo, Inc.
SPDX-License-Identifier: Apache-2.0
-->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<log_file alias="idea.log" path="$PROJECT_DIR$/build/idea-sandbox/system/log/idea.log" />
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/jetbrains/plugin" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="runIde" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,255 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: APACHE2.0
// SPDX-License-Identifier: Apache-2.0
// Convenient for reading variables from gradle.properties
fun properties(key: String) = providers.gradleProperty(key)
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.8.10"
id("org.jetbrains.intellij") version "1.17.4"
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
id("io.gitlab.arturbosch.detekt") version "1.23.4"
}
apply("genPlatform.gradle")
// ------------------------------------------------------------
// The 'debugMode' setting controls how plugin resources are prepared during the build process.
// It supports the following three modes:
//
// 1. "idea" — Local development mode (used for debugging VSCode plugin integration)
// - Copies theme resources from src/main/resources/themes to:
// ../resources/<vscodePlugin>/src/integrations/theme/default-themes/
// - Automatically creates a .env file, which the Extension Host (Node.js side) reads at runtime.
// - Enables the VSCode plugin to load resources from this directory for integration testing.
// - Typically used when running IntelliJ with an Extension Host for live debugging and hot-reloading.
//
// 2. "release" — Production build mode (used to generate deployment artifacts)
// - Requires platform.zip to exist, which can be retrieved via git-lfs or generated with genPlatform.gradle.
// - This file includes the full runtime environment for VSCode plugins (e.g., node_modules, platform.txt).
// - The zip is extracted to build/platform/, and its node_modules take precedence over other dependencies.
// - Copies compiled host outputs (dist, package.json, node_modules) and plugin resources.
// - The result is a fully self-contained package ready for deployment across platforms.
//
// 3. "none" (default) — Lightweight mode (used for testing and CI)
// - Does not rely on platform.zip or prepare VSCode runtime resources.
// - Only copies the plugin's core assets such as themes.
// - Useful for early-stage development, static analysis, unit tests, and continuous integration pipelines.
//
// How to configure:
// - Set via gradle argument: -PdebugMode=idea / release / none
// Example: ./gradlew prepareSandbox -PdebugMode=idea
// - Defaults to "none" if not explicitly set.
// ------------------------------------------------------------
ext {
set("debugMode", project.findProperty("debugMode") ?: "none")
set("debugResource", project.projectDir.resolve("../resources").absolutePath)
set("vscodePlugin", project.findProperty("vscodePlugin") ?: "roo-code")
}
project.afterEvaluate {
tasks.findByName(":prepareSandbox")?.inputs?.properties?.put("build_mode", ext.get("debugMode"))
}
fun Sync.prepareSandbox() {
// Set duplicate strategy to include files, with later sources taking precedence
duplicatesStrategy = DuplicatesStrategy.INCLUDE
if (ext.get("debugMode") == "idea") {
from("${project.projectDir.absolutePath}/src/main/resources/themes/") {
into("${ext.get("debugResource")}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/")
}
doLast {
val vscodePluginDir = File("${ext.get("debugResource")}/${ext.get("vscodePlugin")}")
vscodePluginDir.mkdirs()
File(vscodePluginDir, ".env").createNewFile()
}
} else {
val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}")
if (!vscodePluginDir.exists()) {
throw IllegalStateException("missing plugin dir")
}
val list = mutableListOf<String>()
val depfile = File("prodDep.txt")
if (!depfile.exists()) {
throw IllegalStateException("missing prodDep.txt")
}
depfile.readLines().let {
it.forEach { line ->
list.add(line.substringAfterLast("node_modules/") + "/**")
}
}
from("../host/dist") { into("${intellij.pluginName.get()}/runtime/") }
from("../host/package.json") { into("${intellij.pluginName.get()}/runtime/") }
// First copy host node_modules
from("../resources/node_modules") {
into("${intellij.pluginName.get()}/node_modules/")
list.forEach {
include(it)
}
}
from("${vscodePluginDir.path}/extension") { into("${intellij.pluginName.get()}/${ext.get("vscodePlugin")}") }
from("src/main/resources/themes/") { into("${intellij.pluginName.get()}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/") }
// The platform.zip file required for release mode is associated with the code in ../base/vscode, currently using version 1.100.0. If upgrading this code later
// Need to modify the vscodeVersion value in gradle.properties, then execute the task named genPlatform, which will generate a new platform.zip file for submission
// To support new architectures, modify according to the logic in genPlatform.gradle script
if (ext.get("debugMode") == "release") {
// Check if platform.zip file exists and is larger than 1MB, otherwise throw exception
val platformZip = File("platform.zip")
if (platformZip.exists() && platformZip.length() >= 1024 * 1024) {
// Extract platform.zip to the platform subdirectory under the project build directory
val platformDir = File("${layout.buildDirectory.get().asFile}/platform")
platformDir.mkdirs()
copy {
from(zipTree(platformZip))
into(platformDir)
}
} else {
throw IllegalStateException("platform.zip file does not exist or is smaller than 1MB. This file is supported through git lfs and needs to be obtained through git lfs")
}
from(File(layout.buildDirectory.get().asFile, "platform/platform.txt")) { into("${intellij.pluginName.get()}/") }
// Copy platform node_modules last to ensure it takes precedence over host node_modules
from(File(layout.buildDirectory.get().asFile, "platform/node_modules")) { into("${intellij.pluginName.get()}/node_modules") }
}
doLast {
File("${destinationDir}/${intellij.pluginName.get()}/${ext.get("vscodePlugin")}/.env").createNewFile()
}
}
}
group = properties("pluginGroup").get()
version = properties("pluginVersion").get()
repositories {
mavenCentral()
}
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("junit:junit:4.13.2")
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4")
}
// Configure Java toolchain to force Java 17
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
// Configure Gradle IntelliJ Plugin
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
version = properties("platformVersion")
type = properties("platformType")
plugins.set(
listOf(
"com.intellij.java",
// Add JCEF support
"org.jetbrains.plugins.terminal"
)
)
}
tasks {
// Create task for generating configuration files
register("generateConfigProperties") {
description = "Generate properties file containing plugin configuration"
doLast {
val configDir = File("$projectDir/src/main/resources/ai/roocode/jetbrains/plugin/config")
configDir.mkdirs()
val configFile = File(configDir, "plugin.properties")
configFile.writeText("debug.mode=${ext.get("debugMode")}")
configFile.appendText("\n")
configFile.appendText("debug.resource=${ext.get("debugResource")}")
println("Configuration file generated: ${configFile.absolutePath}")
}
}
prepareSandbox {
prepareSandbox()
}
// Generate configuration file before compilation
withType<JavaCompile> {
dependsOn("generateConfigProperties")
}
// Set the JVM compatibility versions
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependsOn("generateConfigProperties")
kotlinOptions {
jvmTarget = "17"
}
}
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
patchPluginXml {
version.set(properties("pluginVersion"))
sinceBuild.set(properties("pluginSinceBuild"))
untilBuild.set("")
}
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
}
}
// Configure ktlint
ktlint {
version.set("0.50.0")
debug.set(false)
verbose.set(true)
android.set(false)
outputToConsole.set(true)
outputColorName.set("RED")
ignoreFailures.set(true)
enableExperimentalRules.set(false)
filter {
exclude("**/generated/**")
include("**/kotlin/**")
}
}
// Configure detekt
detekt {
toolVersion = "1.23.4"
config.setFrom(file("detekt.yml"))
buildUponDefaultConfig = true
allRules = false
reports {
html.required.set(true)
xml.required.set(true)
txt.required.set(true)
sarif.required.set(true)
md.required.set(true)
}
}

729
jetbrains/plugin/detekt.yml Normal file
View file

@ -0,0 +1,729 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0
# Detekt configuration file for Roo Code JetBrains Plugin
# https://detekt.github.io/detekt/configurations.html
build:
maxIssues: 5000
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
warningsAsErrors: false
checkExhaustiveness: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
- 'DetektProgressListener'
# - 'KtFileCountProcessor'
# - 'PackageCountProcessor'
# - 'ClassCountProcessor'
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ProjectComplexityProcessor'
# - 'ProjectCognitiveComplexityProcessor'
# - 'ProjectLLOCProcessor'
# - 'ProjectCLOCProcessor'
# - 'ProjectLOCProcessor'
# - 'ProjectSLOCProcessor'
# - 'LicenseHeaderLoaderExtension'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
- 'FindingsReport'
- 'FileBasedFindingsReport'
- 'LiteFindingsReport'
output-reports:
active: true
exclude:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
# - 'MdOutputReport'
# - 'SarifOutputReport'
comments:
active: true
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
KDocReferencesNonPublicProperty:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
OutdatedDocumentation:
active: false
matchTypeParameters: true
matchDeclarationsOrder: true
allowParamOnConstructorProperties: false
UndocumentedPublicClass:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UndocumentedPublicProperty:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
complexity:
active: true
CognitiveComplexMethod:
active: false
threshold: 15
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
CyclomaticComplexMethod:
active: true
threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions:
- 'also'
- 'apply'
- 'forEach'
- 'isNotNull'
- 'ifNull'
- 'let'
- 'run'
- 'use'
- 'with'
LabeledExpression:
active: false
ignoredLabels: []
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 60
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: false
ignoreDataClasses: true
ignoreAnnotatedParameter: []
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
threshold: 4
NestedScopeFunctions:
active: false
threshold: 1
functions:
- 'kotlin.apply'
- 'kotlin.run'
- 'kotlin.with'
- 'kotlin.let'
- 'kotlin.also'
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: true
dispatcherNames:
- 'IO'
- 'Default'
- 'Unconfined'
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptionNames:
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'NullPointerException'
- 'IndexOutOfBoundsException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
naming:
active: true
BooleanPropertyNaming:
active: false
allowedPattern: '^(is|has|are)'
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
forbiddenName: []
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
functionPattern: '[a-z][a-zA-Z0-9]*'
excludeClassPattern: '$^'
ignoreAnnotated: ['Composable']
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
InvalidPackageDeclaration:
active: true
rootPackage: ''
requireRootInDeclaration: false
LambdaParameterNaming:
active: false
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
active: false
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
maximumVariableNameLength: 64
VariableMinLength:
active: false
minimumVariableNameLength: 1
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
threshold: 3
ForEachOnRange:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
SpreadOperator:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
active: false
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: true
mutableTypes:
- 'kotlin.collections.MutableList'
- 'kotlin.collections.MutableMap'
- 'kotlin.collections.MutableSet'
- 'java.util.ArrayList'
- 'java.util.LinkedHashSet'
- 'java.util.HashSet'
- 'java.util.LinkedHashMap'
- 'java.util.HashMap'
ElseCaseInsteadOfExhaustiveWhen:
active: false
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
restrictToConfig: true
returnValueAnnotations:
- '*.CheckResult'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- '*.CanIgnoreReturnValue'
returnValueTypes:
- 'kotlin.sequences.Sequence'
- 'kotlinx.coroutines.flow.Flow'
- 'java.util.stream.Stream'
ignoreFunctionCall: []
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: true
MissingPackageDeclaration:
active: false
excludes: ['**/*.kts']
NullCheckOnMutableProperty:
active: false
NullableToStringCall:
active: false
PropertyUsedBeforeDeclaration:
active: false
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnsafeCast:
active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
AlsoCouldBeApply:
active: false
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
includeElvis: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix:
- 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
comments:
- value: 'FIXME:'
reason: 'Forbidden FIXME todo marker'
- value: 'STOPSHIP:'
reason: 'Forbidden STOPSHIP todo marker'
- value: 'TODO:'
reason: 'Forbidden TODO todo marker'
allowedPatterns: ''
ForbiddenImport:
active: false
imports: []
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods:
- 'kotlin.io.print'
- 'kotlin.io.println'
ForbiddenPublicDataClass:
active: false
excludes: ['**']
ignorePackages:
- '*.internal'
- '*.internal.*'
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: []
LibraryCodeMustSpecifyReturnType:
active: false
excludes: ['**']
LibraryEntitiesShouldNotBePublic:
active: false
excludes: ['**']
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
BracesOnIfStatements:
active: false
singleLine: 'always'
multiLine: 'always'
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
NullableBooleanCheck:
active: false
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 2
excludedFunctions:
- 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: true
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
TrimMultilineRawString:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableLength: 4
allowNonStandardGrouping: false
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
active: false
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedParameter:
active: true
allowedNames: 'ignored|expected'
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: ''
UnusedPrivateProperty:
active: true
allowedNames: '_|ignored|expected|serialVersionUID'
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: true
UseOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreLateinitVar: false
WildcardImport:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludeImports:
- 'java.util.*'

View file

@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import java.nio.file.*
import java.security.MessageDigest
tasks.register('genPlatform', Zip) {
download(project)
from(new File(project.buildDir, "genPlatform/gen"))
into("")
destinationDirectory = project.projectDir
archiveFileName = "platform.zip"
}
def download(Project project){
def version = project.findProperty("vscodeVersion")
String windows_x64 = "https://update.code.visualstudio.com/${version}/win32-x64-archive/stable"
String mac_x64 = "https://update.code.visualstudio.com/${version}/darwin/stable"
String mac_arm64 = "https://update.code.visualstudio.com/${version}/darwin-arm64/stable"
String linux_x64 = "https://update.code.visualstudio.com/${version}/linux-x64/stable"
// To support other platforms, need to synchronously modify the initPlatfromFiles method in WecoderPlugin
def list = [] // node_module directories for multiple platforms
def projectBuild = new File(project.buildDir,"genPlatform")
projectBuild.mkdirs();
println "Downloading Windows platform files"
def windowsZipFile = new File(projectBuild,"windows-x64.zip")
if (!windowsZipFile.exists()) {
windowsZipFile << new URL(windows_x64).openStream()
} else {
println "Windows platform file already exists, skipping download"
}
def windowsDir = new File(projectBuild, "windows-x64")
copy {
from(zipTree(new File(projectBuild, "windows-x64.zip")))
into(windowsDir)
}
new File(windowsDir, "resources/app/node_modules").renameTo( new File(windowsDir, "resources/app/windows-x64"))
list << new File(windowsDir, "resources/app/windows-x64")
println "Downloading Mac x64 platform files"
def macX64ZipFile = new File(projectBuild,"darwin-x64.zip")
if (!macX64ZipFile.exists()) {
macX64ZipFile << new URL(mac_x64).openStream()
} else {
println "Mac x64 platform file already exists, skipping download"
}
def macX64Dir = new File(projectBuild, "darwin-x64")
copy {
from(zipTree(new File(projectBuild, "darwin-x64.zip")))
into(macX64Dir)
}
new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/node_modules").renameTo(new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-x64"))
list << new File(macX64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-x64")
println "Downloading Mac arm64 platform files"
def macArm64ZipFile = new File(projectBuild,"darwin-arm64.zip")
if (!macArm64ZipFile.exists()) {
macArm64ZipFile << new URL(mac_arm64).openStream()
} else {
println "Mac arm64 platform file already exists, skipping download"
}
def macArm64Dir = new File(projectBuild, "darwin-arm64")
copy {
from(zipTree(new File(projectBuild, "darwin-arm64.zip")))
into(macArm64Dir)
}
new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/node_modules").renameTo(new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-arm64"))
list << new File(macArm64Dir, "Visual Studio Code.app/Contents/Resources/app/darwin-arm64")
println "Downloading Linux x64 platform files"
def linuxZipFile = new File(projectBuild,"linux-x64.zip")
if (!linuxZipFile.exists()) {
linuxZipFile << new URL(linux_x64).openStream()
} else {
println "Linux x64 platform file already exists, skipping download"
}
def linuxDir = new File(projectBuild, "linux-x64")
copy {
from(tarTree(resources.gzip(projectBuild.toPath().resolve("linux-x64.zip"))))
into(linuxDir)
}
new File(linuxDir, "VSCode-linux-x64/resources/app/node_modules").renameTo(new File(linuxDir, "VSCode-linux-x64/resources/app/linux-x64"))
list << new File(linuxDir, "VSCode-linux-x64/resources/app/linux-x64")
def targetDir = new File(projectBuild, "gen/node_modules")
def txtFile = new File(projectBuild, "gen/platform.txt")
mergeDirectories(list, targetDir,txtFile)
def zipFile = new File(project.projectDir,"platform.zip")
if(zipFile.exists()) {
zipFile.delete()
}
}
def mergeDirectories(List<File> dirs, File targetDir, File outputFile = null) {
def outputContent = new StringBuilder()
if (!targetDir.exists()) {
targetDir.mkdirs()
}
// Collect all file paths (relative paths), ignore .DS_Store
def allFiles = []
dirs.each { dir ->
if (dir.exists()) {
dir.eachFileRecurse { file ->
if (file.isFile() && file.name != ".DS_Store") { // Ignore .DS_Store
def relativePath = dir.toPath().relativize(file.toPath()).toString()
allFiles << [dir: dir, file: file, relativePath: relativePath]
}
}
}
}
// Group by relative path
def groupedFiles = allFiles.groupBy { it.relativePath }
groupedFiles.each { relativePath, entries ->
def targetFile = new File(targetDir, relativePath)
targetFile.parentFile.mkdirs()
if (entries.size() == 1) {
// Unique file, copy directly
Files.copy(entries[0].file.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} else {
// Check if all file contents are the same
def uniqueHashes = entries.collect { entry ->
def file = entry.file
def digest = MessageDigest.getInstance("SHA-256")
file.withInputStream { is ->
byte[] buffer = new byte[8192]
int read
while ((read = is.read(buffer)) != -1) {
digest.update(buffer, 0, read)
}
}
def hash = digest.digest().encodeHex().toString()
[hash: hash, file: file, dir: entry.dir]
}.groupBy { it.hash }
if (uniqueHashes.size() == 1) {
// Same content, keep one copy
Files.copy(entries[0].file.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} else {
// Different content, add directory name as suffix
if (outputFile) {
outputContent.append("$relativePath\n")
} else {
println "$relativePath"
}
uniqueHashes.each { hash, files ->
def sourceFile = files[0].file
def dirName = files[0].dir.name
def newName = targetFile.name + dirName
def conflictFile = new File(targetFile.parentFile, newName)
Files.copy(sourceFile.toPath(), conflictFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
}
}
if (outputFile && outputContent.length() > 0) {
outputFile.parentFile.mkdirs()
outputFile.text = outputContent.toString()
}
}

View file

@ -0,0 +1,11 @@
# Plugin basic information
pluginGroup=ai.roocode.jetbrains
pluginVersion=4.88.0
# Platform basic information
platformVersion=2024.1.4
platformType=IC
pluginSinceBuild=241
# Other project configurations can be added here
vscodeVersion=1.100.0

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Wed Aug 20 11:09:31 ART 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
jetbrains/plugin/gradlew vendored Executable file
View file

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
jetbrains/plugin/gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,31 @@
{
"name": "@roo-code/jetbrains-plugin",
"license": "Apache-2.0",
"type": "module",
"devDependencies": {
"del-cli": "^5.1.0",
"cpy-cli": "^5.0.0",
"mkdirp": "^3.0.1"
},
"scripts": {
"clean": "./gradlew clean",
"build": "./gradlew buildPlugin -PdebugMode=idea",
"run": "./gradlew runIde -PdebugMode=idea",
"run:bundle": "./gradlew runIde -PdebugMode=release",
"bundle": "./gradlew buildPlugin -PdebugMode=release",
"bundle:name": "node scripts/get_bundle_name.js",
"clean:roocode": "npx del-cli ./plugins/roo-code --force && npx mkdirp ./plugins/roo-code",
"copy:roocode": "npx cpy '../../bin-unpacked/extension/**' './plugins/roo-code/extension' --parents",
"clean:resource-roocode": "npx del-cli ../resources/roo-code --force",
"copy:resource-roocode": "npx cpy '../../bin-unpacked/extension/**' '../resources/roo-code' --parents",
"clean:resource-host": "npx del-cli ../resources/runtime --force",
"copy:resource-host": "npx cpy '../host/dist/**' '../resources/runtime' --parents",
"clean:resource-logs": "npx del-cli ../resources/logs --force",
"copy:resource-logs": "npx mkdirp ../resources/logs",
"clean:resource-nodemodules": "npx del-cli ../resources/node_modules --force && npx del-cli ../resources/package.json --force",
"copy:resource-nodemodules": "cp ../host/package.json ../resources/package.json && npm install --prefix ../resources",
"propDep": "npx del-cli ./propDep.txt --force && npm ls --omit=dev --all --parseable --prefix ../resources > ./prodDep.txt",
"sync:version": "node scripts/sync_version.js",
"sync:changelog": "node scripts/update_change_notes.js"
}
}

View file

@ -0,0 +1,44 @@
#!/usr/bin/env node
import { readFileSync } from "fs"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Get the bundle zip file name based on version from gradle.properties
*/
function getBundleName() {
try {
// Read version from gradle.properties
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
const gradleVersionMatch = gradlePropertiesContent.match(/^pluginVersion=(.+)$/m)
if (!gradleVersionMatch) {
throw new Error("pluginVersion not found in gradle.properties")
}
const version = gradleVersionMatch[1].trim()
// Generate the bundle name following the pattern: Roo Code-{version}.zip
const bundleName = `Roo Code-${version}.zip`
// Output just the filename for CI usage
process.stdout.write(bundleName)
return bundleName
} catch (error) {
console.error("❌ Error getting bundle name:", error.message)
process.exit(1)
}
}
// Run the function if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
getBundleName()
}
export default getBundleName

View file

@ -0,0 +1,54 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Sync version from src/package.json to jetbrains/plugin/gradle.properties
*/
function syncVersion() {
try {
// Read version from src/package.json
const srcPackageJsonPath = join(__dirname, "../../../src/package.json")
const srcPackageJson = JSON.parse(readFileSync(srcPackageJsonPath, "utf8"))
const version = srcPackageJson.version
if (!version) {
throw new Error("Version not found in src/package.json")
}
console.log(`Found version: ${version}`)
// Read gradle.properties
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
// Update pluginVersion in gradle.properties
const updatedContent = gradlePropertiesContent.replace(/^pluginVersion=.*$/m, `pluginVersion=${version}`)
// Check if the replacement was successful
if (updatedContent === gradlePropertiesContent) {
console.warn("Warning: pluginVersion property not found or already up to date")
return
}
// Write updated gradle.properties
writeFileSync(gradlePropertiesPath, updatedContent, "utf8")
console.log(`✅ Successfully updated pluginVersion to ${version} in gradle.properties`)
} catch (error) {
console.error("❌ Error syncing version:", error.message)
process.exit(1)
}
}
// Run the sync if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
syncVersion()
}
export default syncVersion

View file

@ -0,0 +1,158 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Update change-notes in plugin.xml based on version from gradle.properties and CHANGELOG.md
*/
function updateChangeNotes() {
try {
// Read version from gradle.properties
const gradlePropertiesPath = join(__dirname, "../gradle.properties")
const gradlePropertiesContent = readFileSync(gradlePropertiesPath, "utf8")
const gradleVersionMatch = gradlePropertiesContent.match(/^pluginVersion=(.+)$/m)
if (!gradleVersionMatch) {
throw new Error("pluginVersion not found in gradle.properties")
}
const version = gradleVersionMatch[1].trim()
console.log(`Found plugin version: ${version}`)
// Read CHANGELOG.md
const changelogPath = join(__dirname, "../../../CHANGELOG.md")
const changelogContent = readFileSync(changelogPath, "utf8")
// Find the version section in changelog
const versionPattern = new RegExp(`## \\[v${version.replace(/\./g, "\\.")}\\]([\\s\\S]*?)(?=## \\[v|$)`)
const changelogVersionMatch = changelogContent.match(versionPattern)
if (!changelogVersionMatch) {
throw new Error(`Version ${version} not found in CHANGELOG.md`)
}
const changelogSection = changelogVersionMatch[1].trim()
console.log(`Found changelog section for version ${version}`)
// Convert markdown to HTML format suitable for plugin.xml
const changeNotesHtml = convertMarkdownToHtml(changelogSection, version)
// Read plugin.xml
const pluginXmlPath = join(__dirname, "../src/main/resources/META-INF/plugin.xml")
const pluginXmlContent = readFileSync(pluginXmlPath, "utf8")
// Replace change-notes section
const changeNotesPattern = /(<change-notes><!\[CDATA\[)([\s\S]*?)(\]\]><\/change-notes>)/
const updatedPluginXml = pluginXmlContent.replace(changeNotesPattern, `$1\n${changeNotesHtml}\n $3`)
// Check if the replacement was successful
if (updatedPluginXml === pluginXmlContent) {
console.warn("Warning: change-notes section not found or already up to date")
return
}
// Write updated plugin.xml
writeFileSync(pluginXmlPath, updatedPluginXml, "utf8")
console.log(`✅ Successfully updated change-notes for version ${version} in plugin.xml`)
} catch (error) {
console.error("❌ Error updating change-notes:", error.message)
process.exit(1)
}
}
/**
* Convert markdown changelog to HTML format suitable for plugin.xml
*/
function convertMarkdownToHtml(markdown, version) {
let html = ` <h3>Version ${version}</h3>\n <ul>`
// Split into lines and process
const lines = markdown.split("\n").filter((line) => line.trim())
for (const line of lines) {
const trimmedLine = line.trim()
// Skip empty lines and section headers
if (!trimmedLine || trimmedLine.startsWith("##") || trimmedLine.startsWith("###")) {
continue
}
// Handle main bullet points (features/changes)
if (trimmedLine.startsWith("- ")) {
const content = trimmedLine.substring(2).trim()
const cleanContent = cleanMarkdownContent(content)
if (cleanContent) {
html += `\n <li>${escapeHtml(cleanContent)}</li>`
}
}
// Handle sub-bullet points (patch changes, etc.)
else if (trimmedLine.match(/^\s*-\s/)) {
const content = trimmedLine.replace(/^\s*-\s/, "").trim()
const cleanContent = cleanMarkdownContent(content)
if (cleanContent) {
html += `\n <li>${escapeHtml(cleanContent)}</li>`
}
}
}
html += "\n </ul>"
return html
}
/**
* Clean markdown content by removing links, PR references, and contributor mentions
*/
function cleanMarkdownContent(content) {
return (
content
// Remove PR links like [#2012](https://github.com/...)
.replace(/\[#\d+\]\([^)]+\)\s*/g, "")
// Remove commit hash links like [`1fd698a`](https://github.com/...)
.replace(/\[`[^`]+`\]\([^)]+\)\s*/g, "")
// Remove GitHub user links like [@catrielmuller](https://github.com/catrielmuller)
.replace(/\[@[^\]]+\]\([^)]+\)\s*/g, "")
// Remove "Thanks @username!" mentions at the beginning
.replace(/^Thanks\s+@[^!]+!\s*-?\s*/g, "")
// Remove "Thanks @username!" mentions anywhere
.replace(/Thanks\s+@[^!]+!\s*-?\s*/g, "")
// Remove "Thanks @username" mentions (without exclamation)
.replace(/Thanks\s+@[^,)]+[,)]\s*/g, "")
// Remove standalone contributor mentions like "(thanks @username!)"
.replace(/\(thanks\s+@[^)]+\)\s*/g, "")
// Remove leftover "Thanks !" patterns
.replace(/Thanks\s*!\s*-?\s*/g, "")
// Remove leading dashes and spaces
.replace(/^[-\s]+/g, "")
// Clean up multiple spaces
.replace(/\s+/g, " ")
.trim()
)
}
/**
* Escape HTML special characters
*/
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
}
// Run the update if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
updateChangeNotes()
}
export default updateChangeNotes

View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
rootProject.name = "Roo Code"

View file

@ -0,0 +1,285 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actions
/**
* Constants for action names displayed in the UI.
* These represent the text shown to users in menus and context options.
*/
object ActionNames {
/** Action to explain selected code */
const val EXPLAIN = "Explain Code"
/** Action to fix issues in selected code */
const val FIX = "Fix Code"
/** Action to fix logical issues in selected code */
const val FIX_LOGIC = "Fix Logic"
/** Action to improve selected code */
const val IMPROVE = "Improve Code"
/** Action to add selected code to context */
const val ADD_TO_CONTEXT = "Add to Context"
/** Action to create a new task */
const val NEW_TASK = "New Task"
}
/**
* Command identifiers used for internal command registration and execution.
* These IDs are used to register commands with the IDE.
*/
object CommandIds {
/** Command ID for explaining code */
const val EXPLAIN = "roo-code.explainCode"
/** Command ID for fixing code */
const val FIX = "roo-code.fixCode"
/** Command ID for improving code */
const val IMPROVE = "roo-code.improveCode"
/** Command ID for adding to context */
const val ADD_TO_CONTEXT = "roo-code.addToContext"
/** Command ID for creating a new task */
const val NEW_TASK = "roo-code.newTask"
}
/** Type alias for prompt type identifiers */
typealias SupportPromptType = String
/** Type alias for prompt parameters map */
typealias PromptParams = Map<String, Any?>
/**
* Data class representing a prompt configuration with a template string.
* Templates contain placeholders that will be replaced with actual values.
*/
data class SupportPromptConfig(val template: String)
/**
* Collection of predefined prompt configurations for different use cases.
* Each configuration contains a template with placeholders for dynamic content.
*/
object SupportPromptConfigs {
/**
* Template for enhancing user prompts.
* Instructs the AI to generate an improved version of the user's input.
*/
val ENHANCE = SupportPromptConfig(
"""Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes):
${'$'}{userInput}"""
)
/**
* Template for explaining code.
* Provides structure for code explanation requests with file path and line information.
*/
val EXPLAIN = SupportPromptConfig(
"""Explain the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
${'$'}{userInput}
```
${'$'}{selectedText}
```
Please provide a clear and concise explanation of what this code does, including:
1. The purpose and functionality
2. Key components and their interactions
3. Important patterns or techniques used"""
)
/**
* Template for fixing code issues.
* Includes diagnostic information and structured format for issue resolution.
*/
val FIX = SupportPromptConfig(
"""Fix any issues in the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
${'$'}{diagnosticText}
${'$'}{userInput}
```
${'$'}{selectedText}
```
Please:
1. Address all detected problems listed above (if any)
2. Identify any other potential bugs or issues
3. Provide corrected code
4. Explain what was fixed and why"""
)
/**
* Template for improving code quality.
* Focuses on readability, performance, best practices, and error handling.
*/
val IMPROVE = SupportPromptConfig(
"""Improve the following code from file path ${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
${'$'}{userInput}
```
${'$'}{selectedText}
```
Please suggest improvements for:
1. Code readability and maintainability
2. Performance optimization
3. Best practices and patterns
4. Error handling and edge cases
Provide the improved code along with explanations for each enhancement."""
)
/**
* Template for adding code to context.
* Simple format that includes file path, line range, and selected code.
*/
val ADD_TO_CONTEXT = SupportPromptConfig(
"""${'$'}{filePath}:${'$'}{startLine}-${'$'}{endLine}
```
${'$'}{selectedText}
```"""
)
/**
* Template for adding terminal output to context.
* Includes user input and terminal content.
*/
val TERMINAL_ADD_TO_CONTEXT = SupportPromptConfig(
"""${'$'}{userInput}
Terminal output:
```
${'$'}{terminalContent}
```"""
)
/**
* Template for fixing terminal commands.
* Structured format for identifying and resolving command issues.
*/
val TERMINAL_FIX = SupportPromptConfig(
"""${'$'}{userInput}
Fix this terminal command:
```
${'$'}{terminalContent}
```
Please:
1. Identify any issues in the command
2. Provide the corrected command
3. Explain what was fixed and why"""
)
/**
* Template for explaining terminal commands.
* Provides structure for command explanation with focus on functionality and behavior.
*/
val TERMINAL_EXPLAIN = SupportPromptConfig(
"""${'$'}{userInput}
Explain this terminal command:
```
${'$'}{terminalContent}
```
Please provide:
1. What the command does
2. Explanation of each part/flag
3. Expected output and behavior"""
)
/**
* Template for creating a new task.
* Simple format that passes through user input directly.
*/
val NEW_TASK = SupportPromptConfig(
"""${'$'}{userInput}"""
)
/**
* Map of all available prompt configurations indexed by their type identifiers.
* Used for lookup when creating prompts.
*/
val configs = mapOf(
"ENHANCE" to ENHANCE,
"EXPLAIN" to EXPLAIN,
"FIX" to FIX,
"IMPROVE" to IMPROVE,
"ADD_TO_CONTEXT" to ADD_TO_CONTEXT,
"TERMINAL_ADD_TO_CONTEXT" to TERMINAL_ADD_TO_CONTEXT,
"TERMINAL_FIX" to TERMINAL_FIX,
"TERMINAL_EXPLAIN" to TERMINAL_EXPLAIN,
"NEW_TASK" to NEW_TASK
)
}
/**
* Utility object for working with support prompts.
* Provides methods for creating and customizing prompts based on templates.
*/
object SupportPrompt {
/**
* Generates formatted diagnostic text from a list of diagnostic items.
*
* @param diagnostics List of diagnostic items containing source, message, and code
* @return Formatted string of diagnostic messages or empty string if no diagnostics
*/
private fun generateDiagnosticText(diagnostics: List<Map<String, Any?>>?): String {
if (diagnostics.isNullOrEmpty()) return ""
return "\nCurrent problems detected:\n" + diagnostics.joinToString("\n") { d ->
val source = d["source"] as? String ?: "Error"
val message = d["message"] as? String ?: ""
val code = d["code"] as? String
"- [$source] $message${code?.let { " ($it)" } ?: ""}"
}
}
/**
* Creates a prompt by replacing placeholders in a template with actual values.
*
* @param template The prompt template with placeholders
* @param params Map of parameter values to replace placeholders
* @return The processed prompt with placeholders replaced by actual values
*/
private fun createPrompt(template: String, params: PromptParams): String {
val pattern = Regex("""\$\{(.*?)}""")
return pattern.replace(template) { matchResult ->
val key = matchResult.groupValues[1]
if (key == "diagnosticText") {
generateDiagnosticText(params["diagnostics"] as? List<Map<String, Any?>>)
} else if (params.containsKey(key)) {
// Ensure the value is treated as a string for replacement
val value = params[key]
when (value) {
is String -> value
else -> {
// Convert non-string values to string for replacement
value?.toString() ?: ""
}
}
} else {
// If the placeholder key is not in params, replace with empty string
""
}
}
}
/**
* Gets the template for a specific prompt type, with optional custom overrides.
*
* @param customSupportPrompts Optional map of custom prompt templates
* @param type The type of prompt to retrieve
* @return The template string for the specified prompt type
*/
fun get(customSupportPrompts: Map<String, String>?, type: SupportPromptType): String {
return customSupportPrompts?.get(type) ?: SupportPromptConfigs.configs[type]?.template ?: ""
}
/**
* Creates a complete prompt by getting the template and replacing placeholders.
*
* @param type The type of prompt to create
* @param params Parameters to substitute into the template
* @param customSupportPrompts Optional custom prompt templates
* @return The final prompt with all placeholders replaced
*/
fun create(type: SupportPromptType, params: PromptParams, customSupportPrompts: Map<String, String>? = null): String {
val template = get(customSupportPrompts, type)
return createPrompt(template, params)
}
}

View file

@ -0,0 +1,299 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actions
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import ai.roocode.jetbrains.webview.WebViewManager
/**
* Code action provider, similar to VSCode's CodeActionProvider.
* Provides functionality for creating and managing code-related actions.
*/
class CodeActionProvider {
/**
* Creates a single code action with the specified title and command.
*
* @param title The display title for the action
* @param command The command identifier to execute when action is triggered
* @return An AnAction instance that can be registered with the IDE
*/
private fun createAction(
title: String,
command: String
): AnAction {
return object : AnAction(title) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val file = e.dataContext.getData(CommonDataKeys.VIRTUAL_FILE) ?: return
// Get current parameters when the action is clicked
val effectiveRange = getEffectiveRange(editor)
if (effectiveRange == null) return
val args = mutableMapOf<String, Any?>()
args["filePath"] = file.path
args["selectedText"] = effectiveRange.text
args["startLine"] = effectiveRange.startLine + 1
args["endLine"] = effectiveRange.endLine + 1
handleCodeAction(command, title, args, project)
}
}
}
/**
* Creates a pair of actions (new task version and current task version).
*
* @param baseTitle The base title for the actions
* @param baseCommand The base command identifier
* @return List of AnAction instances
*/
private fun createActionPair(
baseTitle: String,
baseCommand: String
): List<AnAction> {
return listOf(
createAction("$baseTitle in Current Task", "${baseCommand}InCurrentTask")
)
}
/**
* Gets the effective range and text from the current editor selection.
*
* @param editor The current editor instance
* @return EffectiveRange object containing selected text and line numbers, or null if no selection
*/
private fun getEffectiveRange(editor: Editor): EffectiveRange? {
val document = editor.document
val selectionModel = editor.selectionModel
return if (selectionModel.hasSelection()) {
val selectedText = selectionModel.selectedText ?: ""
val startLine = document.getLineNumber(selectionModel.selectionStart)
val endLine = document.getLineNumber(selectionModel.selectionEnd)
EffectiveRange(selectedText, startLine, endLine)
} else {
null
}
}
/**
* Provides a list of code actions for the given action event.
*
* @param e The action event containing context information
* @return List of available code actions
*/
fun provideCodeActions(e: AnActionEvent): List<AnAction> {
val actions = mutableListOf<AnAction>()
// Add to context action
actions.add(
createAction(
ActionNames.ADD_TO_CONTEXT,
CommandIds.ADD_TO_CONTEXT
)
)
// Explain code action pair
actions.addAll(
createActionPair(
ActionNames.EXPLAIN,
CommandIds.EXPLAIN
)
)
// Fix code action pair (logic fix)
actions.addAll(
createActionPair(
ActionNames.FIX_LOGIC,
CommandIds.FIX
)
)
// Improve code action pair
actions.addAll(
createActionPair(
ActionNames.IMPROVE,
CommandIds.IMPROVE
)
)
return actions
}
}
/**
* Data class representing an effective range of selected text.
* Contains the selected text and its start/end line numbers.
*
* @property text The selected text content
* @property startLine The starting line number (0-based)
* @property endLine The ending line number (0-based)
*/
data class EffectiveRange(
val text: String,
val startLine: Int,
val endLine: Int
)
/**
* Registers a code action with the specified parameters.
*
* @param command The command identifier
* @param promptType The type of prompt to use
* @param inputPrompt Optional prompt text for user input dialog
* @param inputPlaceholder Optional placeholder text for input field
* @return An AnAction instance that can be registered with the IDE
*/
fun registerCodeAction(
command: String,
promptType: String,
inputPrompt: String? = null,
inputPlaceholder: String? = null
) : AnAction {
return object : AnAction(command) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
var userInput: String? = null
if (inputPrompt != null) {
userInput = Messages.showInputDialog(
project,
inputPrompt,
"Kilo Code",
null,
inputPlaceholder,
null
)
if (userInput == null) return // Cancelled
}
// Get selected content, line numbers, etc.
val document = editor.document
val selectionModel = editor.selectionModel
val selectedText = selectionModel.selectedText ?: ""
val startLine = if (selectionModel.hasSelection()) document.getLineNumber(selectionModel.selectionStart) else null
val endLine = if (selectionModel.hasSelection()) document.getLineNumber(selectionModel.selectionEnd) else null
val file = e.getData(CommonDataKeys.VIRTUAL_FILE)
val filePath = file?.path ?: ""
val params = mutableMapOf<String, Any?>(
"filePath" to filePath,
"selectedText" to selectedText
)
if (startLine != null) params["startLine"] = (startLine + 1).toString()
if (endLine != null) params["endLine"] = (endLine + 1).toString()
if (!userInput.isNullOrEmpty()) params["userInput"] = userInput
handleCodeAction(command, promptType, params, e.project)
}
}
}
/**
* Registers a pair of code actions with the specified parameters.
*
* @param baseCommand The base command identifier
* @param inputPrompt Optional prompt text for user input dialog
* @param inputPlaceholder Optional placeholder text for input field
* @return An AnAction instance for the new task version
*/
fun registerCodeActionPair(
baseCommand: String,
inputPrompt: String? = null,
inputPlaceholder: String? = null
) : AnAction {
// New task version
return registerCodeAction(baseCommand, baseCommand, inputPrompt, inputPlaceholder)
}
/**
* Core logic for handling code actions.
* Processes different types of commands and sends appropriate messages to the webview.
*
* @param command The command identifier
* @param promptType The type of prompt to use
* @param params Parameters for the action (can be Map or List)
* @param project The current project
*/
fun handleCodeAction(command: String, promptType: String, params: Any, project: Project?) {
val latestWebView = project?.getService(WebViewManager::class.java)?.getLatestWebView()
if (latestWebView == null) {
return
}
// Create message content based on command type
val messageContent = when {
// Add to context command
command.contains("addToContext") -> {
val promptParams = if (params is Map<*, *>) params as Map<String, Any?> else emptyMap()
mapOf(
"type" to "invoke",
"invoke" to "setChatBoxMessage",
"text" to SupportPrompt.create("ADD_TO_CONTEXT", promptParams)
)
}
// Command executed in current task
command.endsWith("InCurrentTask") -> {
val promptParams = if (params is Map<*, *>) params as Map<String, Any?> else emptyMap()
val basePromptType = when {
command.contains("explain") -> "EXPLAIN"
command.contains("fix") -> "FIX"
command.contains("improve") -> "IMPROVE"
else -> promptType
}
mapOf(
"type" to "invoke",
"invoke" to "sendMessage",
"text" to SupportPrompt.create(basePromptType, promptParams)
)
}
// Command executed in new task
else -> {
val promptParams = if (params is List<*>) {
// Process parameter list from createAction
val argsList = params as List<Any>
if (argsList.size >= 4) {
mapOf(
"filePath" to argsList[0],
"selectedText" to argsList[1],
"startLine" to argsList[2],
"endLine" to argsList[3]
)
} else {
emptyMap()
}
} else if (params is Map<*, *>) {
params as Map<String, Any?>
} else {
emptyMap()
}
val basePromptType = when {
command.contains("explain") -> "EXPLAIN"
command.contains("fix") -> "FIX"
command.contains("improve") -> "IMPROVE"
else -> promptType
}
mapOf(
"type" to "invoke",
"invoke" to "initClineWithTask",
"text" to SupportPrompt.create(basePromptType, promptParams)
)
}
}
// Convert to JSON and send
val messageJson = com.google.gson.Gson().toJson(messageContent)
latestWebView.postMessageToWebView(messageJson)
}

View file

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actions
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.project.DumbAware
/**
* Right-click menu code action group, similar to VSCode's code action provider.
* This class manages the dynamic actions that appear in the context menu when text is selected.
* Implements DumbAware to ensure the action works during indexing, and ActionUpdateThreadAware
* to specify which thread should handle action updates.
*/
class RightClickChatActionGroup : DefaultActionGroup(), DumbAware, ActionUpdateThreadAware {
/**
* Provider that supplies the actual code actions to be displayed in the menu.
*/
private val codeActionProvider = CodeActionProvider()
/**
* Updates the action group based on the current context.
* This method is called each time the menu needs to be displayed.
*
* @param e The action event containing context information
*/
override fun update(e: AnActionEvent) {
removeAll()
// Check if there is an editor and selected text
val editor = e.getData(CommonDataKeys.EDITOR)
val hasSelection = editor?.selectionModel?.hasSelection() == true
if (hasSelection) {
loadDynamicActions(e)
}
// Set the visibility of the action group
e.presentation.isVisible = hasSelection
}
/**
* Loads dynamic actions into this action group based on the current context.
*
* @param e The action event containing context information
*/
private fun loadDynamicActions(e: AnActionEvent) {
// Use actions provided by CodeActionProvider
val actions = codeActionProvider.provideCodeActions(e)
actions.forEach { action ->
add(action)
}
}
/**
* Specifies which thread should be used for updating this action.
* EDT (Event Dispatch Thread) is used for UI-related operations.
*
* @return The thread to use for action updates
*/
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
}

View file

@ -0,0 +1,176 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actions
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.core.PluginContext
import ai.roocode.jetbrains.core.ServiceProxyRegistry
/**
* Executes a VSCode command with the given command ID.
* This function uses the RPC protocol to communicate with the extension host.
*
* @param commandId The identifier of the command to execute
* @param project The current project context
*/
fun executeCommand(commandId: String, project: Project?) {
val proxy =
project?.getService(PluginContext::class.java)?.getRPCProtocol()?.getProxy(ServiceProxyRegistry.ExtHostContext.ExtHostCommands)
proxy?.executeContributedCommand(commandId, emptyList())
}
/**
* Action that handles clicks on the Plus button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class PlusButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(PlusButtonClickAction::class.java)
private val commandId: String = "roo-code.plusButtonClicked"
/**
* Performs the action when the Plus button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("Plus button clicked")
executeCommand(commandId,e.project)
}
}
/**
* Action that handles clicks on the Prompts button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class PromptsButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(PromptsButtonClickAction::class.java)
private val commandId: String = "roo-code.promptsButtonClicked"
/**
* Performs the action when the Prompts button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("Prompts button clicked")
executeCommand(commandId, e.project)
}
}
/**
* Action that handles clicks on the MCP button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class MCPButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(MCPButtonClickAction::class.java)
private val commandId: String = "roo-code.mcpButtonClicked"
/**
* Performs the action when the MCP button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("MCP button clicked")
executeCommand(commandId, e.project)
}
}
/**
* Action that handles clicks on the History button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class HistoryButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(HistoryButtonClickAction::class.java)
private val commandId: String = "roo-code.historyButtonClicked"
/**
* Performs the action when the History button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("History button clicked")
executeCommand(commandId, e.project)
}
}
class ProfileButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(ProfileButtonClickAction::class.java)
private val commandId: String = "roo-code.profileButtonClicked"
/**
* Performs the action when the Profile button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("Profile button clicked")
executeCommand(commandId, e.project)
}
}
/**
* Action that handles clicks on the Settings button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class SettingsButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(SettingsButtonClickAction::class.java)
private val commandId: String = "roo-code.settingsButtonClicked"
/**
* Performs the action when the Settings button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("Settings button clicked")
executeCommand(commandId, e.project)
}
}
/**
* Action that handles clicks on the Marketplace button in the UI.
* Executes the corresponding VSCode command when triggered.
*/
class MarketplaceButtonClickAction : AnAction() {
private val logger: Logger = Logger.getInstance(MarketplaceButtonClickAction::class.java)
private val commandId: String = "roo-code.marketplaceButtonClicked"
/**
* Performs the action when the Marketplace button is clicked.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
logger.info("Marketplace button clicked")
executeCommand(commandId, e.project)
}
}
/**
* Action that opens developer tools for the WebView.
* Takes a function that provides the current WebView instance.
*
* @property getWebViewInstance Function that returns the current WebView instance or null if not available
*/
class OpenDevToolsAction(private val getWebViewInstance: () -> ai.roocode.jetbrains.webview.WebViewInstance?) : AnAction("Open Developer Tools") {
private val logger: Logger = Logger.getInstance(OpenDevToolsAction::class.java)
/**
* Performs the action to open developer tools for the WebView.
*
* @param e The action event containing context information
*/
override fun actionPerformed(e: AnActionEvent) {
val webView = getWebViewInstance()
if (webView != null) {
webView.openDevTools()
} else {
logger.warn("No WebView instance available, cannot open developer tools")
}
}
}

View file

@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import ai.roocode.jetbrains.editor.EditorAndDocManager
import ai.roocode.jetbrains.editor.EditorHolder
import ai.roocode.jetbrains.editor.WorkspaceEdit
import ai.roocode.jetbrains.ipc.proxy.SerializableObjectWithBuffers
import kotlinx.coroutines.delay
import java.io.File
import java.nio.file.Files
/**
* Interface for handling bulk edits in the main thread.
* Provides functionality to apply workspace edits that may include multiple file and text changes.
*/
interface MainThreadBulkEditsShape {
/**
* Attempts to apply a workspace edit.
*
* @param workspaceEditDto The workspace edit data transfer object
* @param undoRedoGroupId Optional ID for grouping undo/redo operations
* @param respectAutoSaveConfig Whether to respect auto-save configuration
* @return True if all edits were applied successfully, false otherwise
*/
suspend fun tryApplyWorkspaceEdit(workspaceEditDto: SerializableObjectWithBuffers<Any>, undoRedoGroupId: Int?, respectAutoSaveConfig: Boolean?): Boolean
}
/**
* Implementation of MainThreadBulkEditsShape that handles bulk edits in the main thread.
* Processes workspace edits including file operations (create, delete, rename) and text edits.
*
* @property project The current project context
*/
class MainThreadBulkEdits(val project: Project) : MainThreadBulkEditsShape {
val logger = Logger.getInstance(MainThreadBulkEditsShape::class.java)
/**
* Attempts to apply a workspace edit by processing file operations and text edits.
*
* @param workspaceEditDto The workspace edit data transfer object
* @param undoRedoGroupId Optional ID for grouping undo/redo operations
* @param respectAutoSaveConfig Whether to respect auto-save configuration
* @return True if all edits were applied successfully, false otherwise
*/
override suspend fun tryApplyWorkspaceEdit(workspaceEditDto: SerializableObjectWithBuffers<Any>, undoRedoGroupId: Int?, respectAutoSaveConfig: Boolean?): Boolean {
val json = workspaceEditDto.value as String
logger.info("[Bulk Edit] Starting process: $json")
val cto = WorkspaceEdit.from(json)
var allSuccess = true
// Process file edits - using background thread to avoid EDT violations
cto.files.forEach { fileEdit ->
if (fileEdit.oldResource != null && fileEdit.newResource != null) {
val oldResource = File(fileEdit.oldResource.path)
val newResource = File(fileEdit.newResource.path)
try {
Files.move(oldResource.toPath(), newResource.toPath())
// Move VFS refresh operations to background thread
ApplicationManager.getApplication().executeOnPooledThread {
val vfs = LocalFileSystem.getInstance()
vfs.refreshIoFiles(listOf(oldResource, newResource))
}
logger.info("[Bulk Edit] Renamed file: ${oldResource.path} -> ${newResource.path}")
} catch (e: Exception) {
logger.error("[Bulk Edit] Failed to rename file: ${oldResource.path} -> ${newResource.path}", e)
allSuccess = false
}
} else if (fileEdit.oldResource != null) {
val oldResource = File(fileEdit.oldResource.path)
try {
oldResource.delete()
// Move VFS refresh operations to background thread
ApplicationManager.getApplication().executeOnPooledThread {
val vfs = LocalFileSystem.getInstance()
vfs.refreshIoFiles(listOf(oldResource.parentFile))
}
logger.info("[Bulk Edit] Deleted file: ${oldResource.path}")
} catch (e: Exception) {
logger.error("[Bulk Edit] Failed to delete file: ${oldResource.path}", e)
allSuccess = false
}
} else if (fileEdit.newResource != null) {
val newResource = File(fileEdit.newResource.path)
try {
val parentDir = newResource.parentFile
if (!parentDir.exists()) {
parentDir.mkdirs()
}
if (fileEdit.options?.contents != null) {
Files.write(newResource.toPath(), fileEdit.options!!.contents!!.toByteArray(Charsets.UTF_8))
} else {
newResource.createNewFile()
}
// Move VFS refresh operations to background thread
ApplicationManager.getApplication().executeOnPooledThread {
val vfs = LocalFileSystem.getInstance()
vfs.refreshIoFiles(listOf(newResource))
}
logger.info("[Bulk Edit] Created file: ${newResource.path}")
} catch (e: Exception) {
logger.error("[Bulk Edit] Failed to create file: ${newResource.path}", e)
allSuccess = false
}
}
}
// Process text edits
cto.texts.forEach { textEdit ->
logger.info("[Bulk Edit] Processing text edit: ${textEdit.resource.path}")
if (textEdit.resource.scheme != "file") {
logger.error("[Bulk Edit] Non-file resources not supported: ${textEdit.resource.path}")
allSuccess = false
return@forEach
}
var handle:EditorHolder? = null;
try {
handle = project.getService(EditorAndDocManager::class.java).getEditorHandleByUri(textEdit.resource,true)
if (handle == null) {
handle = project.getService(EditorAndDocManager::class.java).sync2ExtHost(textEdit.resource,true)
}
} catch (e: Exception) {
logger.info("[Bulk Edit] Failed to get editor handle: ${textEdit.resource.path}", e)
}
if (handle == null) {
logger.info("[Bulk Edit] Editor handle not found: ${textEdit.resource.path}")
allSuccess = false
return@forEach
}
try {
val result = handle.applyEdit(textEdit)
if (!result) {
logger.info("[Bulk Edit] Failed to apply edit: ${textEdit.resource.path}")
allSuccess = false
} else {
logger.info("[Bulk Edit] Successfully updated file: ${textEdit.resource.path}")
}
} catch (e: Exception) {
logger.error("[Bulk Edit] Exception applying edit: ${textEdit.resource.path}", e)
allSuccess = false
}
}
return allSuccess
}
}

View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
/**
* Main thread clipboard interface.
* Corresponds to the MainThreadClipboardShape interface in VSCode.
*/
interface MainThreadClipboardShape : Disposable {
/**
* Reads text from the clipboard.
* @return The string from the clipboard, or null if no text is available
*/
fun readText(): String?
/**
* Writes text to the clipboard.
* @param value The string to write to the clipboard
*/
fun writeText(value: String?)
}
/**
* Implementation of the MainThreadClipboardShape interface.
* Provides functionality to read from and write to the system clipboard.
*/
class MainThreadClipboard : MainThreadClipboardShape {
private val logger = Logger.getInstance(MainThreadClipboardShape::class.java)
/**
* Reads text from the system clipboard.
*
* @return The string from the clipboard, or null if no text is available or an error occurs
*/
override fun readText(): String? {
logger.info("Reading clipboard text")
return try {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val data = clipboard.getContents(null)
if (data != null && data.isDataFlavorSupported(DataFlavor.stringFlavor)) {
data.getTransferData(DataFlavor.stringFlavor) as? String
} else {
null
}
} catch (e: Exception) {
logger.error("Failed to read clipboard", e)
null
}
}
/**
* Writes text to the system clipboard.
*
* @param value The string to write to the clipboard
*/
override fun writeText(value: String?) {
value?.let {
logger.info("Writing clipboard text: $value")
try {
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
val selection = StringSelection(value)
clipboard.setContents(selection, selection)
} catch (e: Exception) {
logger.error("Failed to write to clipboard", e)
}
}
}
/**
* Releases resources used by this clipboard handler.
*/
override fun dispose() {
logger.info("Releasing resources: MainThreadClipboard")
}
}

View file

@ -0,0 +1,153 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.commands.CommandRegistry
import ai.roocode.jetbrains.commands.ICommand
import ai.roocode.jetbrains.editor.registerOpenEditorAPICommands
import ai.roocode.jetbrains.terminal.registerTerminalAPICommands
import ai.roocode.jetbrains.util.doInvokeMethod
import kotlin.reflect.full.functions
/**
* Main thread commands interface.
* Corresponds to the MainThreadCommandsShape interface in VSCode.
*/
interface MainThreadCommandsShape : Disposable {
/**
* Registers a command.
* @param id The command identifier
*/
fun registerCommand(id: String)
/**
* Unregisters a command.
* @param id The command identifier
*/
fun unregisterCommand(id: String)
/**
* Fires a command activation event.
* @param id The command identifier
*/
fun fireCommandActivationEvent(id: String)
/**
* Executes a command.
* @param id The command identifier
* @param args List of arguments for the command
* @return The execution result
*/
suspend fun executeCommand(id: String, args: List<Any?>): Any?
/**
* Gets all registered commands.
* @return List of command identifiers
*/
fun getCommands(): List<String>
}
/**
* Implementation of MainThreadCommandsShape that handles command registration and execution.
* Manages a registry of commands and provides methods to interact with them.
*
* @property project The current project context
*/
class MainThreadCommands(val project: Project) : MainThreadCommandsShape {
private val registry = CommandRegistry(project)
private val logger = Logger.getInstance(MainThreadCommandsShape::class.java)
/**
* Initializes the command registry with default commands.
*/
init {
registerOpenEditorAPICommands(project,registry);
registerTerminalAPICommands(project,registry);
//TODO other commands
}
/**
* Registers a command with the given identifier.
*
* @param id The command identifier
*/
override fun registerCommand(id: String) {
logger.info("Registering command: $id")
}
/**
* Unregisters a command with the given identifier.
*
* @param id The command identifier
*/
override fun unregisterCommand(id: String) {
logger.info("Unregistering command: $id")
}
/**
* Fires an activation event for the specified command.
*
* @param id The command identifier
*/
override fun fireCommandActivationEvent(id: String) {
logger.info("Firing command activation event: $id")
}
/**
* Executes a command with the given identifier and arguments.
*
* @param id The command identifier
* @param args List of arguments for the command
* @return The execution result
*/
override suspend fun executeCommand(id: String, args: List<Any?>): Any? {
logger.info("Executing command: $id ")
registry.getCommand(id)?.let { cmd->
runCmd(cmd,args)
}?: run {
logger.warn("Command not found: $id")
}
return Unit
}
/**
* Gets all registered command identifiers.
*
* @return List of command identifiers
*/
override fun getCommands(): List<String> {
logger.info("Getting all commands")
return registry.getCommands().keys.toList()
}
/**
* Releases resources used by this command handler.
*/
override fun dispose() {
logger.info("Releasing resources: MainThreadCommands")
}
/**
* Runs a command with the given arguments.
* Finds the appropriate method on the command handler and invokes it.
*
* @param cmd The command to run
* @param args List of arguments for the command
*/
private suspend fun runCmd(cmd: ICommand, args: List<Any?>) {
val handler = cmd.handler();
val method = try {
// handler.javaClass.methods.first { it.name == cmd.getMethod()}
handler::class.functions.first{ it.name == cmd.getMethod()}
}catch (e: Exception){
logger.error("Command method not found: ${cmd.getMethod()}")
return
}
doInvokeMethod(method,args,handler)
}
}

View file

@ -0,0 +1,366 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import ai.roocode.jetbrains.core.PluginContext
import ai.roocode.jetbrains.util.URI
import ai.roocode.jetbrains.util.URIComponents
/**
* Enum for configuration targets.
* Corresponds to the ConfigurationTarget enum in VSCode.
* Defines the different scopes where configuration can be applied.
*/
enum class ConfigurationTarget(val value: Int) {
/** Application-level configuration, applies globally to the entire IDE */
APPLICATION(1),
/** User-level configuration, applies to the current user across all projects */
USER(2),
/** Local user configuration, specific to the local machine */
USER_LOCAL(3),
/** Remote user configuration, for remote development scenarios */
USER_REMOTE(4),
/** Workspace-level configuration, applies to the current project workspace */
WORKSPACE(5),
/** Workspace folder-level configuration, applies to specific folders within a workspace */
WORKSPACE_FOLDER(6),
/** Default configuration target when no specific target is provided */
DEFAULT(7),
/** Memory-only configuration, temporary and not persisted */
MEMORY(8);
companion object {
/**
* Creates a ConfigurationTarget from its integer value.
* @param value The integer value representing the configuration target
* @return The corresponding ConfigurationTarget enum value, or null if not found
*/
fun fromValue(value: Int?): ConfigurationTarget? {
return values().find { it.value == value }
}
/**
* Converts a ConfigurationTarget enum to its string representation.
* @param target The configuration target to convert
* @return The string name of the configuration target
*/
fun toString(target: ConfigurationTarget): String {
return when(target) {
APPLICATION -> "APPLICATION"
USER -> "USER"
USER_LOCAL -> "USER_LOCAL"
USER_REMOTE -> "USER_REMOTE"
WORKSPACE -> "WORKSPACE"
WORKSPACE_FOLDER -> "WORKSPACE_FOLDER"
DEFAULT -> "DEFAULT"
MEMORY -> "MEMORY"
}
}
}
}
/**
* Interface for configuration overrides.
* Corresponds to the IConfigurationOverrides interface in VSCode.
* Used to provide context-specific configuration overrides.
*/
data class ConfigurationOverrides(
/** Optional identifier for overriding configuration values, typically used for language-specific settings */
val overrideIdentifier: String? = null,
/** Optional URI specifying the resource context for the configuration override */
val resource: URI? = null
)
/**
* Main thread configuration interface.
* Corresponds to the MainThreadConfigurationShape interface in VSCode.
* Defines the contract for configuration management operations that can be performed
* from the main thread of the IDE.
*/
interface MainThreadConfigurationShape : Disposable {
/**
* Updates a configuration option with the specified parameters.
* @param target Configuration target scope (application, user, workspace, etc.)
* @param key Configuration key path (e.g., "editor.fontSize")
* @param value Configuration value to set, can be null to unset
* @param overrides Optional configuration overrides for specific contexts
* @param scopeToLanguage Whether to scope this configuration to a specific language
*/
fun updateConfigurationOption(
target: Int,
key: String,
value: Any?,
overrides: Map<String, Any>?,
scopeToLanguage: Boolean?
)
/**
* Removes a configuration option from the specified target scope.
* @param target Configuration target scope from which to remove the setting
* @param key Configuration key path to remove
* @param overrides Optional configuration overrides to consider during removal
* @param scopeToLanguage Whether the configuration was scoped to a specific language
*/
fun removeConfigurationOption(
target: Int,
key: String,
overrides: Map<String, Any>?,
scopeToLanguage: Boolean?
)
}
/**
* Implementation of the main thread configuration interface.
* Provides concrete implementation for managing IDE configuration settings
* across different scopes and contexts.
*/
class MainThreadConfiguration : MainThreadConfigurationShape {
private val logger = Logger.getInstance(MainThreadConfiguration::class.java)
/**
* Updates a configuration option in the specified target scope.
* Handles the conversion of parameters and delegates to the appropriate
* storage mechanism based on the configuration target.
*/
override fun updateConfigurationOption(
target: Int,
key: String,
value: Any?,
overrides: Map<String, Any>?,
scopeToLanguage: Boolean?
) {
// Convert parameter types from raw values to type-safe objects
val configTarget = ConfigurationTarget.fromValue(target)
val configOverrides = convertToConfigurationOverrides(overrides)
// Log the configuration update for debugging purposes
logger.info("Update configuration option: target=${configTarget?.let { ConfigurationTarget.toString(it) }}, key=$key, value=$value, " +
"overrideIdentifier=${configOverrides?.overrideIdentifier}, resource=${configOverrides?.resource}, " +
"scopeToLanguage=$scopeToLanguage")
// Build the complete configuration key including overrides and language scoping
val fullKey = buildConfigurationKey(key, configOverrides, scopeToLanguage)
// Store the configuration value based on the target scope
when (configTarget) {
ConfigurationTarget.APPLICATION -> {
// Application-level configuration applies to all projects and users
val properties = PropertiesComponent.getInstance()
storeValue(properties, fullKey, value)
}
ConfigurationTarget.WORKSPACE, ConfigurationTarget.WORKSPACE_FOLDER -> {
// Project-level configuration applies to the current project
val activeProject = getActiveProject()
if (activeProject != null) {
val properties = PropertiesComponent.getInstance(activeProject)
storeValue(properties, fullKey, value)
} else {
logger.warn("Failed to save project-level configuration, no active project found")
}
}
ConfigurationTarget.USER, ConfigurationTarget.USER_LOCAL -> {
// User-level configuration applies to the current user across projects
val properties = PropertiesComponent.getInstance()
val userPrefixedKey = "user.$fullKey"
storeValue(properties, userPrefixedKey, value)
}
else -> {
// Memory-level configuration is temporary and not persisted
val properties = PropertiesComponent.getInstance()
val memoryPrefixedKey = "memory.$fullKey"
storeValue(properties, memoryPrefixedKey, value)
}
}
}
/**
* Removes a configuration option from the specified target scope.
* Handles the conversion of parameters and delegates to the appropriate
* removal mechanism based on the configuration target.
*/
override fun removeConfigurationOption(
target: Int,
key: String,
overrides: Map<String, Any>?,
scopeToLanguage: Boolean?
) {
// Convert parameter types from raw values to type-safe objects
val configTarget = ConfigurationTarget.fromValue(target)
val configOverrides = convertToConfigurationOverrides(overrides)
// Log the configuration removal for debugging purposes
logger.info("Remove configuration option: target=${configTarget?.let { ConfigurationTarget.toString(it) }}, key=$key, " +
"overrideIdentifier=${configOverrides?.overrideIdentifier}, resource=${configOverrides?.resource}, " +
"scopeToLanguage=$scopeToLanguage")
// Build the complete configuration key including overrides and language scoping
val fullKey = buildConfigurationKey(key, configOverrides, scopeToLanguage)
// Remove the configuration value based on the target scope
when (configTarget) {
ConfigurationTarget.APPLICATION -> {
// Remove application-level configuration
val properties = PropertiesComponent.getInstance()
properties.unsetValue(fullKey)
}
ConfigurationTarget.WORKSPACE, ConfigurationTarget.WORKSPACE_FOLDER -> {
// Remove project-level configuration
val activeProject = getActiveProject()
if (activeProject != null) {
val properties = PropertiesComponent.getInstance(activeProject)
properties.unsetValue(fullKey)
} else {
logger.warn("Failed to remove project-level configuration, no active project found")
}
}
ConfigurationTarget.USER, ConfigurationTarget.USER_LOCAL -> {
// Remove user-level configuration
val properties = PropertiesComponent.getInstance()
val userPrefixedKey = "user.$fullKey"
properties.unsetValue(userPrefixedKey)
}
else -> {
// Remove memory-level configuration
val properties = PropertiesComponent.getInstance()
val memoryPrefixedKey = "memory.$fullKey"
properties.unsetValue(memoryPrefixedKey)
}
}
}
/**
* Converts a Map<String, Any> to a ConfigurationOverrides object.
* Handles the parsing of URI strings and map structures into proper URI objects.
* @param overridesMap The overrides map containing configuration override data
* @return The configuration overrides object, or null if conversion fails
*/
@Suppress("UNCHECKED_CAST")
private fun convertToConfigurationOverrides(overridesMap: Map<String, Any>?): ConfigurationOverrides? {
if (overridesMap.isNullOrEmpty()) {
return null
}
try {
val overrideIdentifier = overridesMap["overrideIdentifier"] as? String
val resourceUri = when (val uriObj = overridesMap["resource"]) {
is Map<*, *> -> {
// Extract URI components from the map structure
val scheme = uriObj["scheme"] as? String ?: ""
val path = uriObj["path"] as? String ?: ""
val authority = uriObj["authority"] as? String ?: ""
val query = uriObj["query"] as? String ?: ""
val fragment = uriObj["fragment"] as? String ?: ""
if (path.isNotEmpty()) {
// Create URI instance using URI.from static method
val uriComponents = object : URIComponents {
override val scheme: String = scheme
override val authority: String = authority
override val path: String = path
override val query: String = query
override val fragment: String = fragment
}
URI.from(uriComponents)
} else {
null
}
}
is String -> {
try {
// Parse URI string using URI.parse static method
URI.parse(uriObj)
} catch (e: Exception) {
logger.warn("Failed to parse URI string: $uriObj", e)
null
}
}
else -> null
}
return ConfigurationOverrides(overrideIdentifier, resourceUri)
} catch (e: Exception) {
logger.error("Failed to convert configuration overrides: $overridesMap", e)
return null
}
}
/**
* Builds a complete configuration key based on base key, overrides, and language scoping.
* Constructs a unique key that incorporates override identifiers and resource contexts.
* @param baseKey The base configuration key
* @param overrides Optional configuration overrides to include in the key
* @param scopeToLanguage Whether to scope the configuration to a specific language
* @return The complete configuration key string
*/
private fun buildConfigurationKey(baseKey: String, overrides: ConfigurationOverrides?, scopeToLanguage: Boolean?): String {
val keyBuilder = StringBuilder(baseKey)
// Add override identifier if language scoping is enabled
overrides?.let {
it.overrideIdentifier?.let { identifier ->
if (scopeToLanguage == true) {
keyBuilder.append(".").append(identifier)
}
}
// Add resource identifier if URI is provided
it.resource?.let { uri ->
keyBuilder.append("@").append(uri.toString().hashCode())
}
}
return keyBuilder.toString()
}
/**
* Retrieves the currently active project in the IDE.
* @return The active project instance, or null if no project is currently open
*/
private fun getActiveProject(): Project? {
val openProjects = ProjectManager.getInstance().openProjects
return openProjects.firstOrNull { it.isInitialized && !it.isDisposed }
}
/**
* Stores a configuration value in the properties component based on its type.
* Handles type-specific storage for common data types and falls back to string
* representation for complex objects.
* @param properties The properties component to store the value in
* @param key The configuration key
* @param value The configuration value to store
*/
private fun storeValue(properties: PropertiesComponent, key: String, value: Any?) {
when (value) {
null -> properties.unsetValue(key)
is String -> properties.setValue(key, value)
is Boolean -> properties.setValue(key, value)
is Int -> properties.setValue(key, value, 0)
is Float -> properties.setValue(key, value.toString())
is Double -> properties.setValue(key, value.toString())
is Long -> properties.setValue(key, value.toString())
else -> {
// Convert complex objects to JSON string for storage
try {
properties.setValue(key, value.toString())
} catch (e: Exception) {
logger.error("Failed to serialize configuration value, type: ${value.javaClass.name}", e)
}
}
}
}
/**
* Disposes of resources when the configuration manager is no longer needed.
* Called when the plugin or component is being unloaded.
*/
override fun dispose() {
logger.info("Releasing resources: MainThreadConfiguration")
}
}

View file

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
/**
* Remote console log.
* Corresponds to the IRemoteConsoleLog interface in TypeScript.
*/
data class RemoteConsoleLog(
val type: String, // Log type: "log", "warn", "error", "info", "debug"
val severity: Int, // Severity level
val args: List<Any?>, // Log arguments
val source: String? = null, // Log source
val line: Int? = null, // Source line number
val columnNumber: Int? = null, // Column number
val timestamp: Long = System.currentTimeMillis() // Timestamp
)
/**
* Main thread console service interface.
* Corresponds to the MainThreadConsoleShape interface in VSCode.
*/
interface MainThreadConsoleShape : Disposable {
/**
* Logs extension host message.
* @param msg Log message object
*/
fun logExtensionHostMessage(msg: Map<String, Any>)
/**
* Releases resources.
*/
override fun dispose()
}
class MainThreadConsole : MainThreadConsoleShape {
private val logger = Logger.getInstance(MainThreadConsole::class.java)
/**
* Logs extension host message.
* @param msg Log message object
*/
override fun logExtensionHostMessage(msg: Map<String, Any>) {
val type = msg["type"]
val severity = msg["severity"]
val arguments = msg["arguments"]?.let { args ->
if (args is List<*>) {
args.joinToString(", ") { it.toString() }
} else {
args.toString()
}
} ?: return
try {
when (severity) {
// "log", "info" -> logger.info("[Extension Host] $arguments")
"warn" -> logger.warn("[Extension Host] $arguments")
"error" -> logger.warn("[Extension Host] ERROR: $arguments")
// "debug" -> logger.debug("[Extension Host] $arguments")
// else -> logger.info("[Extension Host] $arguments")
}
} catch (e: Exception) {
logger.error("Failed to process extension host log message", e)
}
}
/**
* Releases resources.
*/
override fun dispose() {
logger.info("Disposing MainThreadConsole")
}
}

View file

@ -0,0 +1,305 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import java.net.URI
/**
* Main thread debug service interface.
* This interface defines the contract for debug services that operate on the main thread,
* providing methods for managing debug sessions, breakpoints, and debug adapter communication.
*/
interface MainThreadDebugServiceShape : Disposable {
/**
* Registers debug types that this service can handle.
* @param debugTypes List of debug type identifiers (e.g., "java", "python", "node")
*/
fun registerDebugTypes(debugTypes: List<String>)
/**
* Notifies that a debug session has been cached/stored for later use.
* @param sessionID Unique identifier for the debug session
*/
fun sessionCached(sessionID: String)
/**
* Accepts and processes a message from the debug adapter.
* @param handle Unique handle identifying the debug adapter connection
* @param message The protocol message received from the debug adapter
*/
fun acceptDAMessage(handle: Int, message: Any)
/**
* Accepts and processes an error reported by the debug adapter.
* @param handle Unique handle identifying the debug adapter connection
* @param name The error name/type
* @param message Human-readable error message
* @param stack Optional stack trace for the error
*/
fun acceptDAError(handle: Int, name: String, message: String, stack: String?)
/**
* Accepts notification that the debug adapter has exited.
* @param handle Unique handle identifying the debug adapter connection
* @param code Optional exit code (null if terminated by signal)
* @param signal Optional signal name that caused termination (null if exited normally)
*/
fun acceptDAExit(handle: Int, code: Int?, signal: String?)
/**
* Registers a debug configuration provider for a specific debug type.
* @param type The debug type this provider handles
* @param triggerKind When this provider should be triggered (1=initial, 2=dynamic)
* @param hasProvideMethod Whether this provider has a provideDebugConfigurations method
* @param hasResolveMethod Whether this provider has a resolveDebugConfiguration method
* @param hasResolve2Method Whether this provider has a resolveDebugConfigurationWithSubstitutedVariables method
* @param handle Unique handle for this provider registration
* @return Registration result (typically Unit or success indicator)
*/
fun registerDebugConfigurationProvider(
type: String,
triggerKind: Int,
hasProvideMethod: Boolean,
hasResolveMethod: Boolean,
hasResolve2Method: Boolean,
handle: Int
): Any
/**
* Registers a debug adapter descriptor factory for a specific debug type.
* @param type The debug type this factory creates adapters for
* @param handle Unique handle for this factory registration
* @return Registration result (typically Unit or success indicator)
*/
fun registerDebugAdapterDescriptorFactory(type: String, handle: Int): Any
/**
* Unregisters a debug configuration provider.
* @param handle The handle of the provider to unregister
*/
fun unregisterDebugConfigurationProvider(handle: Int)
/**
* Unregisters a debug adapter descriptor factory.
* @param handle The handle of the factory to unregister
*/
fun unregisterDebugAdapterDescriptorFactory(handle: Int)
/**
* Starts a new debugging session.
* @param folder Optional workspace folder URI for the debug session
* @param nameOrConfig Either the name of a predefined configuration or the configuration object itself
* @param options Launch options for the debug session
* @return Success indicator (true if debugging started successfully)
*/
fun startDebugging(folder: URI?, nameOrConfig: Any, options: Any): Any
/**
* Stops an active debugging session.
* @param sessionId Optional session ID to stop (null stops all sessions)
* @return Operation result (typically Unit)
*/
fun stopDebugging(sessionId: String?): Any
/**
* Sets a custom name for a debug session.
* @param id The session ID to name
* @param name The display name for the session
*/
fun setDebugSessionName(id: String, name: String)
/**
* Sends a custom request to the debug adapter.
* @param id The session ID to send the request to
* @param command The debug adapter protocol command
* @param args Arguments for the command
* @return The response from the debug adapter
*/
fun customDebugAdapterRequest(id: String, command: String, args: Any): Any
/**
* Retrieves information about a specific breakpoint from the debug protocol.
* @param id The session ID
* @param breakpoinId The breakpoint ID to query
* @return Breakpoint information or null if not found
*/
fun getDebugProtocolBreakpoint(id: String, breakpoinId: String): Any?
/**
* Appends text to the debug console output.
* @param value The text to append to the console
*/
fun appendDebugConsole(value: String)
/**
* Registers new breakpoints with the debug service.
* @param breakpoints List of breakpoint objects to register
* @return Registration result (typically Unit or success indicator)
*/
fun registerBreakpoints(breakpoints: List<Any>): Any
/**
* Unregisters existing breakpoints.
* @param breakpointIds List of regular breakpoint IDs to remove
* @param functionBreakpointIds List of function breakpoint IDs to remove
* @param dataBreakpointIds List of data breakpoint IDs to remove
* @return Unregistration result (typically Unit)
*/
fun unregisterBreakpoints(
breakpointIds: List<String>,
functionBreakpointIds: List<String>,
dataBreakpointIds: List<String>
): Any
/**
* Registers a debug visualizer extension.
* @param extensionId The ID of the extension providing the visualizer
* @param id The unique ID of the visualizer within the extension
*/
fun registerDebugVisualizer(extensionId: String, id: String)
/**
* Unregisters a debug visualizer extension.
* @param extensionId The ID of the extension providing the visualizer
* @param id The unique ID of the visualizer within the extension
*/
fun unregisterDebugVisualizer(extensionId: String, id: String)
/**
* Registers a debug visualizer tree structure.
* @param treeId Unique identifier for the tree
* @param canEdit Whether the tree structure can be edited by users
*/
fun registerDebugVisualizerTree(treeId: String, canEdit: Boolean)
/**
* Unregisters a debug visualizer tree structure.
* @param treeId Unique identifier for the tree to unregister
*/
fun unregisterDebugVisualizerTree(treeId: String)
}
/**
* Main thread debug service implementation.
* This class provides the concrete implementation of the MainThreadDebugServiceShape interface,
* handling debug session management, breakpoint operations, and debug adapter communication.
* All operations are logged for debugging purposes.
*/
class MainThreadDebugService : MainThreadDebugServiceShape {
private val logger = Logger.getInstance(MainThreadDebugService::class.java)
override fun registerDebugTypes(debugTypes: List<String>) {
logger.info("Registering debug types: $debugTypes")
}
override fun sessionCached(sessionID: String) {
logger.info("Session cached: $sessionID")
}
override fun acceptDAMessage(handle: Int, message: Any) {
logger.info("Received debug adapter message: handle=$handle, message=$message")
}
override fun acceptDAError(handle: Int, name: String, message: String, stack: String?) {
logger.info("Received debug adapter error: handle=$handle, name=$name, message=$message, stack=$stack")
}
override fun acceptDAExit(handle: Int, code: Int?, signal: String?) {
logger.info("Received debug adapter exit: handle=$handle, code=$code, signal=$signal")
}
override fun registerDebugConfigurationProvider(
type: String,
triggerKind: Int,
hasProvideMethod: Boolean,
hasResolveMethod: Boolean,
hasResolve2Method: Boolean,
handle: Int
): Any {
logger.info("Registering debug configuration provider: type=$type, triggerKind=$triggerKind, " +
"hasProvideMethod=$hasProvideMethod, hasResolveMethod=$hasResolveMethod, " +
"hasResolve2Method=$hasResolve2Method, handle=$handle")
return Unit
}
override fun registerDebugAdapterDescriptorFactory(type: String, handle: Int): Any {
logger.info("Registering debug adapter descriptor factory: type=$type, handle=$handle")
return Unit
}
override fun unregisterDebugConfigurationProvider(handle: Int) {
logger.info("Unregistering debug configuration provider: handle=$handle")
}
override fun unregisterDebugAdapterDescriptorFactory(handle: Int) {
logger.info("Unregistering debug adapter descriptor factory: handle=$handle")
}
override fun startDebugging(folder: URI?, nameOrConfig: Any, options: Any): Any {
logger.info("Starting debugging: folder=$folder, nameOrConfig=$nameOrConfig, options=$options")
return true
}
override fun stopDebugging(sessionId: String?): Any {
logger.info("Stopping debugging: sessionId=$sessionId")
return Unit
}
override fun setDebugSessionName(id: String, name: String) {
logger.info("Setting debug session name: id=$id, name=$name")
}
override fun customDebugAdapterRequest(id: String, command: String, args: Any): Any {
logger.info("Custom debug adapter request: id=$id, command=$command, args=$args")
return Unit
}
override fun getDebugProtocolBreakpoint(id: String, breakpoinId: String): Any? {
logger.info("Getting debug protocol breakpoint: id=$id, breakpoinId=$breakpoinId")
return Unit
}
override fun appendDebugConsole(value: String) {
logger.info("Appending to debug console: $value")
}
override fun registerBreakpoints(breakpoints: List<Any>): Any {
logger.info("Registering breakpoints: ${breakpoints.size} total")
return Unit
}
override fun unregisterBreakpoints(
breakpointIds: List<String>,
functionBreakpointIds: List<String>,
dataBreakpointIds: List<String>
): Any {
logger.info("Unregistering breakpoints: ${breakpointIds.size} regular, " +
"${functionBreakpointIds.size} function, " +
"${dataBreakpointIds.size} data breakpoints")
return Unit
}
override fun registerDebugVisualizer(extensionId: String, id: String) {
logger.info("Registering debug visualizer: extensionId=$extensionId, id=$id")
}
override fun unregisterDebugVisualizer(extensionId: String, id: String) {
logger.info("Unregistering debug visualizer: extensionId=$extensionId, id=$id")
}
override fun registerDebugVisualizerTree(treeId: String, canEdit: Boolean) {
logger.info("Registering debug visualizer tree: treeId=$treeId, canEdit=$canEdit")
}
override fun unregisterDebugVisualizerTree(treeId: String) {
logger.info("Unregistering debug visualizer tree: treeId=$treeId")
}
override fun dispose() {
logger.info("Disposing MainThreadDebugService")
}
}

View file

@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import ai.roocode.jetbrains.util.URI
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.fileChooser.FileSaverDescriptor
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.nio.file.Path
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* Configuration options for the open file dialog.
* This data class encapsulates all the parameters needed to customize the file chooser dialog.
*
* @property defaultUri The default URI/path to start browsing from
* @property openLabel Custom label text for the dialog's open button
* @property canSelectFiles Whether files can be selected in the dialog
* @property canSelectFolders Whether folders can be selected in the dialog
* @property canSelectMany Whether multiple items can be selected simultaneously
* @property filters File extension filters for filtering displayed files (format: {"Description": ["ext1", "ext2"]})
* @property title Custom title for the dialog window
* @property allowUIResources Whether to allow UI resources to be selected
*/
data class MainThreadDialogOpenOptions(
val defaultUri: Map<String, String?>?,
val openLabel: String?,
val canSelectFiles: Boolean?,
val canSelectFolders: Boolean?,
val canSelectMany: Boolean?,
val filters: MutableMap<String, MutableList<String>>?,
val title: String?,
val allowUIResources: Boolean?,
)
/**
* Interface defining the contract for main thread dialog operations.
* This interface provides methods for showing file open and save dialogs that must be executed on the main UI thread.
*/
interface MainThreadDiaglogsShape : Disposable {
/**
* Shows an open file dialog and returns the selected file URIs.
*
* @param options Configuration options for customizing the dialog behavior
* @return List of selected file URIs, or null if the dialog was cancelled
*/
suspend fun showOpenDialog(options: Map<String, Any?>?): MutableList<URI>?
/**
* Shows a save file dialog and returns the selected file URI.
*
* @param options Configuration options for customizing the dialog behavior
* @return The selected file URI for saving, or null if the dialog was cancelled
*/
suspend fun showSaveDialog(options: Map<String, Any?>?): URI?
}
/**
* Implementation of MainThreadDiaglogsShape that provides file dialog functionality
* executed on the IntelliJ platform's main UI thread.
*
* This class handles both file open and save dialogs using IntelliJ's file chooser APIs,
* ensuring all UI operations are performed on the main thread as required by the platform.
*/
class MainThreadDiaglogs : MainThreadDiaglogsShape {
private val logger = Logger.getInstance(MainThreadDiaglogs::class.java)
/**
* Shows an open file dialog with the specified options.
*
* This method creates a file chooser dialog that allows users to select one or more files
* based on the provided configuration. The operation is performed on the main UI thread
* using IntelliJ's invokeLater mechanism.
*
* @param map Configuration map containing dialog options
* @return Mutable list of selected file URIs, or null if cancelled
*/
override suspend fun showOpenDialog(map: Map<String, Any?>?): MutableList<URI>? {
// Convert the configuration map to typed options
val options = create(map)
// Create file chooser descriptor with default values for unspecified options
val descriptor = FileChooserDescriptor(
/* chooseFiles = */ true,
/* chooseFolders = */ options?.canSelectFolders ?: true,
/* chooseJars = */ false,
/* chooseJarsAsFiles = */ false,
/* chooseMultipleJars = */ false,
/* chooseMultiple = */ options?.canSelectMany ?: true
)
.withTitle(options?.title ?: "Open")
.withDescription(options?.openLabel ?: "Select files")
// Apply file extension filters if provided
options?.filters?.forEach { (name, extensions) ->
descriptor.withFileFilter { file ->
extensions.any { file.extension?.equals(it, true) ?: false }
}
}
// Use coroutine to handle the asynchronous file chooser operation
return suspendCancellableCoroutine { continuation ->
ApplicationManager.getApplication().invokeLater({
try {
// Show the file chooser dialog and get selected files
val files = FileChooser.chooseFiles(descriptor, null, null)
// Convert IntelliJ VirtualFile objects to URI objects
val result = files.map { file ->
URI.file(file.path)
}.toMutableList()
// Resume coroutine with the result
continuation.resume(result)
} catch (e: Exception) {
// Resume coroutine with exception if an error occurs
continuation.resumeWithException(e)
}
}, ModalityState.defaultModalityState())
}
}
/**
* Shows a save file dialog with the specified options.
*
* This method creates a file saver dialog that allows users to select a location
* and filename for saving a file. The operation is performed on the main UI thread.
*
* @param map Configuration map containing dialog options
* @return URI of the selected save location, or null if cancelled
*/
override suspend fun showSaveDialog(map: Map<String, Any?>?): URI? {
// Convert the configuration map to typed options
val options = create(map)
// Create file saver descriptor with custom title and description
val descriptor = FileSaverDescriptor("Save", options?.openLabel ?: "Select save location")
// Apply file extension filters if provided
options?.filters?.forEach { (name, extensions) ->
descriptor.withFileFilter { file ->
extensions.any { file.extension?.equals(it, true) ?: false }
}
}
// Extract default path and filename from options
val path = options?.defaultUri?.get("path")
var fileName: String? = null
// Convert the path string to a Path object and extract filename
val virtualFile = path?.let { filePath ->
val file = File(filePath)
fileName = file.name
Path.of(file.parentFile.absolutePath)
}
// Use coroutine to handle the asynchronous save dialog operation
return suspendCancellableCoroutine { continuation ->
ApplicationManager.getApplication().invokeLater({
try {
// Show the save file dialog and get the selected file
val file = FileChooserFactory.getInstance()
.createSaveFileDialog(descriptor, null)
.save(virtualFile, fileName)
// Convert the result to URI format
val result = file?.let { URI.file(it.file.absolutePath) }
// Resume coroutine with the result
continuation.resume(result)
} catch (e: Exception) {
// Resume coroutine with exception if an error occurs
continuation.resumeWithException(e)
}
}, ModalityState.defaultModalityState())
}
}
/**
* Creates a MainThreadDialogOpenOptions instance from a configuration map.
*
* This helper method safely extracts typed values from a generic map structure
* and constructs a properly typed configuration object.
*
* @param map Configuration map containing dialog options as key-value pairs
* @return MainThreadDialogOpenOptions instance, or null if map is null
*/
private fun create(map: Map<String, Any?>?): MainThreadDialogOpenOptions? {
map?.let {
return MainThreadDialogOpenOptions(
defaultUri = it["defaultUri"] as? Map<String, String?>,
openLabel = it["openLabel"] as? String,
canSelectFiles = it["canSelectFiles"] as? Boolean,
canSelectFolders = it["canSelectFolders"] as? Boolean,
canSelectMany = it["canSelectMany"] as? Boolean,
filters = it["filters"] as? MutableMap<String, MutableList<String>>,
title = it["title"] as? String,
allowUIResources = it["allowUIResources"] as? Boolean
)
} ?: return null
}
/**
* Disposes of any resources held by this dialog handler.
*
* This method is called when the plugin or component is being shut down,
* allowing for proper cleanup of resources.
*/
override fun dispose() {
logger.info("Disposing MainThreadDiaglogs")
}
}

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import java.net.URI
import kotlinx.coroutines.CompletableDeferred
/**
* Interfaces related to document content providers.
*/
interface MainThreadDocumentContentProvidersShape : Disposable {
/**
* Registers a text content provider.
* @param handle Provider handle
* @param scheme URI scheme
*/
fun registerTextContentProvider(handle: Int, scheme: String)
/**
* Unregisters a text content provider.
* @param handle Provider handle
*/
fun unregisterTextContentProvider(handle: Int)
/**
* Virtual document content change.
* @param uri Document URI
* @param value New content
* @return Execution result
*/
suspend fun onVirtualDocumentChange(uri: Map<String, Any?>, value: String): Any
}
class MainThreadDocumentContentProviders : MainThreadDocumentContentProvidersShape {
private val logger = Logger.getInstance(MainThreadDocumentContentProviders::class.java)
override fun registerTextContentProvider(handle: Int, scheme: String) {
logger.info("Register text content provider: handle=$handle, scheme=$scheme")
}
override fun unregisterTextContentProvider(handle: Int) {
logger.info("Unregister text content provider: handle=$handle")
}
override suspend fun onVirtualDocumentChange(uri: Map<String, Any?>, value: String): Any {
logger.info("Virtual document content changed: uri=$uri")
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
}
override fun dispose() {
logger.info("Disposing MainThreadDocumentContentProviders resources")
}
}

View file

@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.google.common.collect.Maps
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.testFramework.utils.vfs.createFile
import ai.roocode.jetbrains.editor.EditorAndDocManager
import ai.roocode.jetbrains.editor.createURI
import ai.roocode.jetbrains.service.DocumentSyncService
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Document
import com.intellij.util.messages.MessageBusConnection
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import com.intellij.openapi.progress.ProcessCanceledException
import kotlinx.coroutines.cancel
import java.io.File
interface MainThreadDocumentsShape {
suspend fun tryCreateDocument(options: Map<String, Any?>?): Map<String, Any?>
suspend fun tryOpenDocument(uri: Map<String, Any?>, options: Map<String, Any?>?): Map<String, Any?>
suspend fun trySaveDocument(uri: Map<String, Any?>): Boolean
suspend fun tryOpenDocument(map: Map<String, Any?>, options: String?): Map<String, Any?>
}
class MainThreadDocuments(var project: Project) : MainThreadDocumentsShape {
val logger = Logger.getInstance(MainThreadDocuments::class.java)
private var messageBusConnection: MessageBusConnection? = null
private val documentSyncService = DocumentSyncService(project)
/** Coroutine scope tied to this instance, cancelled in [dispose]. */
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
init {
setupDocumentSaveListener()
}
private fun setupDocumentSaveListener() {
try {
// Connect to the message bus
messageBusConnection = ApplicationManager.getApplication().messageBus.connect()
// Listen for document save events
messageBusConnection?.subscribe(
FileDocumentManagerListener.TOPIC,
object : FileDocumentManagerListener {
override fun beforeDocumentSaving(document: Document) {
handleDocumentSaving(document)
}
}
)
logger.info("Document save listener registered successfully")
} catch (e: Exception) {
logger.error("Failed to setup document save listener", e)
}
}
private fun handleDocumentSaving(document: Document) {
// Get the virtual file associated with the document
val virtualFile = FileDocumentManager.getInstance().getFile(document)
logger.info("Handle document save event: ${virtualFile?.path}")
if (virtualFile != null && documentSyncService.shouldHandleFileEvent(virtualFile)) {
// Handle in the coroutine scope dedicated to this instance to avoid issues with older IDEs lacking project.coroutineScope
coroutineScope.launch {
try {
// Wait a short time to ensure the save operation is complete
delay(50)
if (!project.isDisposed) {
documentSyncService.syncDocumentStateOnSave(virtualFile, document)
}
} catch (e: ProcessCanceledException) {
// Normal control flow exception, can be ignored
logger.debug("Document save cancelled because project is disposed")
} catch (e: Exception) {
logger.error("Error handling document save event", e)
}
}
}
}
override suspend fun tryCreateDocument(options: Map<String, Any?>?): Map<String, Any?> {
logger.info("tryCreateDocument$options")
return mapOf()
}
override suspend fun tryOpenDocument(map: Map<String, Any?>, options: Map<String, Any?>?): Map<String, Any?> {
val uri = createURI(map)
logger.info("tryOpenDocument : ${uri.path}")
val file = File(uri.path)
val vfs = LocalFileSystem.getInstance()
if (!file.exists()) {
file.parentFile.mkdirs()
val vf = vfs.findFileByIoFile(file.parentFile)
vf?.createFile(file.name)
}
project.getService(EditorAndDocManager::class.java).openDocument(uri)
logger.info("tryOpenDocument : ${uri.path} execution completed")
return map
}
// This function is designed to work around a VS Code type system issue where a string argument may be incorrectly treated as an options: {} object. To prevent this, multiple function overloads are declared.
override suspend fun tryOpenDocument(map: Map<String, Any?>, options: String?): Map<String, Any?> {
return tryOpenDocument(map, HashMap())
}
override suspend fun trySaveDocument(map: Map<String, Any?>): Boolean {
val uri = createURI(map)
logger.info("trySaveDocument ${uri.path}")
project.getService(EditorAndDocManager::class.java).getEditorHandleByUri(uri,true)?.updateDocumentDirty(false) ?: run {
logger.info("trySaveDocument ${uri.path} not found")
return false
}
logger.info("trySaveDocument ${uri.path} execution completed")
return true
}
fun dispose() {
try {
messageBusConnection?.disconnect()
messageBusConnection = null
documentSyncService.dispose()
coroutineScope.cancel()
logger.info("Document save listener disposed")
} catch (e: Exception) {
logger.error("Error disposing document save listener", e)
}
}
}

View file

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.editor.EditorAndDocManager
interface MainThreadEditorTabsShape {
fun moveTab(tabId: String, index: Int, viewColumn: Int, preserveFocus: Boolean?)
suspend fun closeTab(tabIds: List<String>, preserveFocus: Boolean?): Boolean
suspend fun closeGroup(groupIds: List<Int>, preservceFocus: Boolean?): Boolean
}
class MainThreadEditorTabs(val project : Project) : MainThreadEditorTabsShape {
private val logger = Logger.getInstance(MainThreadEditorTabs::class.java)
override fun moveTab(tabId: String, index: Int, viewColumn: Int, preserveFocus: Boolean?) {
logger.info("moveTab $tabId")
}
override suspend fun closeTab(tabIds: List<String>, preserveFocus: Boolean?): Boolean {
logger.info("closeTab $tabIds")
// Iterate all tab IDs and trigger close event
var closedAny = true
for (tabId in tabIds){
val tab = project.getService(EditorAndDocManager::class.java).closeTab(tabId)
// closedAny = tab?.triggerClose()?:false
// if (closedAny){
// project.getService(TabStateManager::class.java).removeTab(tabId)
// }
}
return closedAny
}
override suspend fun closeGroup(groupIds: List<Int>, preservceFocus: Boolean?): Boolean {
logger.info("closeGroup $groupIds")
// Iterate all tab group IDs and trigger close event
var closedAny = false
for (groupId in groupIds){
val group = project.getService(EditorAndDocManager::class.java).closeGroup(groupId)
// closedAny = group?.triggerClose()?:false
}
return closedAny
}
}

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
/**
* Main thread error handling interface.
* Corresponds to the MainThreadErrorsShape interface in VSCode.
*/
interface MainThreadErrorsShape : Disposable {
/**
* Handles unexpected errors.
* @param err Error information
*/
fun onUnexpectedError(err: Any?)
/**
* Releases resources.
*/
override fun dispose()
}
class MainThreadErrors : MainThreadErrorsShape {
private val logger = Logger.getInstance(MainThreadErrors::class.java)
/**
* Handles unexpected errors.
* @param err Error information
*/
override fun onUnexpectedError(err: Any?) {
logger.warn("Unexpected error occurred in plugin: $err")
}
/**
* Releases resources.
*/
override fun dispose() {
logger.info("Dispose MainThreadErrors")
}
}

View file

@ -0,0 +1,281 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.core.ExtensionManager
import ai.roocode.jetbrains.ipc.proxy.IRPCProtocol
import java.net.URI
/**
* Main thread extension service interface.
* Defines the contract for managing extensions in the main thread context.
* This interface provides methods for extension lifecycle management,
* activation, error handling, and utility operations.
*/
interface MainThreadExtensionServiceShape : Disposable {
/**
* Retrieves extension information by extension ID.
* @param extensionId Extension identifier, typically provided as a Map with "value" key
* @return Extension description object containing metadata about the extension,
* or null if the extension is not found
*/
fun getExtension(extensionId: Any): Any?
/**
* Activates an extension with the specified ID and reason.
* This method triggers the extension activation process and waits for completion.
* @param extensionId Extension identifier to activate
* @param reason Optional activation reason or context information
* @return Boolean indicating whether the activation was successful (true) or failed (false)
*/
fun activateExtension(extensionId: Any, reason: Any?): Any
/**
* Called immediately before an extension is about to be activated.
* This provides a hook for pre-activation setup or logging.
* @param extensionId Extension identifier that will be activated
*/
fun onWillActivateExtension(extensionId: Any)
/**
* Called after an extension has been successfully activated.
* Provides detailed timing information about the activation process.
* @param extensionId Extension identifier that was activated
* @param codeLoadingTime Time taken to load extension code (in milliseconds)
* @param activateCallTime Time taken for the activation call (in milliseconds)
* @param activateResolvedTime Time taken to resolve activation (in milliseconds)
* @param activationReason Reason or context for the activation
*/
fun onDidActivateExtension(
extensionId: Any,
codeLoadingTime: Double,
activateCallTime: Double,
activateResolvedTime: Double,
activationReason: Any?
)
/**
* Handles extension activation errors.
* Called when an extension fails to activate due to errors or missing dependencies.
* @param extensionId Extension identifier that failed to activate
* @param error Error information or exception details
* @param missingExtensionDependency Information about missing dependencies, if applicable
* @return Unit (void) - the method handles the error internally
*/
fun onExtensionActivationError(
extensionId: Any,
error: Any?,
missingExtensionDependency: Any?
): Any
/**
* Handles runtime errors that occur during extension execution.
* Called when an extension encounters errors after successful activation.
* @param extensionId Extension identifier that encountered the runtime error
* @param error Error information or exception details
*/
fun onExtensionRuntimeError(extensionId: Any, error: Any?)
/**
* Sets performance marks for extension profiling and monitoring.
* Used to track performance metrics across extension lifecycle events.
* @param marks List of performance mark objects containing timing information
* @return Unit (void) - the method processes the marks internally
*/
fun setPerformanceMarks(marks: List<Any>)
/**
* Converts a standard URI to a browser-compatible URI format.
* This method ensures URIs are properly formatted for web browser contexts.
* @param uri The original URI to convert
* @return Browser-compatible URI object
*/
fun asBrowserUri(uri: URI): URI
}
/**
* Main thread extension service implementation.
* Provides concrete implementation for extension management in the main thread,
* handling extension lifecycle events, activation, and error management.
*
* @param extensionManager Core extension manager responsible for extension operations
* @param rpcProtocol RPC protocol for inter-process communication with extensions
*/
class MainThreadExtensionService(
private val extensionManager: ExtensionManager,
private val rpcProtocol: IRPCProtocol
) : MainThreadExtensionServiceShape {
private val logger = Logger.getInstance(MainThreadExtensionService::class.java)
/**
* Retrieves extension information by extension ID.
* Safely extracts the extension ID from various input formats and queries the extension manager.
*
* @param extensionId Extension identifier, expected as Map with "value" key or any other type
* @return Extension description object containing metadata, or null if not found
*/
override fun getExtension(extensionId: Any): Any? {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.info("Retrieving extension: $extensionIdStr")
return extensionManager.getExtensionDescription(extensionIdStr.toString())
}
/**
* Activates an extension with the specified ID and reason.
* Uses asynchronous activation via Future and waits for completion.
*
* @param extensionId Extension identifier to activate
* @param reason Optional activation reason or context information
* @return Boolean indicating activation success (true) or failure (false)
*/
override fun activateExtension(extensionId: Any, reason: Any?): Any {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.info("Activating extension: $extensionIdStr, reason: $reason")
// Use Future to get asynchronous activation result
val future = extensionManager.activateExtension(extensionIdStr.toString(), rpcProtocol)
return try {
// Wait for Future completion and return result
val result = future.get()
logger.info("Extension $extensionIdStr activation ${if (result) "successful" else "failed"}")
true
} catch (e: Exception) {
logger.error("Extension $extensionIdStr activation exception", e)
false
}
}
/**
* Called immediately before extension activation begins.
* Provides logging for pre-activation state tracking.
*
* @param extensionId Extension identifier about to be activated
*/
override fun onWillActivateExtension(extensionId: Any) {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.info("Extension $extensionIdStr is about to be activated")
}
/**
* Called after extension activation has completed successfully.
* Logs activation completion with detailed timing information.
*
* @param extensionId Extension identifier that was activated
* @param codeLoadingTime Time taken to load extension code (milliseconds)
* @param activateCallTime Time taken for activation call (milliseconds)
* @param activateResolvedTime Time taken to resolve activation (milliseconds)
* @param activationReason Reason or context for activation
*/
override fun onDidActivateExtension(
extensionId: Any,
codeLoadingTime: Double,
activateCallTime: Double,
activateResolvedTime: Double,
activationReason: Any?
) {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.info("Extension $extensionIdStr activated, reason: $activationReason")
}
/**
* Handles extension activation errors with detailed logging.
* Called when extension activation fails due to errors or missing dependencies.
*
* @param extensionId Extension identifier that failed activation
* @param error Error information or exception details
* @param missingExtensionDependency Information about missing dependencies
* @return Unit (void) - error is handled through logging
*/
override fun onExtensionActivationError(
extensionId: Any,
error: Any?,
missingExtensionDependency: Any?
): Any {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.error("Extension $extensionIdStr activation error: $error, missing dependency: $missingExtensionDependency")
return Unit
}
/**
* Handles runtime errors that occur during extension execution.
* Called when an activated extension encounters runtime errors.
*
* @param extensionId Extension identifier that encountered the error
* @param error Error information or exception details
*/
override fun onExtensionRuntimeError(extensionId: Any, error: Any?) {
// Safely extract extension ID string from input parameter
val extensionIdStr = try {
(extensionId as? Map<*, *>)?.get("value") as? String
} catch (e: Exception) {
// Fallback to string representation if extraction fails
"$extensionId"
}
logger.warn("Extension $extensionIdStr runtime error: $error")
}
/**
* Sets performance marks for extension profiling and monitoring.
* Used to track performance metrics across extension operations.
*
* @param marks List of performance mark objects containing timing information
*/
override fun setPerformanceMarks(marks: List<Any>) {
logger.info("Setting performance marks: $marks")
}
/**
* Converts a standard URI to browser-compatible format.
* Ensures URIs are properly formatted for web browser contexts.
*
* @param uri The original URI to convert
* @return Browser-compatible URI object (currently returns the original URI)
*/
override fun asBrowserUri(uri: URI): URI {
logger.info("Converting to browser URI: $uri")
return uri
}
/**
* Disposes of resources when the service is no longer needed.
* Called during application shutdown or when the extension service is being replaced.
*/
override fun dispose() {
logger.info("Disposing MainThreadExtensionService")
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
/**
* File system event service interface.
* Provides functionality for watching file system changes.
* Corresponds to the MainThreadFileSystemEventServiceShape interface in VSCode.
*/
interface MainThreadFileSystemEventServiceShape : Disposable {
/**
* Watches for file system changes.
*
* @param extensionId The extension identifier
* @param session The session identifier
* @param resource The resource URI as a map
* @param opts Watch options
* @param correlate Whether to correlate events
*/
fun watch(
extensionId: String,
session: Int,
resource: Map<String, Any?>,
opts: Map<String, Any?>,
correlate: Boolean
)
/**
* Stops watching for file system changes.
*
* @param session The session identifier to stop watching
*/
fun unwatch(session: Int)
}
/**
* Implementation of the file system event service interface.
* Handles watching and unwatching file system changes.
*/
class MainThreadFileSystemEventService : MainThreadFileSystemEventServiceShape {
private val logger = Logger.getInstance(MainThreadFileSystemEventService::class.java)
/**
* Starts watching for file system changes.
*
* @param extensionId The extension identifier
* @param session The session identifier
* @param resource The resource URI as a map
* @param opts Watch options
* @param correlate Whether to correlate events
*/
override fun watch(
extensionId: String,
session: Int,
resource: Map<String, Any?>,
opts: Map<String, Any?>,
correlate: Boolean
) {
logger.info("Starting to watch file system changes: extensionId=$extensionId, session=$session, resource=$resource, opts=$opts, correlate=$correlate")
}
/**
* Stops watching for file system changes.
*
* @param session The session identifier to stop watching
*/
override fun unwatch(session: Int) {
logger.info("Stopping file system watch: session=$session")
}
/**
* Releases resources used by this service.
*/
override fun dispose() {
logger.info("Releasing MainThreadFileSystemEventService resources")
}
}

View file

@ -0,0 +1,616 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.vfs.VirtualFile
import java.io.File
import java.net.URI
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.concurrent.ConcurrentHashMap
/**
* File type enumeration.
* Defines the possible types of files in the file system.
*/
enum class FileType {
UNKNOWN,
FILE,
DIRECTORY,
SYMBOLIC_LINK
}
/**
* File statistics information.
* Contains metadata about a file including its type, creation time, modification time, and size.
*
* @property type The type of the file (file, directory, symbolic link, etc.)
* @property ctime The creation time of the file in milliseconds since epoch
* @property mtime The last modification time of the file in milliseconds since epoch
* @property size The size of the file in bytes
*/
data class FileStat(
val type: FileType,
val ctime: Long,
val mtime: Long,
val size: Long
)
/**
* File system provider capabilities.
* Defines the capabilities and features supported by a file system provider.
*
* @property isCaseSensitive Whether the file system is case-sensitive
* @property isReadonly Whether the file system is read-only
* @property isPathCaseSensitive Whether file paths are case-sensitive
* @property canHandleFileUri Whether the provider can handle file URIs
* @property hasFileCopy Whether the provider supports file copying
* @property hasFolderCopy Whether the provider supports folder copying
* @property hasOpenReadWriteCloseCapability Whether the provider supports open/read/write/close operations
* @property hasLegacyWatchCapability Whether the provider supports legacy file watching
* @property hasDiffCapability Whether the provider supports diff operations
* @property hasFileChangeCapability Whether the provider supports file change notifications
*/
data class FileSystemProviderCapabilities(
val isCaseSensitive: Boolean,
val isReadonly: Boolean,
val isPathCaseSensitive: Boolean,
val canHandleFileUri: Boolean,
val hasFileCopy: Boolean,
val hasFolderCopy: Boolean,
val hasOpenReadWriteCloseCapability: Boolean,
val hasLegacyWatchCapability: Boolean,
val hasDiffCapability: Boolean,
val hasFileChangeCapability: Boolean
)
/**
* File overwrite options.
* Options for controlling file overwrite behavior during write operations.
*
* @property overwrite Whether to overwrite existing files
*/
data class FileOverwriteOptions(
val overwrite: Boolean
)
/**
* File delete options.
* Options for controlling file deletion behavior.
*
* @property recursive Whether to delete directories recursively
* @property useTrash Whether to move files to trash instead of permanent deletion
*/
data class FileDeleteOptions(
val recursive: Boolean,
val useTrash: Boolean
)
/**
* File change data.
* Represents a change event in the file system.
*
* @property type The type of change: 1 for ADDED, 2 for UPDATED, 3 for DELETED
* @property resource The resource that was changed, represented as a map of properties
*/
data class FileChangeDto(
val type: Int, // 1: ADDED, 2: UPDATED, 3: DELETED
val resource: Map<String, Any?>
)
/**
* Markdown string interface.
* Represents a string that can contain markdown formatting.
*/
interface MarkdownString {
val value: String
val isTrusted: Boolean
}
/**
* Main thread file system service interface.
* Corresponds to the MainThreadFileSystemShape interface in VSCode.
* Provides an abstraction layer for file system operations that can be executed from the main thread.
*/
interface MainThreadFileSystemShape : Disposable {
/**
* Registers a file system provider with the given handle and scheme.
*
* @param handle A unique identifier for the provider
* @param scheme The URI scheme this provider handles (e.g., "file", "ftp", etc.)
*/
fun registerFileSystemProvider(handle: Int, scheme: String)
/**
* Unregisters a file system provider.
*
* @param handle The handle of the provider to unregister
*/
fun unregisterProvider(handle: Int)
/**
* Gets file status information for the specified resource.
*
* @param resource The URI of the file or directory to get information about
* @return FileStat object containing file metadata
*/
fun stat(resource: URI): FileStat
/**
* Reads directory contents.
* Returns a list of entries in the specified directory.
*
* @param resource The URI of the directory to read
* @return List of pairs, where each pair contains (filename, fileType)
*/
fun readdir(resource: URI): List<Pair<String, String>>
/**
* Reads file content.
* Returns the raw bytes of the specified file.
*
* @param uri The URI of the file to read
* @return Byte array containing the file content
*/
fun readFile(uri: URI): ByteArray
/**
* Writes file content.
* Writes the provided content to the specified file.
*
* @param uri The URI of the file to write
* @param content The content to write as a byte array
* @param overwrite Whether to overwrite if the file already exists
* @return The written content as a byte array
*/
fun writeFile(uri: URI, content: ByteArray, overwrite: Boolean): ByteArray
/**
* Renames a file or directory.
* Moves a file or directory from source to target location.
*
* @param source The URI of the source file/directory
* @param target The URI of the target location
* @param options Additional options for the rename operation
*/
fun rename(source: URI, target: URI, options: Map<String, Any>)
/**
* Copies a file or directory.
* Creates a copy of the source at the target location.
*
* @param source The URI of the source file/directory
* @param target The URI of the target location
* @param options Additional options for the copy operation
*/
fun copy(source: URI, target: URI, options: Map<String, Any>)
/**
* Creates a directory.
* Creates the specified directory and any necessary parent directories.
*
* @param uri The URI of the directory to create
*/
fun mkdir(uri: URI)
/**
* Deletes a file or directory.
* Removes the specified file or directory from the file system.
*
* @param uri The URI of the file/directory to delete
* @param options Additional options for the delete operation
*/
fun delete(uri: URI, options: Map<String, Any>)
/**
* Ensures activation.
* Ensures that the file system provider for the given scheme is activated.
*
* @param scheme The URI scheme to ensure is activated
*/
fun ensureActivation(scheme: String)
/**
* Listens for file system changes.
* Processes file system change notifications from providers.
*
* @param handle The handle of the provider sending the change notification
* @param resources List of file changes to process
*/
fun onFileSystemChange(handle: Int, resources: List<FileChangeDto>)
}
/**
* Main thread file system service implementation.
* Provides implementation of file system related functionality for the IDEA platform.
* This class implements the MainThreadFileSystemShape interface and provides
* concrete implementations for all file system operations.
*/
class MainThreadFileSystem : MainThreadFileSystemShape {
private val logger = Logger.getInstance(MainThreadFileSystem::class.java)
// Registered file system providers mapped by their handles
private val providers = ConcurrentHashMap<Int, String>()
/**
* Registers a file system provider with the given handle and scheme.
* This method stores the provider information for later use.
*
* @param handle A unique identifier for the provider
* @param scheme The URI scheme this provider handles
*/
override fun registerFileSystemProvider(handle: Int, scheme: String) {
logger.info("Registering file system provider: handle=$handle, scheme=$scheme")
try {
// Store provider information
providers[handle] = scheme
// Actual implementation would need to integrate with IDEA's VFS
// based on the scheme
} catch (e: Exception) {
logger.error("Failed to register file system provider: $e")
throw e
}
}
/**
* Unregisters a file system provider.
* Removes the provider associated with the given handle.
*
* @param handle The handle of the provider to unregister
*/
override fun unregisterProvider(handle: Int) {
logger.info("Unregistering file system provider: handle=$handle")
try {
// Remove provider information
providers.remove(handle)
// Actual implementation would need to unregister the corresponding file system provider
} catch (e: Exception) {
logger.error("Failed to unregister file system provider: $e")
throw e
}
}
/**
* Gets file status information for the specified resource.
* Retrieves metadata about a file or directory including type, timestamps, and size.
*
* @param resource The URI of the file or directory to get information about
* @return FileStat object containing file metadata
*/
override fun stat(resource: URI): FileStat {
logger.info("Getting file status information: $resource")
try {
val path = getPathFromUriComponents(resource)
val file = File(path)
if (!file.exists()) {
throw Exception("File does not exist: $path")
}
val type = when {
file.isDirectory -> FileType.DIRECTORY
Files.isSymbolicLink(Paths.get(file.toURI())) -> FileType.SYMBOLIC_LINK
else -> FileType.FILE
}
val ctime = file.lastModified()
val mtime = file.lastModified()
val size = file.length()
return FileStat(type, ctime, mtime, size)
} catch (e: Exception) {
logger.error("Failed to get file status information: $e")
throw e
}
}
/**
* Reads directory contents.
* Returns a list of all entries in the specified directory.
*
* @param resource The URI of the directory to read
* @return List of pairs, where each pair contains (filename, fileType)
*/
override fun readdir(resource: URI): List<Pair<String, String>> {
logger.info("Reading directory contents: $resource")
try {
val path = getPathFromUriComponents(resource)
val file = File(path)
if (!file.exists() || !file.isDirectory) {
throw Exception("Directory does not exist or is not a directory: $path")
}
// Read directory contents
return file.listFiles()?.map {
Pair(it.name, if (it.isDirectory) FileType.DIRECTORY.ordinal.toString() else FileType.FILE.ordinal.toString())
} ?: emptyList()
} catch (e: Exception) {
logger.error("Failed to read directory contents: $e")
throw e
}
}
/**
* Reads file content.
* Returns the raw bytes of the specified file.
*
* @param uri The URI of the file to read
* @return Byte array containing the file content
*/
override fun readFile(uri: URI): ByteArray {
logger.info("Reading file content: $uri")
try {
val path = getPathFromUriComponents(uri)
val file = File(path)
if (!file.exists() || file.isDirectory) {
throw Exception("File does not exist or is a directory: $path")
}
// Read file content
return file.readBytes()
} catch (e: Exception) {
logger.error("Failed to read file content: $e")
throw e
}
}
/**
* Writes file content.
* Writes the provided content to the specified file.
*
* @param uri The URI of the file to write
* @param content The content to write as a byte array
* @param overwrite Whether to overwrite if the file already exists
* @return The written content as a byte array
*/
override fun writeFile(uri: URI, content: ByteArray, overwrite: Boolean): ByteArray {
logger.info("Writing file content: $uri, content size: ${content.size} bytes")
try {
val path = getPathFromUriComponents(uri)
val file = File(path)
// Ensure parent directory exists
file.parentFile?.mkdirs()
// Write file content
file.writeBytes(content)
return content
} catch (e: Exception) {
logger.error("Failed to write file content: $e")
throw e
}
}
/**
* Renames a file or directory.
* Moves a file or directory from source to target location.
*
* @param source The URI of the source file/directory
* @param target The URI of the target location
* @param options Additional options for the rename operation
*/
override fun rename(source: URI, target: URI, options: Map<String, Any>) {
logger.info("Renaming: $source -> $target")
try {
val sourcePath = getPathFromUriComponents(source)
val targetPath = getPathFromUriComponents(target)
val overwrite = options["overwrite"] as? Boolean ?: false
val sourceFile = File(sourcePath)
val targetFile = File(targetPath)
if (!sourceFile.exists()) {
throw Exception("Source file does not exist: $sourcePath")
}
if (targetFile.exists() && !overwrite) {
throw Exception("Target file already exists and overwrite is not allowed: $targetPath")
}
// Ensure parent directory exists
targetFile.parentFile?.mkdirs()
// Perform rename operation
if (!sourceFile.renameTo(targetFile)) {
// If simple rename fails, try copy then delete
Files.move(
Paths.get(sourcePath),
Paths.get(targetPath),
if (overwrite) StandardCopyOption.REPLACE_EXISTING else StandardCopyOption.ATOMIC_MOVE
)
}
} catch (e: Exception) {
logger.error("Rename operation failed: $e")
throw e
}
}
/**
* Copies a file or directory.
* Creates a copy of the source at the target location.
*
* @param source The URI of the source file/directory
* @param target The URI of the target location
* @param options Additional options for the copy operation
*/
override fun copy(source: URI, target: URI, options: Map<String, Any>) {
logger.info("Copying: $source -> $target")
try {
val sourcePath = getPathFromUriComponents(source)
val targetPath = getPathFromUriComponents(target)
val overwrite = options["overwrite"] as? Boolean ?: false
val sourceFile = File(sourcePath)
val targetFile = File(targetPath)
if (!sourceFile.exists()) {
throw Exception("Source file does not exist: $sourcePath")
}
if (targetFile.exists() && !overwrite) {
throw Exception("Target file already exists and overwrite is not allowed: $targetPath")
}
// Ensure parent directory exists
targetFile.parentFile?.mkdirs()
if (sourceFile.isDirectory) {
// Copy directory recursively
sourceFile.copyRecursively(targetFile, overwrite)
} else {
// Copy file
Files.copy(
Paths.get(sourcePath),
Paths.get(targetPath),
if (overwrite) StandardCopyOption.REPLACE_EXISTING else StandardCopyOption.COPY_ATTRIBUTES
)
}
} catch (e: Exception) {
logger.error("Copy operation failed: $e")
throw e
}
}
/**
* Creates a directory.
* Creates the specified directory and any necessary parent directories.
*
* @param uri The URI of the directory to create
*/
override fun mkdir(uri: URI) {
logger.info("Creating directory: $uri")
try {
val path = getPathFromUriComponents(uri)
val file = File(path)
if (file.exists()) {
throw Exception("File or directory already exists: $path")
}
// Create directory
if (!file.mkdirs()) {
throw Exception("Failed to create directory: $path")
}
} catch (e: Exception) {
logger.error("Failed to create directory: $e")
throw e
}
}
/**
* Deletes a file or directory.
* Removes the specified file or directory from the file system.
*
* @param uri The URI of the file/directory to delete
* @param options Additional options for the delete operation
*/
override fun delete(uri: URI, options: Map<String, Any>) {
logger.info("Deleting: $uri, options: $options")
try {
val path = getPathFromUriComponents(uri)
val file = File(path)
val recursive = options["recursive"] as? Boolean ?: false
val useTrash = options["useTrash"] as? Boolean ?: false
if (!file.exists()) {
// If file doesn't exist, consider deletion successful
return
}
if (useTrash) {
// TODO: Implement trash deletion based on platform
// Currently performs direct deletion, should move to trash in actual implementation
logger.warn("Trash deletion not implemented, performing direct deletion")
}
if (file.isDirectory && recursive) {
// Recursively delete directory
file.deleteRecursively()
} else if (file.isDirectory && !recursive) {
throw Exception("Cannot delete non-empty directory unless recursive=true: $path")
} else {
// Delete file
file.delete()
}
} catch (e: Exception) {
logger.error("Delete operation failed: $e")
throw e
}
}
/**
* Ensures activation.
* Ensures that the file system provider for the given scheme is activated.
*
* @param scheme The URI scheme to ensure is activated
*/
override fun ensureActivation(scheme: String) {
logger.info("Ensuring activation: $scheme")
try {
// This should handle file system activation
// Actual implementation may need to notify IDEA's VFS to refresh
} catch (e: Exception) {
logger.error("Failed to ensure activation: $e")
throw e
}
}
/**
* Listens for file system changes.
* Processes file system change notifications from providers.
*
* @param handle The handle of the provider sending the change notification
* @param resources List of file changes to process
*/
override fun onFileSystemChange(handle: Int, resources: List<FileChangeDto>) {
logger.info("File system change notification: handle=$handle, resources=${resources.joinToString { it.resource.toString() }}")
try {
// This should handle file system change notifications
// Actual implementation may need to notify IDEA's VFS to refresh
} catch (e: Exception) {
logger.error("Failed to process file system change notification: $e")
throw e
}
}
/**
* Gets file system path from URI components.
* Converts a URI to a local file system path.
*
* @param uri The URI to convert
* @return The corresponding file system path
*/
private fun getPathFromUriComponents(uri: URI): String {
return File(uri).path
}
/**
* Disposes of resources.
* Cleans up resources when this service is no longer needed.
*/
override fun dispose() {
logger.info("Disposing MainThreadFileSystem resources")
providers.clear()
}
}

View file

@ -0,0 +1,713 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.core.ExtensionIdentifier
/**
* Language features related interface.
* Corresponds to the MainThreadLanguageFeaturesShape interface in VSCode.
* This interface defines the contract for language feature providers that run on the main thread.
* It provides methods to register various language intelligence features like code completion,
* hover information, symbol navigation, and more.
*/
interface MainThreadLanguageFeaturesShape : Disposable {
/**
* Unregisters service.
* @param handle Provider handle
*/
fun unregister(handle: Int)
/**
* Registers document symbol provider.
* @param handle Provider handle
* @param selector Document selector
* @param label Label
*/
fun registerDocumentSymbolProvider(handle: Int, selector: List<Map<String, Any?>>, label: String)
/**
* Registers code lens support.
* @param handle Provider handle
* @param selector Document selector
* @param eventHandle Event handle
*/
fun registerCodeLensSupport(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?)
/**
* Emits code lens event.
* @param eventHandle Event handle
* @param event Event content
*/
fun emitCodeLensEvent(eventHandle: Int, event: Any?)
/**
* Registers definition support.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers declaration support.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerDeclarationSupport(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers implementation support.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerImplementationSupport(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers type definition support.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerTypeDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers hover provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerHoverProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers evaluatable expression provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerEvaluatableExpressionProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers inline values provider.
* @param handle Provider handle
* @param selector Document selector
* @param eventHandle Event handle
*/
fun registerInlineValuesProvider(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?)
/**
* Emits inline values event.
* @param eventHandle Event handle
* @param event Event content
*/
fun emitInlineValuesEvent(eventHandle: Int, event: Any?)
/**
* Registers document highlight provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers multi-document highlight provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerMultiDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers linked editing range provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerLinkedEditingRangeProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers reference support.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerReferenceSupport(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers code action support.
* @param handle Provider handle
* @param selector Document selector
* @param metadata Metadata
* @param displayName Display name
* @param extensionID Extension ID
* @param supportsResolve Whether to support resolve
*/
fun registerCodeActionSupport(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>,
displayName: String,
extensionID: String,
supportsResolve: Boolean
)
/**
* Registers paste edit provider.
* @param handle Provider handle
* @param selector Document selector
* @param metadata Metadata
*/
fun registerPasteEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>
)
/**
* Registers document formatting support.
* @param handle Provider handle
* @param selector Document selector
* @param extensionId Extension ID
* @param displayName Display name
*/
fun registerDocumentFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String
)
/**
* Registers range formatting support.
* @param handle Provider handle
* @param selector Document selector
* @param extensionId Extension ID
* @param displayName Display name
* @param supportRanges Whether to support ranges
*/
fun registerRangeFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String,
supportRanges: Boolean
)
/**
* Registers on-type formatting support.
* @param handle Provider handle
* @param selector Document selector
* @param autoFormatTriggerCharacters Auto-format trigger characters
* @param extensionId Extension ID
*/
fun registerOnTypeFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
autoFormatTriggerCharacters: List<String>,
extensionId: ExtensionIdentifier
)
/**
* Registers navigate type support.
* @param handle Provider handle
* @param supportsResolve Whether to support resolve
*/
fun registerNavigateTypeSupport(handle: Int, supportsResolve: Boolean)
/**
* Registers rename support.
* @param handle Provider handle
* @param selector Document selector
* @param supportsResolveInitialValues Whether to support resolve initial values
*/
fun registerRenameSupport(handle: Int, selector: List<Map<String, Any?>>, supportsResolveInitialValues: Boolean)
/**
* Registers new symbol names provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerNewSymbolNamesProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers document semantic tokens provider.
* @param handle Provider handle
* @param selector Document selector
* @param legend Legend
* @param eventHandle Event handle
*/
fun registerDocumentSemanticTokensProvider(
handle: Int,
selector: List<Map<String, Any?>>,
legend: Map<String, Any?>,
eventHandle: Int?
)
/**
* Emits document semantic tokens event.
* @param eventHandle Event handle
*/
fun emitDocumentSemanticTokensEvent(eventHandle: Int)
/**
* Registers document range semantic tokens provider.
* @param handle Provider handle
* @param selector Document selector
* @param legend Legend
*/
fun registerDocumentRangeSemanticTokensProvider(
handle: Int,
selector: List<Map<String, Any?>>,
legend: Map<String, Any?>
)
/**
* Registers completions provider.
* @param handle Provider handle
* @param selector Document selector
* @param triggerCharacters Trigger characters
* @param supportsResolveDetails Whether to support resolve details
* @param extensionId Extension ID
*/
fun registerCompletionsProvider(
handle: Int,
selector: List<Map<String, Any?>>,
triggerCharacters: List<String>,
supportsResolveDetails: Boolean,
extensionId: ExtensionIdentifier
)
/**
* Registers inline completions support.
* @param handle Provider handle
* @param selector Document selector
* @param supportsHandleDidShowCompletionItem Whether to support handle did show completion item
* @param extensionId Extension ID
* @param yieldsToExtensionIds Yields to extension IDs
* @param displayName Display name
* @param debounceDelayMs Debounce delay in milliseconds
*/
fun registerInlineCompletionsSupport(
handle: Int,
selector: List<Map<String, Any?>>,
supportsHandleDidShowCompletionItem: Boolean,
extensionId: String,
yieldsToExtensionIds: List<String>,
displayName: String?,
debounceDelayMs: Int?
)
/**
* Registers inline edit provider.
* @param handle Provider handle
* @param selector Document selector
* @param extensionId Extension ID
* @param displayName Display name
*/
fun registerInlineEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String
)
/**
* Registers signature help provider.
* @param handle Provider handle
* @param selector Document selector
* @param metadata Metadata
*/
fun registerSignatureHelpProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>
)
/**
* Registers inlay hints provider.
* @param handle Provider handle
* @param selector Document selector
* @param supportsResolve Whether to support resolve
* @param eventHandle Event handle
* @param displayName Display name
*/
fun registerInlayHintsProvider(
handle: Int,
selector: List<Map<String, Any?>>,
supportsResolve: Boolean,
eventHandle: Int?,
displayName: String?
)
/**
* Emits inlay hints event.
* @param eventHandle Event handle
*/
fun emitInlayHintsEvent(eventHandle: Int)
/**
* Registers document link provider.
* @param handle Provider handle
* @param selector Document selector
* @param supportsResolve Whether to support resolve
*/
fun registerDocumentLinkProvider(
handle: Int,
selector: List<Map<String, Any?>>,
supportsResolve: Boolean
)
/**
* Registers document color provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerDocumentColorProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers folding range provider.
* @param handle Provider handle
* @param selector Document selector
* @param extensionId Extension ID
* @param eventHandle Event handle
*/
fun registerFoldingRangeProvider(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
eventHandle: Int?
)
/**
* Emits folding range event.
* @param eventHandle Event handle
* @param event Event content
*/
fun emitFoldingRangeEvent(eventHandle: Int, event: Any?)
/**
* Registers selection range provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerSelectionRangeProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers call hierarchy provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerCallHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers type hierarchy provider.
* @param handle Provider handle
* @param selector Document selector
*/
fun registerTypeHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>)
/**
* Registers document on drop edit provider.
* @param handle Provider handle
* @param selector Document selector
* @param metadata Metadata
*/
fun registerDocumentOnDropEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>?
)
/**
* Resolves paste file data.
* @param handle Provider handle
* @param requestId Request ID
* @param dataId Data ID
* @return File data
*/
fun resolvePasteFileData(handle: Int, requestId: Int, dataId: String): ByteArray
/**
* Resolves document on drop file data.
* @param handle Provider handle
* @param requestId Request ID
* @param dataId Data ID
* @return File data
*/
fun resolveDocumentOnDropFileData(handle: Int, requestId: Int, dataId: String): ByteArray
/**
* Sets language configuration.
* @param handle Provider handle
* @param languageId Language ID
* @param configuration Configuration
*/
fun setLanguageConfiguration(handle: Int, languageId: String, configuration: Map<String, Any?>)
}
/**
* Language features related implementation class.
* This class implements the MainThreadLanguageFeaturesShape interface and provides
* concrete implementations for all language feature registration methods.
* It acts as a bridge between the extension host and the IDE's language services.
*/
class MainThreadLanguageFeatures : MainThreadLanguageFeaturesShape {
private val logger = Logger.getInstance(MainThreadLanguageFeatures::class.java)
override fun unregister(handle: Int) {
logger.info("Unregistering service: handle=$handle")
}
override fun registerDocumentSymbolProvider(handle: Int, selector: List<Map<String, Any?>>, label: String) {
logger.info("Registering document symbol provider: handle=$handle, selector=$selector, label=$label")
}
override fun registerCodeLensSupport(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?) {
logger.info("Registering code lens support: handle=$handle, selector=$selector, eventHandle=$eventHandle")
}
override fun emitCodeLensEvent(eventHandle: Int, event: Any?) {
logger.info("Emitting code lens event: eventHandle=$eventHandle, event=$event")
}
override fun registerDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering definition support: handle=$handle, selector=$selector")
}
override fun registerDeclarationSupport(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering declaration support: handle=$handle, selector=$selector")
}
override fun registerImplementationSupport(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering implementation support: handle=$handle, selector=$selector")
}
override fun registerTypeDefinitionSupport(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering type definition support: handle=$handle, selector=$selector")
}
override fun registerHoverProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering hover provider: handle=$handle, selector=$selector")
}
override fun registerEvaluatableExpressionProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering evaluatable expression provider: handle=$handle, selector=$selector")
}
override fun registerInlineValuesProvider(handle: Int, selector: List<Map<String, Any?>>, eventHandle: Int?) {
logger.info("Registering inline values provider: handle=$handle, selector=$selector, eventHandle=$eventHandle")
}
override fun emitInlineValuesEvent(eventHandle: Int, event: Any?) {
logger.info("Emitting inline values event: eventHandle=$eventHandle, event=$event")
}
override fun registerDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering document highlight provider: handle=$handle, selector=$selector")
}
override fun registerMultiDocumentHighlightProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering multi-document highlight provider: handle=$handle, selector=$selector")
}
override fun registerLinkedEditingRangeProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering linked editing range provider: handle=$handle, selector=$selector")
}
override fun registerReferenceSupport(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering reference support: handle=$handle, selector=$selector")
}
override fun registerCodeActionSupport(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>,
displayName: String,
extensionID: String,
supportsResolve: Boolean
) {
logger.info("Registering code action support: handle=$handle, selector=$selector, metadata=$metadata, displayName=$displayName, extensionID=$extensionID, supportsResolve=$supportsResolve")
}
override fun registerPasteEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>
) {
logger.info("Registering paste edit provider: handle=$handle, selector=$selector, metadata=$metadata")
}
override fun registerDocumentFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String
) {
logger.info("Registering document formatting support: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName")
}
override fun registerRangeFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String,
supportRanges: Boolean
) {
logger.info("Registering range formatting support: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName, supportRanges=$supportRanges")
}
override fun registerOnTypeFormattingSupport(
handle: Int,
selector: List<Map<String, Any?>>,
autoFormatTriggerCharacters: List<String>,
extensionId: ExtensionIdentifier
) {
logger.info("Registering on-type formatting support: handle=$handle, selector=$selector, autoFormatTriggerCharacters=$autoFormatTriggerCharacters, extensionId=${extensionId.value}")
}
override fun registerNavigateTypeSupport(handle: Int, supportsResolve: Boolean) {
logger.info("Registering navigate type support: handle=$handle, supportsResolve=$supportsResolve")
}
override fun registerRenameSupport(handle: Int, selector: List<Map<String, Any?>>, supportsResolveInitialValues: Boolean) {
logger.info("Registering rename support: handle=$handle, selector=$selector, supportsResolveInitialValues=$supportsResolveInitialValues")
}
override fun registerNewSymbolNamesProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering new symbol names provider: handle=$handle, selector=$selector")
}
override fun registerDocumentSemanticTokensProvider(
handle: Int,
selector: List<Map<String, Any?>>,
legend: Map<String, Any?>,
eventHandle: Int?
) {
logger.info("Registering document semantic tokens provider: handle=$handle, selector=$selector, legend=$legend, eventHandle=$eventHandle")
}
override fun emitDocumentSemanticTokensEvent(eventHandle: Int) {
logger.info("Emitting document semantic tokens event: eventHandle=$eventHandle")
}
override fun registerDocumentRangeSemanticTokensProvider(
handle: Int,
selector: List<Map<String, Any?>>,
legend: Map<String, Any?>
) {
logger.info("Registering document range semantic tokens provider: handle=$handle, selector=$selector, legend=$legend")
}
override fun registerCompletionsProvider(
handle: Int,
selector: List<Map<String, Any?>>,
triggerCharacters: List<String>,
supportsResolveDetails: Boolean,
extensionId: ExtensionIdentifier
) {
logger.info("Registering completions provider: handle=$handle, selector=$selector, triggerCharacters=$triggerCharacters, supportsResolveDetails=$supportsResolveDetails, extensionId=${extensionId.value}")
}
override fun registerInlineCompletionsSupport(
handle: Int,
selector: List<Map<String, Any?>>,
supportsHandleDidShowCompletionItem: Boolean,
extensionId: String,
yieldsToExtensionIds: List<String>,
displayName: String?,
debounceDelayMs: Int?
) {
logger.info("Registering inline completions support: handle=$handle, selector=$selector, supportsHandleDidShowCompletionItem=$supportsHandleDidShowCompletionItem, extensionId=$extensionId, yieldsToExtensionIds=$yieldsToExtensionIds, displayName=$displayName, debounceDelayMs=$debounceDelayMs")
}
override fun registerInlineEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
displayName: String
) {
logger.info("Registering inline edit provider: handle=$handle, selector=$selector, extensionId=${extensionId.value}, displayName=$displayName")
}
override fun registerSignatureHelpProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>
) {
logger.info("Registering signature help provider: handle=$handle, selector=$selector, metadata=$metadata")
}
override fun registerInlayHintsProvider(
handle: Int,
selector: List<Map<String, Any?>>,
supportsResolve: Boolean,
eventHandle: Int?,
displayName: String?
) {
logger.info("Registering inlay hints provider: handle=$handle, selector=$selector, supportsResolve=$supportsResolve, eventHandle=$eventHandle, displayName=$displayName")
}
override fun emitInlayHintsEvent(eventHandle: Int) {
logger.info("Emitting inlay hints event: eventHandle=$eventHandle")
}
override fun registerDocumentLinkProvider(
handle: Int,
selector: List<Map<String, Any?>>,
supportsResolve: Boolean
) {
logger.info("Registering document link provider: handle=$handle, selector=$selector, supportsResolve=$supportsResolve")
}
override fun registerDocumentColorProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering document color provider: handle=$handle, selector=$selector")
}
override fun registerFoldingRangeProvider(
handle: Int,
selector: List<Map<String, Any?>>,
extensionId: ExtensionIdentifier,
eventHandle: Int?
) {
logger.info("Registering folding range provider: handle=$handle, selector=$selector, extensionId=${extensionId.value}, eventHandle=$eventHandle")
}
override fun emitFoldingRangeEvent(eventHandle: Int, event: Any?) {
logger.info("Emitting folding range event: eventHandle=$eventHandle, event=$event")
}
override fun registerSelectionRangeProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering selection range provider: handle=$handle, selector=$selector")
}
override fun registerCallHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering call hierarchy provider: handle=$handle, selector=$selector")
}
override fun registerTypeHierarchyProvider(handle: Int, selector: List<Map<String, Any?>>) {
logger.info("Registering type hierarchy provider: handle=$handle, selector=$selector")
}
override fun registerDocumentOnDropEditProvider(
handle: Int,
selector: List<Map<String, Any?>>,
metadata: Map<String, Any?>?
) {
logger.info("Registering document on drop edit provider: handle=$handle, selector=$selector, metadata=$metadata")
}
override fun resolvePasteFileData(handle: Int, requestId: Int, dataId: String): ByteArray {
logger.info("Resolving paste file data: handle=$handle, requestId=$requestId, dataId=$dataId")
return ByteArray(0) // Return empty array, actual implementation needs to handle real file data
}
override fun resolveDocumentOnDropFileData(handle: Int, requestId: Int, dataId: String): ByteArray {
logger.info("Resolving document on drop file data: handle=$handle, requestId=$requestId, dataId=$dataId")
return ByteArray(0) // Return empty array, actual implementation needs to handle real file data
}
override fun setLanguageConfiguration(handle: Int, languageId: String, configuration: Map<String, Any?>) {
logger.info("Setting language configuration: handle=$handle, languageId=$languageId, configuration=$configuration")
}
override fun dispose() {
logger.info("Disposing MainThreadLanguageFeatures resources")
}
}

View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.plugin.SystemObjectProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import com.intellij.openapi.diagnostic.logger
import java.util.concurrent.ConcurrentHashMap
/**
* Language model tools service interface.
* Corresponds to the MainThreadLanguageModelTools interface in VSCode.
*/
interface MainThreadLanguageModelToolsShape : Disposable {
/**
* Gets all available tool list.
*/
fun getTools(): List<Map<String, Any?>>
/**
* Invokes the specified tool.
* @param dto Tool invocation parameters
* @param token Cancellation token
*/
fun invokeTool(dto: Map<String, Any?>, token: Any? = null): Map<String, Any?>
/**
* Calculates the number of tokens for the given input.
* @param callId Call ID
* @param input Input content
* @param token Cancellation token
*/
fun countTokensForInvocation(callId: String, input: String, token: Any?): Int
/**
* Registers a tool.
* @param id Tool ID
*/
fun registerTool(id: String)
/**
* Unregisters a tool.
* @param name Tool name
*/
fun unregisterTool(name: String)
}
/**
* Implementation of the language model tools service.
*/
class MainThreadLanguageModelTools : MainThreadLanguageModelToolsShape {
private val logger = logger<MainThreadLanguageModelTools>()
private val tools = ConcurrentHashMap<String, ToolInfo>()
/**
* Tool information
*/
private data class ToolInfo(
val id: String,
val registered: Boolean = true
)
override fun getTools(): List<Map<String, Any?>> {
logger.info("Get available language model tool list")
// Return the list of registered tools
return tools.values.filter { it.registered }.map {
mapOf("id" to it.id)
}
}
override fun invokeTool(dto: Map<String, Any?>, token: Any?): Map<String, Any?> {
val toolId = dto["id"] as? String ?: throw IllegalArgumentException("Tool ID cannot be empty")
val params = dto["params"] ?: emptyMap<String, Any?>()
logger.info("Invoke language model tool: $toolId")
val toolInfo = tools[toolId] ?: throw IllegalArgumentException("Tool with ID $toolId not found")
if (!toolInfo.registered) {
throw IllegalStateException("Tool $toolId is not registered")
}
// The actual tool should be invoked here. Currently returns a mock result.
// In the actual implementation, it may need to call the real tool in the extension process via RPC.
return mapOf(
"result" to "Tool $toolId invoked successfully",
"id" to toolId
)
}
override fun countTokensForInvocation(callId: String, input: String, token: Any?): Int {
logger.info("Calculate token count for tool invocation $callId")
// The actual token count should be calculated here. Currently returns a mock result.
// In the actual implementation, it may need to use a specific algorithm or service to calculate the token count.
return input.length / 4 + 1 // Simple mock token calculation
}
override fun registerTool(id: String) {
logger.info("Register language model tool: $id")
tools[id] = ToolInfo(id, true)
}
override fun unregisterTool(name: String) {
logger.info("Unregister language model tool: $name")
if (tools.containsKey(name)) {
tools[name] = tools[name]!!.copy(registered = false)
} else {
logger.warn("Attempting to unregister non-existent tool: $name")
}
}
override fun dispose() {
logger.info("Dispose MainThreadLanguageModelTools resources")
tools.clear()
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.util.URI
/**
* Main thread logger interface.
*/
interface MainThreadLoggerShape : Disposable {
/**
* Logs messages.
* @param file Log file URI
* @param messages List of log messages
*/
fun log(file: URI, messages: List<String>)
/**
* Flushes log.
* @param file Log file URI
*/
fun flush(file: URI)
/**
* Creates logger.
* @param file Log file URI
* @param options Log options
* @return Creation result
*/
fun createLogger(file: URI, options: Map<String, Any?>): Any
/**
* Registers logger.
* @param logger Logger information
* @return Registration result
*/
fun registerLogger(logger: Map<String, Any?>): Any
/**
* Deregisters logger.
* @param resource Resource URI
* @return Deregistration result
*/
fun deregisterLogger(resource: String): Any
/**
* Sets logger visibility.
* @param resource Resource URI
* @param visible Whether visible
* @return Setting result
*/
fun setVisibility(resource: String, visible: Boolean): Any
}
class MainThreadLogger : MainThreadLoggerShape {
private val logger = Logger.getInstance(MainThreadLogger::class.java)
override fun log(file: URI, messages: List<String>) {
logger.info("Logging to file: $file")
}
override fun flush(file: URI) {
logger.info("Flushing log file: $file")
}
override fun createLogger(file: URI, options: Map<String, Any?>): Any {
logger.info("Creating logger for file: $file with options: $options")
return Unit // Placeholder for actual logger object
}
override fun registerLogger(log: Map<String, Any?>): Any {
logger.info("Registering logger: $log")
return Unit // Placeholder for actual registration result
}
override fun deregisterLogger(resource: String): Any {
logger.info("Deregistering logger for resource: $resource")
return Unit // Placeholder for actual deregistration result
}
override fun setVisibility(resource: String, visible: Boolean): Any {
logger.info("Setting visibility for resource: $resource to $visible")
return Unit // Placeholder for actual visibility result
}
override fun dispose() {
logger.info("Disposing MainThreadLogger")
}
}

View file

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.application.ApplicationManager
import java.util.concurrent.atomic.AtomicReference
interface MainThreadMessageServiceShape : Disposable {
// $showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number }[]): Promise<number | undefined>;
fun showMessage(severity: Int, message: String, options: Map<String, Any>, commands: List<Map<String, Any>>): Int?
}
class MainThreadMessageService : MainThreadMessageServiceShape {
private val logger = Logger.getInstance(MainThreadMessageService::class.java)
override fun showMessage(
severity: Int,
message: String,
options: Map<String, Any>,
commands: List<Map<String, Any>>
): Int? {
logger.info("showMessage - severity: $severity, message: $message, options: $options, commands: $commands")
val project = ProjectManager.getInstance().defaultProject
val isModal = options["modal"] as? Boolean ?: false
val detail = options["detail"] as? String
return if (isModal) {
showModalMessage(project, severity, message, detail, options, commands)
} else {
showNotificationMessage(project, severity, message)
null
}
}
private fun showModalMessage(
project: com.intellij.openapi.project.Project,
severity: Int,
message: String,
detail: String?,
options: Map<String, Any>,
commands: List<Map<String, Any>>
): Int? {
// Find if there's a button with isCloseAffordance=true as cancel button
var cancelIdx = commands.indexOfFirst { it["isCloseAffordance"] == true }
// If no cancel button, automatically add a "Cancel" button at the end
val commandsWithCancel = if (cancelIdx < 0) {
val cancelHandle = commands.size
commands + mapOf("title" to "Cancel", "handle" to cancelHandle, "isCloseAffordance" to true)
} else {
commands
}
// Button title array for dialog buttons
val buttonTitles = commandsWithCancel.map { it["title"].toString() }
// Establish mapping from button index to handle for returning handle later
val handleMap = commandsWithCancel.mapIndexed { idx, cmd -> idx to (cmd["handle"] as? Number)?.toInt() }.toMap()
// Re-find the index of cancel button
val cancelIdxFinal = commandsWithCancel.indexOfFirst { it["isCloseAffordance"] == true }
// Assemble dialog main message and subtitle
val dialogMessage = if (detail.isNullOrBlank()) message else "$message\n\n$detail"
// For thread-safe retrieval of user-selected button index
val selectedIdxRef = AtomicReference<Int>()
// Ensure UI operations execute on EDT thread, show modal dialog
ApplicationManager.getApplication().invokeAndWait {
val selectedIdx = Messages.showDialog(
project,
dialogMessage,
options["source"]?.let { (it as? Map<*, *>)?.get("label")?.toString() } ?: "roo-code",
buttonTitles.toTypedArray(),
if (cancelIdxFinal >= 0) cancelIdxFinal else 0,
// Choose different icons based on severity
when (severity) {
1 -> Messages.getInformationIcon()
2 -> Messages.getWarningIcon()
3 -> Messages.getErrorIcon()
else -> Messages.getInformationIcon()
}
)
selectedIdxRef.set(selectedIdx)
}
// Get user-clicked button index and return corresponding handle
val selectedIdx = selectedIdxRef.get()
return if (selectedIdx != null && selectedIdx >= 0) handleMap[selectedIdx] else null
}
private fun showNotificationMessage(
project: com.intellij.openapi.project.Project,
severity: Int,
message: String
) {
val notificationType = when (severity) {
1 -> NotificationType.INFORMATION
2 -> NotificationType.WARNING
3 -> NotificationType.ERROR
else -> NotificationType.INFORMATION
}
val notification = NotificationGroupManager.getInstance().getNotificationGroup("roo-code").createNotification(
message,
notificationType
)
notification.notify(project)
}
override fun dispose() {
logger.info("dispose")
}
}

View file

@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.util.URI
/**
* Main thread output service interface.
* Corresponds to the MainThreadOutputServiceShape interface in VSCode.
*/
interface MainThreadOutputServiceShape : Disposable {
/**
* Registers output channel.
* @param label Label
* @param file File URI components
* @param languageId Language ID
* @param extensionId Extension ID
* @return Channel ID
*/
suspend fun register(label: String, file: Map<String, Any>, languageId: String?, extensionId: String): String
/**
* Updates output channel.
* @param channelId Channel ID
* @param mode Update mode
* @param till Update to specified position
*/
suspend fun update(channelId: String, mode: Int, till: Int? = null)
/**
* Reveals output channel.
* @param channelId Channel ID
* @param preserveFocus Whether to preserve focus
*/
suspend fun reveal(channelId: String, preserveFocus: Boolean)
/**
* Closes output channel.
* @param channelId Channel ID
*/
suspend fun close(channelId: String)
/**
* Disposes output channel.
* @param channelId Channel ID
*/
suspend fun dispose(channelId: String)
}
/**
* Implementation of the main thread output service.
*/
class MainThreadOutputService : MainThreadOutputServiceShape {
private val logger = Logger.getInstance(MainThreadOutputService::class.java)
/**
* Registers output channel.
* @param label Label
* @param file File URI components
* @param languageId Language ID
* @param extensionId Extension ID
* @return Channel ID
*/
override suspend fun register(label: String, file: Map<String, Any>, languageId: String?, extensionId: String): String {
logger.info("Register output channel: label=$label, file=$file, extensionId=$extensionId")
return label // Use label as channel ID
}
/**
* Updates output channel.
* @param channelId Channel ID
* @param mode Update mode
* @param till Update to specified position
*/
override suspend fun update(channelId: String, mode: Int, till: Int?) {
logger.info("Update output channel: channelId=$channelId, mode=$mode, till=$till")
}
/**
* Reveals output channel.
* @param channelId Channel ID
* @param preserveFocus Whether to preserve focus
*/
override suspend fun reveal(channelId: String, preserveFocus: Boolean) {
logger.info("Reveal output channel: channelId=$channelId, preserveFocus=$preserveFocus")
}
/**
* Closes output channel.
* @param channelId Channel ID
*/
override suspend fun close(channelId: String) {
logger.info("Close output channel: channelId=$channelId")
}
/**
* Disposes output channel.
* @param channelId Channel ID
*/
override suspend fun dispose(channelId: String) {
logger.info("Disposing output channel: channelId=$channelId")
}
/**
* Dispose all resources
*/
override fun dispose() {
logger.info("Disposing all output channel resources")
}
}

View file

@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import java.net.URI
/**
* Main thread search service interface.
* Corresponds to the MainThreadSearchShape interface in VSCode.
*/
interface MainThreadSearchShape : Disposable {
/**
* Registers file search provider.
* @param handle Provider ID
* @param scheme Scheme
*/
fun registerFileSearchProvider(handle: Int, scheme: String)
/**
* Registers AI text search provider.
* @param handle Provider ID
* @param scheme Scheme
*/
fun registerAITextSearchProvider(handle: Int, scheme: String)
/**
* Registers text search provider.
* @param handle Provider ID
* @param scheme Scheme
*/
fun registerTextSearchProvider(handle: Int, scheme: String)
/**
* Unregisters provider.
* @param handle Provider ID
*/
fun unregisterProvider(handle: Int)
/**
* Handles file match.
* @param handle Provider ID
* @param session Session ID
* @param data List of URI components
*/
fun handleFileMatch(handle: Int, session: Int, data: List<Map<String, Any?>>)
/**
* Handles text match.
* @param handle Provider ID
* @param session Session ID
* @param data Raw file match data
*/
fun handleTextMatch(handle: Int, session: Int, data: List<Map<String, Any?>>)
/**
* Handles telemetry data.
* @param eventName Event name
* @param data Telemetry data
*/
fun handleTelemetry(eventName: String, data: Any?)
}
/**
* Implementation of the main thread search service.
* Provides search-related functionality for the IDEA platform.
*/
class MainThreadSearch : MainThreadSearchShape {
private val logger = Logger.getInstance(MainThreadSearch::class.java)
private val searchProviders = mutableMapOf<Int, String>()
private val fileSessions = mutableMapOf<Int, MutableList<URI>>()
private val textSessions = mutableMapOf<Int, MutableList<Map<String, Any?>>>()
override fun registerFileSearchProvider(handle: Int, scheme: String) {
try {
logger.info("Registering file search provider: handle=$handle, scheme=$scheme")
searchProviders[handle] = "file:$scheme"
} catch (e: Exception) {
logger.error("Failed to register file search provider", e)
}
}
override fun registerAITextSearchProvider(handle: Int, scheme: String) {
try {
logger.info("Registering AI text search provider: handle=$handle, scheme=$scheme")
searchProviders[handle] = "aitext:$scheme"
} catch (e: Exception) {
logger.error("Failed to register AI text search provider", e)
}
}
override fun registerTextSearchProvider(handle: Int, scheme: String) {
try {
logger.info("Registering text search provider: handle=$handle, scheme=$scheme")
searchProviders[handle] = "text:$scheme"
} catch (e: Exception) {
logger.error("Failed to register text search provider", e)
}
}
override fun unregisterProvider(handle: Int) {
try {
logger.info("Unregistering provider: handle=$handle")
searchProviders.remove(handle)
} catch (e: Exception) {
logger.error("Failed to unregister search provider", e)
}
}
override fun handleFileMatch(handle: Int, session: Int, data: List<Map<String, Any?>>) {
try {
logger.info("Handling file match: handle=$handle, session=$session, matches=${data.size}")
// Convert URI components to URI
val uris = data.mapNotNull { uriComponents ->
try {
val scheme = uriComponents["scheme"] as? String ?: return@mapNotNull null
val authority = uriComponents["authority"] as? String ?: ""
val path = uriComponents["path"] as? String ?: return@mapNotNull null
val query = uriComponents["query"] as? String ?: ""
val fragment = uriComponents["fragment"] as? String ?: ""
URI(scheme, authority, path, query, fragment)
} catch (e: Exception) {
logger.warn("Failed to convert URI components: $uriComponents", e)
null
}
}
// Store match results
fileSessions.getOrPut(session) { mutableListOf() }.addAll(uris)
// TODO: Actual implementation should display these results in IDEA's search results panel
} catch (e: Exception) {
logger.error("Failed to handle file match", e)
}
}
override fun handleTextMatch(handle: Int, session: Int, data: List<Map<String, Any?>>) {
try {
logger.info("Handling text match: handle=$handle, session=$session, matches=${data.size}")
// Store match results
textSessions.getOrPut(session) { mutableListOf() }.addAll(data)
// TODO: Actual implementation should display these results in IDEA's search results panel, including highlighting matched text
} catch (e: Exception) {
logger.error("Failed to handle text match", e)
}
}
override fun handleTelemetry(eventName: String, data: Any?) {
try {
logger.info("Handling telemetry: event=$eventName, data=$data")
} catch (e: Exception) {
logger.error("Failed to handle telemetry data", e)
}
}
override fun dispose() {
logger.info("Disposing MainThreadSearch")
searchProviders.clear()
fileSessions.clear()
textSessions.clear()
}
}

View file

@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
/**
* Secret state management service interface.
*/
interface MainThreadSecretStateShape : Disposable {
/**
* Gets the secret.
* @param extensionId Extension ID
* @param key Secret key identifier
* @return Secret value, returns null if not exists
*/
suspend fun getPassword(extensionId: String, key: String): String?
/**
* Sets the secret.
* @param extensionId Extension ID
* @param key Secret key identifier
* @param value Secret value
*/
suspend fun setPassword(extensionId: String, key: String, value: String)
/**
* Deletes the secret.
* @param extensionId Extension ID
* @param key Secret key identifier
*/
suspend fun deletePassword(extensionId: String, key: String)
}
/**
* Implementation of the secret state management service.
* Stores secrets in ~/.roo-cline/secrets.json file.
*/
class MainThreadSecretState : MainThreadSecretStateShape {
private val logger = Logger.getInstance(MainThreadSecretState::class.java)
private val gson = GsonBuilder().setPrettyPrinting().create()
private val mutex = Mutex()
// Configuration file path
private val secretsDir = File(System.getProperty("user.home"), ".roo-cline")
private val secretsFile = File(secretsDir, "secrets.json")
init {
// Ensure the directory exists
if (!secretsDir.exists()) {
secretsDir.mkdirs()
logger.info("Create secret storage directory: ${secretsDir.absolutePath}")
}
}
override suspend fun getPassword(extensionId: String, key: String): String? = mutex.withLock {
try {
if (!secretsFile.exists()) {
return null
}
val jsonContent = secretsFile.readText()
if (jsonContent.isBlank()) {
return null
}
val jsonObject = JsonParser.parseString(jsonContent).asJsonObject
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: return null
val passwordElement = extensionObject.get(key) ?: return null
return passwordElement.asString
} catch (e: Exception) {
logger.warn("Failed to get secret: extensionId=$extensionId, key=$key", e)
return null
}
}
override suspend fun setPassword(extensionId: String, key: String, value: String) = mutex.withLock {
try {
val jsonObject = if (secretsFile.exists() && secretsFile.readText().isNotBlank()) {
JsonParser.parseString(secretsFile.readText()).asJsonObject
} else {
JsonObject()
}
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: JsonObject().also {
jsonObject.add(extensionId, it)
}
extensionObject.addProperty(key, value)
val jsonString = gson.toJson(jsonObject)
secretsFile.writeText(jsonString)
logger.info("Successfully set secret: extensionId=$extensionId, key=$key")
} catch (e: Exception) {
logger.error("Failed to set secret: extensionId=$extensionId, key=$key", e)
throw e
}
}
override suspend fun deletePassword(extensionId: String, key: String) = mutex.withLock {
try {
if (!secretsFile.exists()) {
return
}
val jsonContent = secretsFile.readText()
if (jsonContent.isBlank()) {
return
}
val jsonObject = JsonParser.parseString(jsonContent).asJsonObject
val extensionObject = jsonObject.getAsJsonObject(extensionId) ?: return
extensionObject.remove(key)
// If extension object is empty, delete the entire extension
if (extensionObject.size() == 0) {
jsonObject.remove(extensionId)
}
val jsonString = gson.toJson(jsonObject)
secretsFile.writeText(jsonString)
logger.info("Successfully deleted secret: extensionId=$extensionId, key=$key")
} catch (e: Exception) {
logger.error("Failed to delete secret: extensionId=$extensionId, key=$key", e)
throw e
}
}
override fun dispose() {
logger.info("Disposing MainThreadSecretState resources")
// JSON file storage doesn't require special resource disposal
}
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.service.ExtensionStorageService
/**
* Main thread storage service interface.
*/
interface MainThreadStorageShape : Disposable {
/**
* Initializes extension storage.
* @param shared Whether shared
* @param extensionId Extension ID
* @return Initialization result
*/
fun initializeExtensionStorage(shared: Boolean, extensionId: String): Any?
/**
* Sets value.
* @param shared Whether shared
* @param extensionId Extension ID
* @param value Value object
* @return Set result
*/
fun setValue(shared: Boolean, extensionId: String, value: Any)
/**
* Registers extension storage keys for synchronization.
* @param extension Extension ID and version
* @param keys List of keys
*/
fun registerExtensionStorageKeysToSync(extension: Any, keys: List<String>)
}
/**
* Implementation of the main thread storage service.
*/
class MainThreadStorage : MainThreadStorageShape {
private val logger = Logger.getInstance(MainThreadStorage::class.java)
override fun initializeExtensionStorage(shared: Boolean, extensionId: String): Any? {
logger.info("Initializing extension storage: shared=$shared, extensionId=$extensionId")
val storage = service<ExtensionStorageService>()
return storage.getValue(extensionId)
}
override fun setValue(shared: Boolean, extensionId: String, value: Any) {
// logger.info("Setting value: shared=$shared, extensionId=$extensionId, value=$value")
val storage = service<ExtensionStorageService>()
storage.setValue(extensionId, value)
}
override fun registerExtensionStorageKeysToSync(extension: Any, keys: List<String>) {
val extensionId = if (extension is Map<*, *>) {
"${extension["id"]}_${extension["version"]}"
} else {
"$extension"
}
logger.info("Registering extension storage keys for sync: extension=$extensionId, keys=$keys")
}
override fun dispose() {
logger.info("Dispose MainThreadStorage")
}
}

View file

@ -0,0 +1,223 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import ai.roocode.jetbrains.plugin.SystemObjectProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture
/**
* Main thread task service interface.
* Corresponds to the MainThreadTaskShape interface in VSCode.
*/
interface MainThreadTaskShape : Disposable {
/**
* Creates task ID.
* @param task Task DTO
* @return Task ID
*/
fun createTaskId(task: Map<String, Any?>): String
/**
* Registers task provider.
* @param handle Provider ID
* @param type Task type
*/
fun registerTaskProvider(handle: Int, type: String)
/**
* Unregisters task provider.
* @param handle Provider ID
*/
fun unregisterTaskProvider(handle: Int)
/**
* Fetches task list.
* @param filter Task filter
* @return Task list
*/
fun fetchTasks(filter: Map<String, Any?>?): List<Map<String, Any?>>
/**
* Gets task execution instance.
* @param value Task handle or task DTO
* @return Task execution DTO
*/
fun getTaskExecution(value: Map<String, Any?>): Map<String, Any?>
/**
* Executes task.
* @param task Task handle or task DTO
* @return Task execution DTO
*/
fun executeTask(task: Map<String, Any?>): Map<String, Any?>
/**
* Terminates task.
* @param id Task ID
*/
fun terminateTask(id: String)
/**
* Registers task system.
* @param scheme Scheme
* @param info Task system information
*/
fun registerTaskSystem(scheme: String, info: Map<String, Any?>)
/**
* Custom execution complete.
* @param id Task ID
* @param result Execution result
*/
fun customExecutionComplete(id: String, result: Int?)
/**
* Registers supported execution types.
* @param custom Whether supports custom execution
* @param shell Whether supports shell execution
* @param process Whether supports process execution
*/
fun registerSupportedExecutions(custom: Boolean?, shell: Boolean?, process: Boolean?)
}
/**
* Implementation of the main thread task service.
* Provides task-related functionality for the IDEA platform.
*/
class MainThreadTask : MainThreadTaskShape {
private val logger = Logger.getInstance(MainThreadTask::class.java)
private val taskProviders = mutableMapOf<Int, String>()
private val taskExecutions = mutableMapOf<String, Map<String, Any?>>()
override fun createTaskId(task: Map<String, Any?>):String {
try {
logger.info("Creating task ID for task: $task")
val id = "task-${System.currentTimeMillis()}-${task.hashCode()}"
logger.debug("Generated task ID: $id")
return id
} catch (e: Exception) {
logger.error("Failed to create task ID", e)
throw e
}
}
override fun registerTaskProvider(handle: Int, type: String) {
try {
logger.info("Registering task provider: handle=$handle, type=$type")
taskProviders[handle] = type
} catch (e: Exception) {
logger.error("Failed to register task provider", e)
}
}
override fun unregisterTaskProvider(handle: Int) {
try {
logger.info("Unregistering task provider: handle=$handle")
taskProviders.remove(handle)
} catch (e: Exception) {
logger.error("Failed to unregister task provider", e)
}
}
override fun fetchTasks(filter: Map<String, Any?>?): List<Map<String, Any?>> {
try {
logger.info("Fetching tasks with filter: $filter")
// TODO: Actual implementation should query IDEA's task system
return emptyList()
} catch (e: Exception) {
logger.error("Failed to get tasks", e)
throw e
}
}
override fun getTaskExecution(value: Map<String, Any?>): Map<String, Any?> {
try {
val taskId = value["id"] as? String ?: value["taskId"] as? String
logger.info("Getting task execution for task: $taskId")
// Create a simple task execution DTO
return mapOf(
"id" to (taskId ?: "unknown-task"),
"task" to value,
"active" to false
)
} catch (e: Exception) {
logger.error("Failed to get task execution", e)
throw e
}
}
override fun executeTask(task: Map<String, Any?>):Map<String, Any?> {
try {
val taskId = task["id"] as? String ?: task["taskId"] as? String ?: "unknown-task"
logger.info("Executing task: $taskId")
// Create an executing task execution DTO
val execution = mapOf(
"id" to taskId,
"task" to task,
"active" to true
)
// Store task execution information
taskExecutions[taskId] = execution
return execution
} catch (e: Exception) {
logger.error("Failed to execute task", e)
throw e
}
}
override fun terminateTask(id: String) {
try {
logger.info("Terminating task: $id")
taskExecutions.remove(id)
} catch (e: Exception) {
logger.error("Failed to terminate task", e)
}
}
override fun registerTaskSystem(scheme: String, info: Map<String, Any?>) {
try {
logger.info("Registering task system: scheme=$scheme, info=$info")
// Register task system
} catch (e: Exception) {
logger.error("Failed to register task system", e)
}
}
override fun customExecutionComplete(id: String, result: Int?) {
try {
logger.info("Custom execution complete for task: $id with result: $result")
// Update task execution status
taskExecutions[id]?.let { execution ->
taskExecutions[id] = execution + ("active" to false)
}
} catch (e: Exception) {
logger.error("Failed to update custom execution completion status", e)
}
}
override fun registerSupportedExecutions(custom: Boolean?, shell: Boolean?, process: Boolean?) {
try {
logger.info("Registering supported executions: custom=$custom, shell=$shell, process=$process")
} catch (e: Exception) {
logger.error("Failed to register supported execution types", e)
}
}
override fun dispose() {
logger.info("Disposing MainThreadTask")
taskProviders.clear()
taskExecutions.clear()
}
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
/**
* Main thread telemetry service interface.
*/
interface MainThreadTelemetryShape : Disposable {
/**
* Logs public event.
* @param eventName Event name
* @param data Event data
*/
fun publicLog(eventName: String, data: Any?)
/**
* Logs public event (supports categorized events).
* @param eventName Event name
* @param data Event data
*/
fun publicLog2(eventName: String, data: Any?)
}
class MainThreadTelemetry : MainThreadTelemetryShape {
private val logger = Logger.getInstance(MainThreadTelemetry::class.java)
override fun publicLog(eventName: String, data: Any?) {
logger.info("[Telemetry] $eventName: $data")
}
override fun publicLog2(eventName: String, data: Any?) {
logger.info("[Telemetry] $eventName: $data")
}
override fun dispose() {
logger.info("Dispose MainThreadTelemetry")
}
}

View file

@ -0,0 +1,442 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.core.PluginContext
import ai.roocode.jetbrains.terminal.TerminalInstance
import ai.roocode.jetbrains.terminal.TerminalInstanceManager
import ai.roocode.jetbrains.terminal.TerminalConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
/**
* Main thread terminal service interface.
* Corresponds to the MainThreadTerminalServiceShape interface in VSCode.
*/
interface MainThreadTerminalServiceShape : Disposable {
/**
* Creates terminal.
* @param extHostTerminalId Extension host terminal ID
* @param config Terminal launch configuration
*/
suspend fun createTerminal(extHostTerminalId: String, config: Map<String, Any?>)
/**
* Disposes terminal resources.
* @param id Terminal identifier (can be String or Number)
*/
fun dispose(id: Any)
/**
* Hides terminal.
* @param id Terminal identifier (can be String or Number)
*/
fun hide(id: Any)
/**
* Sends text to terminal.
* @param id Terminal identifier (can be String or Number)
* @param text Text to send
* @param shouldExecute Whether to execute
*/
fun sendText(id: Any, text: String, shouldExecute: Boolean?)
/**
* Shows terminal.
* @param id Terminal identifier (can be String or Number)
* @param preserveFocus Whether to preserve focus
*/
fun show(id: Any, preserveFocus: Boolean?)
/**
* Registers process support.
* @param isSupported Whether supported
*/
fun registerProcessSupport(isSupported: Boolean)
/**
* Registers profile provider.
* @param id Profile provider ID
* @param extensionIdentifier Extension identifier
*/
fun registerProfileProvider(id: String, extensionIdentifier: String)
/**
* Unregisters profile provider.
* @param id Profile provider ID
*/
fun unregisterProfileProvider(id: String)
/**
* Registers completion provider.
* @param id Completion provider ID
* @param extensionIdentifier Extension identifier
* @param triggerCharacters List of trigger characters
*/
fun registerCompletionProvider(id: String, extensionIdentifier: String, vararg triggerCharacters: String)
/**
* Unregisters completion provider.
* @param id Completion provider ID
*/
fun unregisterCompletionProvider(id: String)
/**
* Registers quick fix provider.
* @param id Quick fix provider ID
* @param extensionIdentifier Extension identifier
*/
fun registerQuickFixProvider(id: String, extensionIdentifier: String)
/**
* Unregisters quick fix provider.
* @param id Quick fix provider ID
*/
fun unregisterQuickFixProvider(id: String)
/**
* Set environment variable collection
* @param extensionIdentifier Extension identifier
* @param persistent Whether to persist
* @param collection Serializable environment variable collection
* @param descriptionMap Serializable environment description mapping
*/
fun setEnvironmentVariableCollection(
extensionIdentifier: String,
persistent: Boolean,
collection: Map<String, Any?>?,
descriptionMap: Map<String, Any?>
)
/**
* Start sending data events
*/
fun startSendingDataEvents()
/**
* Stop sending data events
*/
fun stopSendingDataEvents()
/**
* Start sending command events
*/
fun startSendingCommandEvents()
/**
* Stop sending command events
*/
fun stopSendingCommandEvents()
/**
* Start link provider
*/
fun startLinkProvider()
/**
* Stop link provider
*/
fun stopLinkProvider()
/**
* Send process data
* @param terminalId Terminal ID
* @param data Data
*/
fun sendProcessData(terminalId: Int, data: String)
/**
* Send process ready
* @param terminalId Terminal ID
* @param pid Process ID
* @param cwd Current working directory
* @param windowsPty Windows PTY information
*/
fun sendProcessReady(
terminalId: Int,
pid: Int,
cwd: String,
windowsPty: Map<String, Any?>?
)
/**
* Send process property
* @param terminalId Terminal ID
* @param property Process property
*/
fun sendProcessProperty(terminalId: Int, property: Map<String, Any?>)
/**
* Send process exit
* @param terminalId Terminal ID
* @param exitCode Exit code
*/
fun sendProcessExit(terminalId: Int, exitCode: Int?)
}
/**
* Main thread terminal service implementation class
* Provides implementation of IDEA platform terminal-related functionality
*/
class MainThreadTerminalService(private val project: Project) : MainThreadTerminalServiceShape {
private val logger = Logger.getInstance(MainThreadTerminalService::class.java)
// Use terminal instance manager
private val terminalManager = project.service<TerminalInstanceManager>()
// Coroutine scope - use IO dispatcher to avoid Main Dispatcher issues
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override suspend fun createTerminal(extHostTerminalId: String, config: Map<String, Any?>) {
logger.info("🚀 Creating terminal: $extHostTerminalId, config: $config")
try {
// Check if terminal already exists
if (terminalManager.containsTerminal(extHostTerminalId)) {
logger.warn("Terminal already exists: $extHostTerminalId")
return
}
// Get RPC protocol instance
val pluginContext = PluginContext.getInstance(project)
val rpcProtocol = pluginContext.getRPCProtocol()
if (rpcProtocol == null) {
logger.error("❌ Unable to get RPC protocol instance, terminal creation failed: $extHostTerminalId")
throw IllegalStateException("RPC protocol not initialized")
}
logger.info("✅ Got RPC protocol instance: ${rpcProtocol.javaClass.simpleName}")
// Allocate numeric ID
val numericId = terminalManager.allocateNumericId()
logger.info("🔢 Allocated terminal numeric ID: $numericId")
// Create terminal instance
val terminalConfig = TerminalConfig.fromMap(config)
val terminalInstance = TerminalInstance(extHostTerminalId, numericId, project, terminalConfig, rpcProtocol)
// Initialize terminal
terminalInstance.initialize()
// Register to manager
terminalManager.registerTerminal(extHostTerminalId, terminalInstance)
logger.info("✅ Terminal created successfully: $extHostTerminalId (numericId: $numericId)")
} catch (e: Exception) {
logger.error("❌ Failed to create terminal: $extHostTerminalId", e)
// Clean up possibly created resources
terminalManager.unregisterTerminal(extHostTerminalId)
throw e
}
}
override fun dispose(id: Any) {
try {
logger.info("🧹 Destroying terminal: $id")
val terminalInstance = terminalManager.unregisterTerminal(id.toString())
if (terminalInstance != null) {
terminalInstance.dispose()
logger.info("✅ Terminal destroyed: $id")
} else {
logger.warn("Terminal does not exist: $id")
}
} catch (e: Exception) {
logger.error("❌ Failed to destroy terminal: $id", e)
}
}
override fun hide(id: Any) {
try {
logger.info("🙈 Hiding terminal: $id")
val terminalInstance = getTerminalInstance(id)
if (terminalInstance != null) {
terminalInstance.hide()
logger.info("✅ Terminal hidden: $id")
} else {
logger.warn("Terminal does not exist: $id")
}
} catch (e: Exception) {
logger.error("❌ Failed to hide terminal: $id", e)
}
}
override fun sendText(id: Any, text: String, shouldExecute: Boolean?) {
try {
logger.debug("📤 Sending text to terminal $id: $text (execute: $shouldExecute)")
val terminalInstance = getTerminalInstance(id)
if (terminalInstance != null) {
terminalInstance.sendText(text, shouldExecute ?: false)
logger.debug("✅ Text sent to terminal: $id")
} else {
logger.warn("Terminal does not exist: $id")
}
} catch (e: Exception) {
logger.error("❌ Failed to send text to terminal: $id", e)
}
}
override fun show(id: Any, preserveFocus: Boolean?) {
try {
logger.info("👁️ Showing terminal: $id (preserve focus: $preserveFocus)")
val terminalInstance = getTerminalInstance(id)
if (terminalInstance != null) {
terminalInstance.show(preserveFocus ?: true)
logger.info("✅ Terminal shown: $id")
} else {
logger.warn("Terminal does not exist: $id")
}
} catch (e: Exception) {
logger.error("❌ Failed to show terminal: $id", e)
}
}
override fun registerProcessSupport(isSupported: Boolean) {
logger.info("📋 Registering process support: $isSupported")
// In IDEA, process support is built-in, mainly used for logging state here
}
override fun registerProfileProvider(id: String, extensionIdentifier: String) {
logger.info("📋 Registering profile provider: $id (extension: $extensionIdentifier)")
// TODO: Implement profile provider registration logic
}
override fun unregisterProfileProvider(id: String) {
logger.info("📋 Unregistering profile provider: $id")
// TODO: Implement profile provider unregistration logic
}
override fun registerCompletionProvider(id: String, extensionIdentifier: String, vararg triggerCharacters: String) {
logger.info("📋 Registering completion provider: $id (extension: $extensionIdentifier, trigger characters: ${triggerCharacters.joinToString()})")
// TODO: Implement completion provider registration logic
}
override fun unregisterCompletionProvider(id: String) {
logger.info("📋 Unregistering completion provider: $id")
// TODO: Implement completion provider unregistration logic
}
override fun registerQuickFixProvider(id: String, extensionIdentifier: String) {
logger.info("📋 Registering quick fix provider: $id (extension: $extensionIdentifier)")
// TODO: Implement quick fix provider registration logic
}
override fun unregisterQuickFixProvider(id: String) {
logger.info("📋 Unregistering quick fix provider: $id")
// TODO: Implement quick fix provider unregistration logic
}
override fun setEnvironmentVariableCollection(
extensionIdentifier: String,
persistent: Boolean,
collection: Map<String, Any?>?,
descriptionMap: Map<String, Any?>
) {
logger.info("📋 Setting environment variable collection: $extensionIdentifier (persistent: $persistent)")
// TODO: Implement environment variable collection setting logic
}
override fun startSendingDataEvents() {
logger.info("📋 Starting to send data events")
// TODO: Implement data event sending logic
}
override fun stopSendingDataEvents() {
logger.info("📋 Stopping data event sending")
// TODO: Implement stopping data event sending logic
}
override fun startSendingCommandEvents() {
logger.info("📋 Starting to send command events")
// TODO: Implement command event sending logic
}
override fun stopSendingCommandEvents() {
logger.info("📋 Stopping command event sending")
// TODO: Implement stopping command event sending logic
}
override fun startLinkProvider() {
logger.info("📋 Starting link provider")
// TODO: Implement link provider startup logic
}
override fun stopLinkProvider() {
logger.info("📋 Stopping link provider")
// TODO: Implement link provider stopping logic
}
override fun sendProcessData(terminalId: Int, data: String) {
logger.debug("Send process data to terminal $terminalId")
// Send process data to terminal
}
override fun sendProcessReady(terminalId: Int, pid: Int, cwd: String, windowsPty: Map<String, Any?>?) {
logger.info("Send process ready: terminal=$terminalId, pid=$pid, cwd=$cwd")
// Send process ready information
}
override fun sendProcessProperty(terminalId: Int, property: Map<String, Any?>) {
logger.debug("📋 Sending process property: terminal=$terminalId")
// TODO: Notify extension host of process property changes
}
override fun sendProcessExit(terminalId: Int, exitCode: Int?) {
logger.info("📋 Sending process exit: terminal=$terminalId, exit code=$exitCode")
// TODO: Notify extension host of process exit
}
/**
* Get terminal instance (by string ID or numeric ID)
*/
fun getTerminalInstance(id: Any): TerminalInstance? {
return when (id) {
is String -> terminalManager.getTerminalInstance(id)
is Number -> terminalManager.getTerminalInstance(id.toInt())
else -> {
logger.warn("Unsupported ID type: ${id.javaClass.name}, attempting to convert to string")
terminalManager.getTerminalInstance(id.toString())
}
}
}
/**
* Get all terminal instances
*/
fun getAllTerminals(): Collection<TerminalInstance> {
return terminalManager.getAllTerminals()
}
override fun dispose() {
logger.info("🧹 Disposing main thread terminal service")
try {
// Cancel coroutine scope
scope.cancel()
// Terminal instance manager will automatically handle cleanup of all terminals
// No manual cleanup needed here as TerminalInstanceManager is project-level service
logger.info("✅ Main thread terminal service disposed")
} catch (e: Exception) {
logger.error("❌ Failed to dispose main thread terminal service", e)
}
}
}

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.terminal.TerminalInstanceManager
interface MainThreadTerminalShellIntegrationShape : Disposable {
fun executeCommand(terminalId: Int, commandLine: String)
}
class MainThreadTerminalShellIntegration(
private val project: Project
) : MainThreadTerminalShellIntegrationShape {
private val logger = Logger.getInstance(MainThreadTerminalShellIntegration::class.java)
private val terminalManager = project.service<TerminalInstanceManager>()
override fun executeCommand(terminalId: Int, commandLine: String) {
logger.info("🚀 Executing Shell Integration command: terminalId=$terminalId, commandLine='$commandLine'")
try {
// Get terminal instance by numeric ID
val terminalInstance = terminalManager.getTerminalInstance(terminalId)
if (terminalInstance == null) {
logger.warn("❌ Terminal instance not found: terminalId=$terminalId")
return
}
logger.info("✅ Found terminal instance: ${terminalInstance.extHostTerminalId}")
// Execute command in terminal
terminalInstance.sendText(commandLine, shouldExecute = true)
logger.info("✅ Command sent to terminal: terminalId=$terminalId, command='$commandLine'")
} catch (e: Exception) {
logger.error("❌ Failed to execute Shell Integration command: terminalId=$terminalId, command='$commandLine'", e)
}
}
override fun dispose() {
logger.info("🧹 Destroying MainThreadTerminalShellIntegration")
}
}

View file

@ -0,0 +1,223 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import ai.roocode.jetbrains.editor.EditorAndDocManager
import ai.roocode.jetbrains.editor.Range
import ai.roocode.jetbrains.editor.createURI
import kotlinx.coroutines.delay
import java.io.File
/**
* Main thread text editor interface
*/
interface MainThreadTextEditorsShape : Disposable {
/**
* Try to show text document
* @param resource Resource URI
* @param options Display options
* @return Editor ID or null
*/
suspend fun tryShowTextDocument(resource: Map<String,Any?>, options: Any?): Any?
/**
* Register text editor decoration type
* @param extensionId Extension ID
* @param key Decoration type key
* @param options Decoration rendering options
*/
fun registerTextEditorDecorationType(extensionId: Map<String, String>, key: String, options: Any)
/**
* Remove text editor decoration type
* @param key Decoration type key
*/
fun removeTextEditorDecorationType(key: String)
/**
* Try to show editor
* @param id Editor ID
* @param position Position
* @return Operation result
*/
fun tryShowEditor(id: String, position: Any?): Any
/**
* Try to hide editor
* @param id Editor ID
* @return Operation result
*/
fun tryHideEditor(id: String): Any
/**
* Try to set options
* @param id Editor ID
* @param options Configuration updates
* @return Operation result
*/
fun trySetOptions(id: String, options: Any): Any
/**
* Try to set decorations
* @param id Editor ID
* @param key Decoration type key
* @param ranges Decoration ranges
* @return Operation result
*/
fun trySetDecorations(id: String, key: String, ranges: List<Any>): Any
/**
* Try to quickly set decorations
* @param id Editor ID
* @param key Decoration type key
* @param ranges Decoration ranges array
* @return Operation result
*/
fun trySetDecorationsFast(id: String, key: String, ranges: List<Any>): Any
/**
* Try to reveal range
* @param id Editor ID
* @param range Display range
* @param revealType Display type
* @return Operation result
*/
fun tryRevealRange(id: String, range: Map<String,Any?>, revealType: Int): Any
/**
* Try to set selections
* @param id Editor ID
* @param selections Selections array
* @return Operation result
*/
fun trySetSelections(id: String, selections: List<Any>): Any
/**
* Try to apply edits
* @param id Editor ID
* @param modelVersionId Model version ID
* @param edits Edit operations
* @param opts Apply options
* @return Whether successful
*/
fun tryApplyEdits(id: String, modelVersionId: Int, edits: List<Any>, opts: Any?): Boolean
/**
* Try to insert snippet
* @param id Editor ID
* @param modelVersionId Model version ID
* @param template Code snippet template
* @param selections Selection ranges
* @param opts Undo options
* @return Whether successful
*/
fun tryInsertSnippet(id: String, modelVersionId: Int, template: String, selections: List<Any>, opts: Any?): Boolean
/**
* Get diff information
* @param id Editor ID
* @return Diff information
*/
fun getDiffInformation(id: String): Any?
}
/**
* Main thread text editor implementation
*/
class MainThreadTextEditors(var project: Project) : MainThreadTextEditorsShape {
private val logger = Logger.getInstance(MainThreadTextEditors::class.java)
override suspend fun tryShowTextDocument(resource: Map<String, Any?>, options: Any?): Any? {
logger.info("Trying to show text document: resource=$resource, options=$options")
val path = resource["path"] as String? ?: ""
val vfs = LocalFileSystem.getInstance()
vfs.refreshIoFiles(listOf(File(path)))
val resourceURI = createURI(resource)
val editorHandle = project.getService(EditorAndDocManager::class.java).openEditor(resourceURI)
logger.info("Trying to show text document: resource=$resource execution completed" )
return editorHandle.id
}
override fun registerTextEditorDecorationType(extensionId: Map<String, String>, key: String, options: Any) {
logger.info("Registering text editor decoration type: extensionId=$extensionId, key=$key, options=$options")
}
override fun removeTextEditorDecorationType(key: String) {
logger.info("Removing text editor decoration type: $key")
}
override fun tryShowEditor(id: String, position: Any?): Any {
logger.info("Trying to show editor: id=$id, position=$position")
return Unit
}
override fun tryHideEditor(id: String): Any {
logger.info("Trying to hide editor: $id")
return Unit
}
override fun trySetOptions(id: String, options: Any): Any {
logger.info("Try to set options: id=$id, options=$options")
return Unit
}
override fun trySetDecorations(id: String, key: String, ranges: List<Any>): Any {
logger.info("Try to set decorations: id=$id, key=$key, ranges=${ranges.size}")
return Unit
}
override fun trySetDecorationsFast(id: String, key: String, ranges: List<Any>): Any {
logger.info("Try to quickly set decorations: id=$id, key=$key, ranges=${ranges.size}")
return Unit
}
override fun tryRevealRange(id: String, range: Map<String,Any?>, revealType: Int): Any {
logger.info("Try to reveal range: id=$id, range=$range, revealType=$revealType")
val handle = project.getService(EditorAndDocManager::class.java).getEditorHandleById(id)
handle?.let {
val rang = createRanges(range)
handle.revealRange(rang)
}
return Unit
}
private fun createRanges(range: Map<String,Any?>): Range {
val startLineNumber = (range["startLineNumber"] as? Number)?.toInt() ?: 0
val startColumn = (range["startColumn"] as? Number)?.toInt() ?: 0
val endLineNumber = (range["endLineNumber"] as? Number)?.toInt() ?: startLineNumber
val endColumn = (range["endColumn"] as? Number)?.toInt() ?: startColumn
return Range(startLineNumber, startColumn, endLineNumber, endColumn)
}
override fun trySetSelections(id: String, selections: List<Any>): Any {
logger.info("Try to set selections: id=$id, selections=$selections")
return Unit
}
override fun tryApplyEdits(id: String, modelVersionId: Int, edits: List<Any>, opts: Any?): Boolean {
logger.info("Try to apply edits: id=$id, modelVersionId=$modelVersionId, edits=$edits, opts=$opts")
return true
}
override fun tryInsertSnippet(id: String, modelVersionId: Int, template: String, selections: List<Any>, opts: Any?): Boolean {
logger.info("Try to insert snippet: id=$id, modelVersionId=$modelVersionId, template=$template, selections=$selections, opts=$opts")
return true
}
override fun getDiffInformation(id: String): Any? {
logger.info("Get diff information: $id")
return null
}
override fun dispose() {
logger.info("Dispose MainThreadTextEditors")
}
}

View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import kotlinx.coroutines.CompletableDeferred
/**
* URL handling related interface
*/
interface MainThreadUrlsShape : Disposable {
/**
* Register URI handler
* @param handle Handler identifier
* @param extensionId Extension ID
* @param extensionDisplayName Extension display name
* @return Execution result
*/
suspend fun registerUriHandler(handle: Int, extensionId: Map<String, String>, extensionDisplayName: String): Any
/**
* Unregister URI handler
* @param handle Handler identifier
* @return Execution result
*/
suspend fun unregisterUriHandler(handle: Int): Any
/**
* Create application URI
* @param uri URI components
* @return Created URI components
*/
suspend fun createAppUri(uri: Map<String, Any?>): Map<String, Any?>
}
class MainThreadUrls : MainThreadUrlsShape {
private val logger = Logger.getInstance(MainThreadUrls::class.java)
override suspend fun registerUriHandler(handle: Int, extensionId: Map<String, String>, extensionDisplayName: String): Any {
logger.info("Registering URI handler: handle=$handle, extensionId=$extensionId, displayName=$extensionDisplayName")
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
}
override suspend fun unregisterUriHandler(handle: Int): Any {
logger.info("Unregistering URI handler: handle=$handle")
return CompletableDeferred<Unit>().also { it.complete(Unit) }.await()
}
override suspend fun createAppUri(uri: Map<String, Any?>): Map<String, Any?> {
logger.info("Creating application URI: uri=$uri")
// Simply return original URI
return uri
}
override fun dispose() {
logger.info("Disposing MainThreadUrls resources")
}
}

View file

@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import ai.roocode.jetbrains.events.WebviewViewProviderData
import ai.roocode.jetbrains.webview.WebViewManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/**
* Webview view related interface
*/
interface MainThreadWebviewViewsShape : Disposable {
/**
* Register Webview view provider
* @param extension Webview extension description
* @param viewType View type
* @param options Option configuration
*/
fun registerWebviewViewProvider(
extension: Map<String, Any?>,
viewType: String,
options: Map<String, Any?>
)
/**
* Unregister Webview view provider
* @param viewType View type
*/
fun unregisterWebviewViewProvider(viewType: String)
/**
* Set Webview view title
* @param handle Webview handle
* @param value Title value
*/
fun setWebviewViewTitle(handle: String, value: String?)
/**
* Set Webview view description
* @param handle Webview handle
* @param value Description content
*/
fun setWebviewViewDescription(handle: String, value: String?)
/**
* Set Webview view badge
* @param handle Webview handle
* @param badge Badge information
*/
fun setWebviewViewBadge(handle: String, badge: Map<String, Any?>?)
/**
* Show Webview view
* @param handle Webview handle
* @param preserveFocus Whether to preserve focus
*/
fun show(handle: String, preserveFocus: Boolean)
}
class MainThreadWebviewViews(val project: Project) : MainThreadWebviewViewsShape {
private val logger = Logger.getInstance(MainThreadWebviewViews::class.java)
private val coroutineScope = CoroutineScope(Dispatchers.Default)
override fun registerWebviewViewProvider(
extension: Map<String, Any?>,
viewType: String,
options: Map<String, Any?>
) {
logger.info("Registering Webview view provider: viewType=$viewType, options=$options")
// Use EventBus to send WebView view provider registration event, using IntelliJ platform compatible method
// project.getService(ProjectEventBus::class.java).emitInApplication(
// WebviewViewProviderRegisterEvent,
// WebviewViewProviderData(extension, viewType, options)
// )
project.getService(WebViewManager::class.java).registerProvider(WebviewViewProviderData(extension, viewType, options))
}
override fun unregisterWebviewViewProvider(viewType: String) {
logger.info("Unregistering Webview view provider: viewType=$viewType")
}
override fun setWebviewViewTitle(handle: String, value: String?) {
logger.info("Setting Webview view title: handle=$handle, title=$value")
}
override fun setWebviewViewDescription(handle: String, value: String?) {
logger.info("Setting Webview view description: handle=$handle, description=$value")
}
override fun setWebviewViewBadge(handle: String, badge: Map<String, Any?>?) {
logger.info("Setting Webview view badge: handle=$handle, badge=$badge")
}
override fun show(handle: String, preserveFocus: Boolean) {
logger.info("Showing Webview view: handle=$handle, preserveFocus=$preserveFocus")
}
override fun dispose() {
logger.info("Disposing MainThreadWebviewViews resources")
}
}

View file

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import ai.roocode.jetbrains.events.EventBus
import ai.roocode.jetbrains.events.ProjectEventBus
import ai.roocode.jetbrains.events.WebviewHtmlUpdateData
import ai.roocode.jetbrains.events.WebviewHtmlUpdateEvent
import ai.roocode.jetbrains.webview.WebViewManager
import java.util.concurrent.ConcurrentHashMap
/**
* Webview handle type
* Corresponds to WebviewHandle type in TypeScript
*/
typealias WebviewHandle = String
/**
* Main thread Webviews service interface
* Corresponds to MainThreadWebviewsShape interface in VSCode
*/
interface MainThreadWebviewsShape : Disposable {
/**
* Set HTML content
* Corresponds to $setHtml method in TypeScript interface
* @param handle Webview handle
* @param value HTML content
*/
fun setHtml(handle: WebviewHandle, value: String)
/**
* Set Webview options
* Corresponds to $setOptions method in TypeScript interface
* @param handle Webview handle
* @param options Webview content options
*/
fun setOptions(handle: WebviewHandle, options: Map<String, Any?>)
/**
* Send message to Webview
* Corresponds to $postMessage method in TypeScript interface
* @param handle Webview handle
* @param value Message content
* @param buffers Binary buffer array
* @return Whether operation succeeded
*/
fun postMessage(handle: WebviewHandle, value: String): Boolean
}
/**
* Main thread Webviews service implementation class
*/
class MainThreadWebviews(val project: Project) : MainThreadWebviewsShape {
private val logger = Logger.getInstance(MainThreadWebviews::class.java)
// Store registered Webviews
private val webviews = ConcurrentHashMap<WebviewHandle, Any?>()
private var webviewHandle : WebviewHandle = ""
override fun setHtml(handle: WebviewHandle, value: String) {
logger.info("Setting Webview HTML: handle=$handle, length=${value.length}")
webviewHandle = handle
try {
// Replace vscode-file protocol format, using regex to match from vscode-file:/ to /roocode/ part
val modifiedHtml = value.replace(Regex("vscode-file:/.*?/roocode/"), "/")
logger.info("Replaced vscode-file protocol path format")
// Send HTML content update event through EventBus
val data = WebviewHtmlUpdateData(handle, modifiedHtml)
// project.getService(ProjectEventBus::class.java).emitInApplication(WebviewHtmlUpdateEvent, data)
project.getService(WebViewManager::class.java).updateWebViewHtml(data)
logger.info("Sent HTML content update event: handle=$handle")
} catch (e: Exception) {
logger.error("Failed to set Webview HTML", e)
}
}
override fun setOptions(handle: WebviewHandle, options: Map<String, Any?>) {
logger.info("Setting Webview options: handle=$handle, options=$options")
webviewHandle = handle
try {
// Actual implementation should set options for Webview component on IDEA platform
// Here we just log
} catch (e: Exception) {
logger.error("Failed to set Webview options: $e")
}
}
override fun postMessage(handle: WebviewHandle, value: String): Boolean {
// logger.info("Sending message to Webview: handle=$handle")
if(value.contains("theme")) {
logger.info("Sending theme message to Webview")
}
return try {
val mangler = project.getService(WebViewManager::class.java)
// mangler.getWebView(handle)?.postMessageToWebView(value)
mangler.getLatestWebView()?.postMessageToWebView(value)
true
} catch (e: Exception) {
logger.error("Failed to send message to Webview: $e")
false
}
}
override fun dispose() {
logger.info("Disposing MainThreadWebviews resources")
webviews.clear()
}
}

View file

@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.actors
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.WindowManager
import ai.roocode.jetbrains.plugin.SystemObjectProvider
import ai.roocode.jetbrains.plugin.WecoderPluginService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.future
import java.awt.Desktop
import java.net.URI
import java.util.concurrent.CompletableFuture
/**
* Main thread window service interface
* Corresponds to MainThreadWindowShape interface in VSCode
*/
interface MainThreadWindowShape : Disposable {
/**
* Get initial state
* @return Initial window state including focus and active status
*/
fun getInitialState(): Map<String, Boolean>
/**
* Open URI
* @param uri URI component
* @param uriString URI string
* @param options Open options
* @return Whether successfully opened
*/
fun openUri(uri: Map<String, Any?>, uriString: String?, options: Map<String, Any?>): Boolean
/**
* Convert to external URI
* @param uri URI component
* @param options Open options
* @return External URI component
*/
fun asExternalUri(uri: Map<String, Any?>, options: Map<String, Any?>): Map<String, Any?>
}
/**
* Main thread window service implementation
* Provides IDEA platform window related functionality
*/
class MainThreadWindow(val project: Project) : MainThreadWindowShape {
private val logger = Logger.getInstance(MainThreadWindow::class.java)
override fun getInitialState(): Map<String, Boolean> {
try {
logger.info("Getting window initial state")
if (project != null) {
// Get current project window state
val frame = WindowManager.getInstance().getFrame(project)
val isFocused = frame?.isFocused ?: false
val isActive = frame?.isActive ?: false
return mapOf(
"isFocused" to isFocused,
"isActive" to isActive
)
} else {
logger.warn("Cannot get current project, returning default window state")
return mapOf(
"isFocused" to false,
"isActive" to false
)
}
} catch (e: Exception) {
logger.error("Failed to get window initial state", e)
return mapOf(
"isFocused" to false,
"isActive" to false
)
}
}
override fun openUri(uri: Map<String, Any?>, uriString: String?, options: Map<String, Any?>): Boolean {
try {
logger.info("Opening URI: $uriString")
// Try to get URI
val actualUri = if (uriString != null) {
try {
URI(uriString)
} catch (e: Exception) {
// If URI string is invalid, try to build from URI components
createUriFromComponents(uri)
}
} else {
createUriFromComponents(uri)
}
return if (actualUri != null) {
// Check if Desktop operation is supported
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(actualUri)
true
} else {
logger.warn("System does not support opening URI")
false
}
} else {
logger.warn("Cannot create valid URI")
false
}
} catch (e: Exception) {
logger.error("Failed to open URI", e)
return false
}
}
override fun asExternalUri(uri: Map<String, Any?>, options: Map<String, Any?>): Map<String, Any?> {
return try {
logger.info("Converting to external URI: $uri")
// For most cases, we directly return the same URI components
// Actual implementation may need to handle specific protocol conversion
uri
} catch (e: Exception) {
logger.error("Failed to convert to external URI", e)
uri // Return original URI on error
}
}
/**
* Create URI from URI components
* @param components URI components
* @return Created URI or null
*/
private fun createUriFromComponents(components: Map<String, Any?>): URI? {
return try {
val scheme = components["scheme"] as? String ?: return null
val authority = components["authority"] as? String ?: ""
val path = components["path"] as? String ?: ""
val query = components["query"] as? String ?: ""
val fragment = components["fragment"] as? String ?: ""
URI(scheme, authority, path, query, fragment)
} catch (e: Exception) {
logger.warn("Failed to create URI from components: $components", e)
null
}
}
override fun dispose() {
logger.info("Disposing MainThreadWindow")
}
}

View file

@ -0,0 +1,143 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.commands
import com.intellij.openapi.project.Project
/**
* Interface representing a command in the system.
* Commands are used to define executable actions that can be registered and invoked.
*/
interface ICommand {
/**
* Gets the unique identifier for this command.
* @return The command ID as a string
*/
fun getId(): String
/**
* Gets the method name that should be invoked when this command is executed.
* @return The method name as a string
*/
fun getMethod(): String
/**
* Gets the handler object that contains the method to be invoked.
* @return The handler object
*/
fun handler(): Any
/**
* Gets the return type of the command, if any.
* @return The return type as a string, or null if the command doesn't return a value
*/
fun returns(): String?
}
/**
* Interface for a registry that manages commands.
* Provides functionality to register, retrieve, and manage commands in the system.
*/
interface ICommandRegistry {
/**
* Called when a command is registered.
* @param id The ID of the registered command
*/
fun onDidRegisterCommand(id: String)
/**
* Registers a command in the registry.
* @param command The command to register
*/
fun registerCommand(command: ICommand)
/**
* Registers an alias for an existing command.
* @param oldId The ID of the existing command
* @param newId The new alias ID for the command
*/
fun registerCommandAlias(oldId: String, newId: String)
/**
* Gets a command by its ID.
* @param id The ID of the command to retrieve
* @return The command, or null if not found
*/
fun getCommand(id: String): ICommand?
/**
* Gets all registered commands.
* @return A map of command IDs to commands
*/
fun getCommands(): Map<String, ICommand>
}
/**
* Implementation of the ICommandRegistry interface.
* Manages commands for a specific project.
*
* @property project The project context for this command registry
*/
class CommandRegistry(val project: Project) : ICommandRegistry {
/**
* Map of command IDs to lists of commands.
* Using a list allows for potential command overloading in the future.
*/
private val commands = mutableMapOf<String, MutableList<ICommand>>()
/**
* Called when a command is registered.
* Currently not implemented.
*
* @param id The ID of the registered command
*/
override fun onDidRegisterCommand(id: String) {
TODO("Not yet implemented")
}
/**
* Registers a command in the registry.
*
* @param command The command to register
*/
override fun registerCommand(command: ICommand) {
commands.put(command.getId(), mutableListOf(command))
}
/**
* Registers an alias for an existing command.
* If the original command exists, creates a new entry with the new ID pointing to the same command.
*
* @param oldId The ID of the existing command
* @param newId The new alias ID for the command
*/
override fun registerCommandAlias(oldId: String, newId: String) {
getCommand(oldId)?.let {
commands.put(newId, mutableListOf(it))
}
}
/**
* Gets a command by its ID.
* Returns the first command registered with the given ID, or null if not found.
*
* @param id The ID of the command to retrieve
* @return The command, or null if not found
*/
override fun getCommand(id: String): ICommand? {
return commands[id]?.firstOrNull()
}
/**
* Gets all registered commands.
* Returns a map of command IDs to the first command registered with each ID.
*
* @return A map of command IDs to commands
*/
override fun getCommands(): Map<String, ICommand> {
return commands.mapValues { it.value.first() }
}
}

View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.commands
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.JBProtocolCommand
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import ai.roocode.jetbrains.core.PluginContext
import ai.roocode.jetbrains.core.ServiceProxyRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
/**
* JetBrains Protocol Command for handling Roo Code authentication URLs
*
* Handles URLs in the format: jetbrains://idea/ai.roocode.jetbrains.auth?token=HERE
* and forwards them to the VSCode extension via RPC protocol
*/
class KiloCodeAuthProtocolCommand : JBProtocolCommand("ai.roocode.jetbrains.auth") {
private val logger = Logger.getInstance(KiloCodeAuthProtocolCommand::class.java)
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
companion object {
const val COMMAND_ID = "ai.roocode.jetbrains.auth"
const val TOKEN_PARAM = "token"
}
/**
* Public method for testing the protocol command execution
* @param target The target parameter from the URL
* @param parameters Map of URL parameters
* @param fragment The URL fragment
* @return null on success, error message on failure
*/
suspend fun executeForTesting(target: String?, parameters: Map<String, String>, fragment: String?): String? {
return execute(target, parameters, fragment)
}
/**
* Handle the protocol command
* @param target The target parameter from the URL
* @param parameters Map of URL parameters
* @param fragment The URL fragment
* @return null on success, error message on failure
*/
override suspend fun execute(target: String?, parameters: Map<String, String>, fragment: String?): String? {
logger.info("Handling Roo Code auth protocol command: target=$target, parameters=$parameters")
return try {
// Extract token from parameters
val token = parameters[TOKEN_PARAM]
if (token.isNullOrBlank()) {
val errorMsg = "No token found in parameters: $parameters"
logger.warn(errorMsg)
return errorMsg
}
logger.info("Extracted token from parameters, forwarding to VSCode extension")
// Forward to VSCode extension via RPC
forwardTokenToVSCodeExtension(token)
null // Success
} catch (e: Exception) {
val errorMsg = "Error handling Roo Code auth protocol command: ${e.message}"
logger.error(errorMsg, e)
errorMsg
}
}
/**
* Forward the token to the VSCode extension by simulating a VSCode URL handler call
*/
private fun forwardTokenToVSCodeExtension(token: String) {
coroutineScope.launch {
try {
// Get the current project (or default project if none is open)
val project = getCurrentProject()
if (project == null) {
logger.warn("No project available to forward token")
return@launch
}
// Get RPC protocol instance
val protocol = project.getService(PluginContext::class.java)?.getRPCProtocol()
if (protocol == null) {
logger.error("Cannot get RPC protocol instance, cannot forward token")
return@launch
}
logger.info("Forwarding token to VSCode extension via RPC")
// Use ExtHostCommands to execute a command that handles the URL
val extHostCommands = protocol.getProxy(ServiceProxyRegistry.ExtHostContext.ExtHostCommands)
// Create the VSCode URI string that would normally be handled by handleUri
val vscodeUriString = "vscode://roocode.kilo-code/roocode?token=${token}"
// Execute a command to handle the URI - this simulates what happens when VSCode receives a URL
// We'll use a special command that the VSCode extension can handle
extHostCommands.executeContributedCommand(
"kilo-code.handleExternalUri",
listOf(vscodeUriString)
)
logger.info("Successfully forwarded token to VSCode extension via command execution")
} catch (e: Exception) {
logger.error("Error forwarding token to VSCode extension", e)
}
}
}
/**
* Get the current project, preferring the focused project
*/
private fun getCurrentProject(): Project? {
return try {
val projectManager = ProjectManager.getInstance()
// Try to get the default project first
val openProjects = projectManager.openProjects
if (openProjects.isNotEmpty()) {
// Return the first open project
openProjects[0]
} else {
// Fallback to default project
projectManager.defaultProject
}
} catch (e: Exception) {
logger.warn("Error getting current project", e)
null
}
}
}

View file

@ -0,0 +1,354 @@
// Copyright 2009-2025 Weibo, Inc.
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0
package ai.roocode.jetbrains.core
import com.google.gson.Gson
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.application.ApplicationInfo
import ai.roocode.jetbrains.editor.EditorAndDocManager
import ai.roocode.jetbrains.ipc.NodeSocket
import ai.roocode.jetbrains.ipc.PersistentProtocol
import ai.roocode.jetbrains.ipc.proxy.ResponsiveState
import ai.roocode.jetbrains.util.PluginConstants
import ai.roocode.jetbrains.util.PluginResourceUtil
import ai.roocode.jetbrains.util.URI
import ai.roocode.jetbrains.workspace.WorkspaceFileChangeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.net.Socket
import java.nio.channels.SocketChannel
import java.nio.file.Paths
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.PluginId
/**
* Extension host manager, responsible for communication with extension processes.
* Handles Ready and Initialized messages from extension processes.
*/
class ExtensionHostManager : Disposable {
companion object {
val LOG = Logger.getInstance(ExtensionHostManager::class.java)
}
private val project: Project
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Communication protocol
private var nodeSocket: NodeSocket
private var protocol: PersistentProtocol? = null
// RPC manager
private var rpcManager: RPCManager? = null
// Extension manager
private var extensionManager: ExtensionManager? = null
// Plugin identifier
private var rooCodeIdentifier: String? = null
// JSON serialization
private val gson = Gson()
// Last diagnostic log time
private var lastDiagnosticLogTime = 0L
private var projectPath: String? = null
// Support Socket constructor
constructor(clientSocket: Socket, projectPath: String,project: Project) {
clientSocket.tcpNoDelay = true
this.nodeSocket = NodeSocket(clientSocket, "extension-host")
this.projectPath = projectPath
this.project = project
}
// Support SocketChannel constructor
constructor(clientChannel: SocketChannel, projectPath: String , project: Project) {
this.nodeSocket = NodeSocket(clientChannel, "extension-host")
this.projectPath = projectPath
this.project = project
}
/**
* Start communication with the extension process.
*/
fun start() {
try {
// Initialize extension manager
extensionManager = ExtensionManager()
val extensionPath = PluginResourceUtil.getResourcePath(PluginConstants.PLUGIN_ID, PluginConstants.PLUGIN_CODE_DIR)
rooCodeIdentifier = extensionPath?.let { extensionManager!!.registerExtension(it).identifier.value }
// Create protocol
protocol = PersistentProtocol(
PersistentProtocol.PersistentProtocolOptions(
socket = nodeSocket,
initialChunk = null,
loadEstimator = null,
sendKeepAlive = true
),
this::handleMessage
)
LOG.info("ExtensionHostManager started successfully")
} catch (e: Exception) {
LOG.error("Failed to start ExtensionHostManager", e)
dispose()
}
}
/**
* Get RPC responsive state.
* @return Responsive state, or null if RPC manager is not initialized.
*/
fun getResponsiveState(): ResponsiveState? {
val currentTime = System.currentTimeMillis()
// Limit diagnostic log frequency, at most once every 60 seconds
val shouldLogDiagnostics = currentTime - lastDiagnosticLogTime > 60000
if (rpcManager == null) {
if (shouldLogDiagnostics) {
LOG.debug("Unable to get responsive state: RPC manager is not initialized")
lastDiagnosticLogTime = currentTime
}
return null
}
// Log connection diagnostic information
if (shouldLogDiagnostics) {
val socketInfo = buildString {
append("NodeSocket: ")
append(if (nodeSocket.isClosed()) "closed" else "active")
append(", input stream: ")
append(if (nodeSocket.isInputClosed()) "closed" else "normal")
append(", output stream: ")
append(if (nodeSocket.isOutputClosed()) "closed" else "normal")
append(", disposed=")
append(nodeSocket.isDisposed())
}
val protocolInfo = protocol?.let { proto ->
"Protocol: ${if (proto.isDisposed()) "disposed" else "active"}"
} ?: "Protocol is null"
LOG.debug("Connection diagnostics: $socketInfo, $protocolInfo")
lastDiagnosticLogTime = currentTime
}
return rpcManager?.getRPCProtocol()?.responsiveState
}
/**
* Handle messages from the extension process.
*/
private fun handleMessage(data: ByteArray) {
// Check if data is a single-byte message (extension host protocol message)
if (data.size == 1) {
// Try to parse as extension host message type
when (ExtensionHostMessageType.fromData(data)) {
ExtensionHostMessageType.Ready -> handleReadyMessage()
ExtensionHostMessageType.Initialized -> handleInitializedMessage()
ExtensionHostMessageType.Terminate -> LOG.info("Received Terminate message")
null -> LOG.debug("Received unknown message type: ${data.contentToString()}")
}
} else {
LOG.debug("Received message with length ${data.size}, not handling as extension host message")
}
}
/**
* Handle Ready message, send initialization data.
*/
private fun handleReadyMessage() {
LOG.info("Received Ready message from extension host")
try {
// Build initialization data
val initData = createInitData()
// Send initialization data
val jsonData = gson.toJson(initData).toByteArray()
protocol?.send(jsonData)
LOG.info("Sent initialization data to extension host")
} catch (e: Exception) {
LOG.error("Failed to handle Ready message", e)
}
}
/**
* Handle Initialized message, create RPC manager and activate plugin.
*/
private fun handleInitializedMessage() {
LOG.info("Received Initialized message from extension host")
try {
// Get protocol
val protocol = this.protocol ?: throw IllegalStateException("Protocol is not initialized")
val extensionManager = this.extensionManager ?: throw IllegalStateException("ExtensionManager is not initialized")
// Create RPC manager
rpcManager = RPCManager(protocol, extensionManager,null, project)
// Start initialization process
rpcManager?.startInitialize()
// Start file monitoring
project.getService(WorkspaceFileChangeManager::class.java)
// WorkspaceFileChangeManager.getInstance()
project.getService(EditorAndDocManager::class.java).initCurrentIdeaEditor()
// Activate RooCode plugin
val rooCodeId = rooCodeIdentifier ?: throw IllegalStateException("RooCode identifier is not initialized")
extensionManager.activateExtension(rooCodeId, rpcManager!!.getRPCProtocol())
.whenComplete { _, error ->
if (error != null) {
LOG.error("Failed to activate RooCode plugin", error)
} else {
LOG.info("RooCode plugin activated successfully")
}
}
LOG.info("Initialized extension host")
} catch (e: Exception) {
LOG.error("Failed to handle Initialized message", e)
}
}
/**
* Create initialization data.
* Corresponds to the initData object in main.js.
*/
private fun createInitData(): Map<String, Any?> {
val pluginDir = getPluginDir()
val basePath = projectPath
return mapOf(
"commit" to "development",
"version" to getIDEVersion(),
"quality" to null,
"parentPid" to ProcessHandle.current().pid(),
"environment" to mapOf(
"isExtensionDevelopmentDebug" to false,
"appName" to getCurrentIDEName(),
"appHost" to "node",
"appLanguage" to "en",
"appUriScheme" to "vscode",
"appRoot" to uriFromPath(pluginDir),
"globalStorageHome" to uriFromPath(Paths.get(System.getProperty("user.home"),".roocode", "globalStorage").toString()),
"workspaceStorageHome" to uriFromPath(Paths.get(System.getProperty("user.home"),".roocode", "workspaceStorage").toString()),
"extensionDevelopmentLocationURI" to null,
"extensionTestsLocationURI" to null,
"useHostProxy" to false,
"skipWorkspaceStorageLock" to false,
"isExtensionTelemetryLoggingOnly" to false,
),
"workspace" to mapOf(
"id" to "intellij-workspace",
"name" to "IntelliJ Workspace",
"transient" to false,
"configuration" to null,
"isUntitled" to false
),
"remote" to mapOf(
"authority" to null,
"connectionData" to null,
"isRemote" to false
),
"extensions" to mapOf<String, Any>(
"versionId" to 1,
"allExtensions" to (extensionManager?.getAllExtensionDescriptions() ?: emptyList<Any>()),
"myExtensions" to (extensionManager?.getAllExtensionDescriptions()?.map { it.identifier } ?: emptyList<Any>()),
"activationEvents" to (extensionManager?.getAllExtensionDescriptions()?.associate { ext ->
ext.identifier.value to (ext.activationEvents ?: emptyList<String>())
} ?: emptyMap())
),
"telemetryInfo" to mapOf(
"sessionId" to "intellij-session",
"machineId" to "intellij-machine",
"sqmId" to "",
"devDeviceId" to "",
"firstSessionDate" to java.time.Instant.now().toString(),
"msftInternal" to false
),
"logLevel" to 0, // Info level
"loggers" to emptyList<Any>(),
"logsLocation" to uriFromPath(Paths.get(pluginDir, "logs").toString()),
"autoStart" to true,
"consoleForward" to mapOf(
"includeStack" to false,
"logNative" to false
),
"uiKind" to 1 // Desktop
)
}
/**
* Get current IDE name.
*/
private fun getCurrentIDEName(): String {
val applicationInfo = ApplicationInfo.getInstance()
val productCode = applicationInfo.build.productCode
val version = applicationInfo.shortVersion ?: "1.0.0"
// Return in the format: wrapper|jetbrains|productCode
val result = "wrapper|jetbrains|$productCode|$version"
return result
}
/**
* Get current IDE version.
*/
private fun getIDEVersion(): String {
val applicationInfo = ApplicationInfo.getInstance()
val version = applicationInfo.shortVersion ?: "1.0.0"
LOG.info("Get IDE version: $version")
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(PluginConstants.PLUGIN_ID))?.version
if (pluginVersion != null) {
val fullVersion = "$version, $pluginVersion"
LOG.info("Get IDE version and plugin version: $fullVersion")
return fullVersion
}
return version
}
/**
* Get plugin directory.
*/
private fun getPluginDir(): String {
return PluginResourceUtil.getResourcePath(PluginConstants.PLUGIN_ID, "")
?: throw IllegalStateException("Unable to get plugin directory")
}
/**
* Create URI object.
*/
private fun uriFromPath(path: String): URI {
return URI.file(path)
}
/**
* Resource disposal.
*/
override fun dispose() {
LOG.info("Disposing ExtensionHostManager")
// Cancel coroutines
coroutineScope.cancel()
// Release RPC manager
rpcManager = null
// Release protocol
protocol?.dispose()
protocol = null
// Release socket
nodeSocket.dispose()
LOG.info("ExtensionHostManager disposed")
}
}

Some files were not shown because too many files have changed in this diff Show more