diff --git a/proto/build-proto-config.js b/proto/build-proto-config.js new file mode 100644 index 0000000000..66bfc107fc --- /dev/null +++ b/proto/build-proto-config.js @@ -0,0 +1,29 @@ +// Configuration file for protocol buffer build scripts +// Contains service name mappings used by both build-proto.js and build-go-proto.js + +// List of gRPC services +// To add a new service, simply add it to this map and run the build scripts +// The service handler will be automatically discovered and used by grpc-handler.ts +export const serviceNameMap = { + account: "cline.AccountService", + browser: "cline.BrowserService", + checkpoints: "cline.CheckpointsService", + file: "cline.FileService", + mcp: "cline.McpService", + state: "cline.StateService", + task: "cline.TaskService", + web: "cline.WebService", + models: "cline.ModelsService", + slash: "cline.SlashService", + ui: "cline.UiService", + // Add new services here - no other code changes needed! +} + +// List of host gRPC services (IDE API bridge) +// These services are implemented in the IDE extension and called by the standalone Cline Core +export const hostServiceNameMap = { + uri: "host.UriService", + watch: "host.WatchService", + workspace: "host.WorkspaceService", + // Add new host services here +} diff --git a/proto/build-proto.js b/proto/build-proto.js index 04a42928b9..3416b071cb 100755 --- a/proto/build-proto.js +++ b/proto/build-proto.js @@ -9,16 +9,18 @@ import chalk from "chalk" import os from "os" import { createRequire } from "module" +import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js" + const require = createRequire(import.meta.url) const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc") const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)) const ROOT_DIR = path.resolve(SCRIPT_DIR, "..") -const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto") -const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js") -const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc") -const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto") +const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto") +const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js") +const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc") +const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto") const isWindows = process.platform === "win32" const TS_PROTO_PLUGIN = isWindows @@ -34,34 +36,13 @@ const TS_PROTO_OPTIONS = [ "useDate=false", // Timestamp fields will not be automatically converted to Date. ] -// List of gRPC services -// To add a new service, simply add it to this map and run this script -// The service handler will be automatically discovered and used by grpc-handler.ts -const serviceNameMap = { - account: "cline.AccountService", - browser: "cline.BrowserService", - checkpoints: "cline.CheckpointsService", - file: "cline.FileService", - mcp: "cline.McpService", - state: "cline.StateService", - task: "cline.TaskService", - web: "cline.WebService", - models: "cline.ModelsService", - slash: "cline.SlashService", - ui: "cline.UiService", - // Add new services here - no other code changes needed! -} -const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey)) +// Service directories derived from imported serviceNameMap +const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey)) -// List of host gRPC services (IDE API bridge) -// These services are implemented in the IDE extension and called by the standalone Cline Core -const hostServiceNameMap = { - uri: "host.UriService", - watch: "host.WatchService", - workspace: "host.WorkspaceService", - // Add new host services here -} -const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey)) +// Host service directories derived from imported hostServiceNameMap +const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => + path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey), +) async function main() { console.log(chalk.bold.blue("Starting Protocol Buffer code generation...")) @@ -177,7 +158,7 @@ export { ${serviceExports.join(",\n\t")} }` - const filePath = path.join(ROOT_DIR, "webview-ui/src/services/grpc-client.ts") + const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts") await writeFileWithMkdirs(filePath, content) log_verbose(chalk.green(`Generated gRPC client at ${filePath}`)) } @@ -386,7 +367,7 @@ export interface ServiceHandlerConfig { export const serviceHandlers: Record = {${serviceConfigs.join(",")} };` - const configPath = path.join(ROOT_DIR, "src/core/controller/grpc-service-config.ts") + const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts") await writeFileWithMkdirs(configPath, content) log_verbose(chalk.green(`Generated service configuration at ${configPath}`)) } @@ -602,13 +583,12 @@ async function cleanup() { for (const file of existingFiles) { await fs.unlink(path.join(TS_OUT_DIR, file)) } - await rmdir(path.join(ROOT_DIR, "src/generated")) + await rmdir(path.join(ROOT_DIR, "src", "generated")) // Clean up generated files that were moved. - await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true }) - await rmdir(path.join(ROOT_DIR, "src/standalone/services")) - - await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: true }) + await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true }) + await rmdir(path.join(ROOT_DIR, "src", "standalone", "services")) + await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true }) await rmdir(path.join(ROOT_DIR, "hosts")) await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true }) @@ -636,13 +616,6 @@ async function rmdir(path) { } } -function serviceNameWithoutPackage(fullServiceName) { - return fullServiceName.replace(/.*\./, "") -} -function lowercaseFirstChar(str) { - return str.charAt(0).toLowerCase() + str.slice(1) -} - // Check for Apple Silicon compatibility function checkAppleSiliconCompatibility() { // Only run check on macOS diff --git a/standalone/runtime-files/vscode/enhanced-terminal.js b/standalone/runtime-files/vscode/enhanced-terminal.js new file mode 100644 index 0000000000..8dbb89f631 --- /dev/null +++ b/standalone/runtime-files/vscode/enhanced-terminal.js @@ -0,0 +1,514 @@ +const { spawn } = require("child_process") +const { EventEmitter } = require("events") +const path = require("path") +const os = require("os") + +// Enhanced terminal management for standalone Cline +// This replaces VSCode's terminal integration with real subprocess management + +class StandaloneTerminalProcess extends EventEmitter { + constructor() { + super() + this.waitForShellIntegration = false // We don't need to wait since we control the process + this.isListening = true + this.buffer = "" + this.fullOutput = "" + this.lastRetrievedIndex = 0 + this.isHot = false + this.hotTimer = null + this.childProcess = null + this.exitCode = null + this.isCompleted = false + } + + async run(terminal, command) { + console.log(`[StandaloneTerminal] Running command: ${command}`) + + // Get shell and working directory from terminal + const shell = terminal._shellPath || this.getDefaultShell() + const cwd = terminal._cwd || process.cwd() + + // Prepare command for execution + const shellArgs = this.getShellArgs(shell, command) + + try { + // Spawn the process + this.childProcess = spawn(shell, shellArgs, { + cwd: cwd, + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env, TERM: "xterm-256color" }, + }) + + // Track process state + let didEmitEmptyLine = false + + // Handle stdout + this.childProcess.stdout.on("data", (data) => { + const output = data.toString() + this.handleOutput(output, didEmitEmptyLine) + if (!didEmitEmptyLine && output) { + this.emit("line", "") // Signal start of output + didEmitEmptyLine = true + } + }) + + // Handle stderr + this.childProcess.stderr.on("data", (data) => { + const output = data.toString() + this.handleOutput(output, didEmitEmptyLine) + if (!didEmitEmptyLine && output) { + this.emit("line", "") + didEmitEmptyLine = true + } + }) + + // Handle process completion + this.childProcess.on("close", (code, signal) => { + console.log(`[StandaloneTerminal] Process closed with code ${code}, signal ${signal}`) + this.exitCode = code + this.isCompleted = true + this.emitRemainingBuffer() + + // Clear hot timer + if (this.hotTimer) { + clearTimeout(this.hotTimer) + this.isHot = false + } + + this.emit("completed") + this.emit("continue") + }) + + // Handle process errors + this.childProcess.on("error", (error) => { + console.error(`[StandaloneTerminal] Process error:`, error) + this.emit("error", error) + }) + + // Update terminal's process reference + terminal._process = this.childProcess + terminal._processId = this.childProcess.pid + } catch (error) { + console.error(`[StandaloneTerminal] Failed to spawn process:`, error) + this.emit("error", error) + } + } + + handleOutput(data, didEmitEmptyLine) { + // Set process as hot (actively outputting) + this.isHot = true + if (this.hotTimer) { + clearTimeout(this.hotTimer) + } + + // Check for compilation markers to adjust hot timeout + const compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"] + const markerNullifiers = [ + "compiled", + "success", + "finish", + "complete", + "succeed", + "done", + "end", + "stop", + "exit", + "terminate", + "error", + "fail", + ] + + const isCompiling = + compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) && + !markerNullifiers.some((nullifier) => data.toLowerCase().includes(nullifier.toLowerCase())) + + const hotTimeout = isCompiling ? 15000 : 2000 + this.hotTimer = setTimeout(() => { + this.isHot = false + }, hotTimeout) + + // Store full output + this.fullOutput += data + + if (this.isListening) { + this.emitLines(data) + this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length + } + } + + emitLines(chunk) { + this.buffer += chunk + let lineEndIndex + while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) { + let line = this.buffer.slice(0, lineEndIndex).trimEnd() + this.emit("line", line) + this.buffer = this.buffer.slice(lineEndIndex + 1) + } + } + + emitRemainingBuffer() { + if (this.buffer && this.isListening) { + const remainingBuffer = this.removeLastLineArtifacts(this.buffer) + if (remainingBuffer) { + this.emit("line", remainingBuffer) + } + this.buffer = "" + this.lastRetrievedIndex = this.fullOutput.length + } + } + + continue() { + this.emitRemainingBuffer() + this.isListening = false + this.removeAllListeners("line") + this.emit("continue") + } + + getUnretrievedOutput() { + const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex) + this.lastRetrievedIndex = this.fullOutput.length + return this.removeLastLineArtifacts(unretrieved) + } + + removeLastLineArtifacts(output) { + const lines = output.trimEnd().split("\n") + if (lines.length > 0) { + const lastLine = lines[lines.length - 1] + lines[lines.length - 1] = lastLine.replace(/[%$#>]\s*$/, "") + } + return lines.join("\n").trimEnd() + } + + getDefaultShell() { + if (process.platform === "win32") { + return process.env.COMSPEC || "cmd.exe" + } else { + return process.env.SHELL || "/bin/bash" + } + } + + getShellArgs(shell, command) { + if (process.platform === "win32") { + if (shell.toLowerCase().includes("powershell") || shell.toLowerCase().includes("pwsh")) { + return ["-Command", command] + } else { + return ["/c", command] + } + } else { + return ["-c", command] + } + } + + // Terminate the process if it's still running + terminate() { + if (this.childProcess && !this.isCompleted) { + console.log(`[StandaloneTerminal] Terminating process ${this.childProcess.pid}`) + this.childProcess.kill("SIGTERM") + + // Force kill after timeout + setTimeout(() => { + if (!this.isCompleted) { + console.log(`[StandaloneTerminal] Force killing process ${this.childProcess.pid}`) + this.childProcess.kill("SIGKILL") + } + }, 5000) + } + } +} + +class StandaloneTerminal { + constructor(options = {}) { + this.name = options.name || `Terminal ${Math.floor(Math.random() * 10000)}` + this.processId = Promise.resolve(Math.floor(Math.random() * 100000)) + this.creationOptions = options + this.exitStatus = undefined + this.state = { isInteractedWith: false } + this._cwd = options.cwd || process.cwd() + this._shellPath = options.shellPath + this._process = null + this._processId = null + + // Mock shell integration for compatibility + this.shellIntegration = { + cwd: { fsPath: this._cwd }, + executeCommand: (command) => { + // Return a mock execution object that the TerminalProcess expects + return { + read: async function* () { + // This will be handled by our StandaloneTerminalProcess + yield "" + }, + } + }, + } + + console.log(`[StandaloneTerminal] Created terminal: ${this.name} in ${this._cwd}`) + } + + sendText(text, addNewLine = true) { + console.log(`[StandaloneTerminal] sendText: ${text}`) + + // If we have an active process, send input to it + if (this._process && !this._process.killed) { + try { + this._process.stdin.write(text + (addNewLine ? "\n" : "")) + } catch (error) { + console.error(`[StandaloneTerminal] Error sending text to process:`, error) + } + } else { + // For compatibility with old behavior, we could spawn a new process + console.log(`[StandaloneTerminal] No active process to send text to`) + } + } + + show() { + console.log(`[StandaloneTerminal] show: ${this.name}`) + this.state.isInteractedWith = true + } + + hide() { + console.log(`[StandaloneTerminal] hide: ${this.name}`) + } + + dispose() { + console.log(`[StandaloneTerminal] dispose: ${this.name}`) + if (this._process && !this._process.killed) { + this._process.kill("SIGTERM") + } + } +} + +// Terminal registry for tracking terminals +class StandaloneTerminalRegistry { + constructor() { + this.terminals = new Map() + this.nextId = 1 + } + + createTerminal(options = {}) { + const terminal = new StandaloneTerminal(options) + const id = this.nextId++ + + const terminalInfo = { + id: id, + terminal: terminal, + busy: false, + lastCommand: "", + shellPath: options.shellPath, + lastActive: Date.now(), + pendingCwdChange: undefined, + cwdResolved: undefined, + } + + this.terminals.set(id, terminalInfo) + console.log(`[StandaloneTerminalRegistry] Created terminal ${id}`) + return terminalInfo + } + + getTerminal(id) { + return this.terminals.get(id) + } + + getAllTerminals() { + return Array.from(this.terminals.values()) + } + + removeTerminal(id) { + const terminalInfo = this.terminals.get(id) + if (terminalInfo) { + terminalInfo.terminal.dispose() + this.terminals.delete(id) + console.log(`[StandaloneTerminalRegistry] Removed terminal ${id}`) + } + } + + updateTerminal(id, updates) { + const terminalInfo = this.terminals.get(id) + if (terminalInfo) { + Object.assign(terminalInfo, updates) + } + } +} + +// Enhanced terminal manager +class StandaloneTerminalManager { + constructor() { + this.registry = new StandaloneTerminalRegistry() + this.processes = new Map() + this.terminalIds = new Set() + this.shellIntegrationTimeout = 4000 + this.terminalReuseEnabled = true + this.terminalOutputLineLimit = 500 + this.defaultTerminalProfile = "default" + } + + runCommand(terminalInfo, command) { + console.log(`[StandaloneTerminalManager] Running command on terminal ${terminalInfo.id}: ${command}`) + + terminalInfo.busy = true + terminalInfo.lastCommand = command + + const process = new StandaloneTerminalProcess() + this.processes.set(terminalInfo.id, process) + + process.once("completed", () => { + terminalInfo.busy = false + console.log(`[StandaloneTerminalManager] Command completed on terminal ${terminalInfo.id}`) + }) + + process.once("error", (error) => { + terminalInfo.busy = false + console.error(`[StandaloneTerminalManager] Command error on terminal ${terminalInfo.id}:`, error) + }) + + // Create promise for the process + const promise = new Promise((resolve, reject) => { + process.once("continue", () => resolve()) + process.once("error", (error) => reject(error)) + }) + + // Run the command immediately (no shell integration wait needed) + process.run(terminalInfo.terminal, command) + + // Return merged promise/process object + return this.mergePromise(process, promise) + } + + async getOrCreateTerminal(cwd) { + const terminals = this.registry.getAllTerminals() + + // Find available terminal with matching CWD + const matchingTerminal = terminals.find((t) => { + if (t.busy) return false + return t.terminal._cwd === cwd + }) + + if (matchingTerminal) { + this.terminalIds.add(matchingTerminal.id) + console.log(`[StandaloneTerminalManager] Reusing terminal ${matchingTerminal.id}`) + return matchingTerminal + } + + // Find any available terminal if reuse is enabled + if (this.terminalReuseEnabled) { + const availableTerminal = terminals.find((t) => !t.busy) + if (availableTerminal) { + // Change directory + await this.runCommand(availableTerminal, `cd "${cwd}"`) + availableTerminal.terminal._cwd = cwd + availableTerminal.terminal.shellIntegration.cwd.fsPath = cwd + this.terminalIds.add(availableTerminal.id) + console.log(`[StandaloneTerminalManager] Reused terminal ${availableTerminal.id} with cd`) + return availableTerminal + } + } + + // Create new terminal + const newTerminalInfo = this.registry.createTerminal({ + cwd: cwd, + name: `Cline Terminal ${this.registry.nextId}`, + }) + this.terminalIds.add(newTerminalInfo.id) + console.log(`[StandaloneTerminalManager] Created new terminal ${newTerminalInfo.id}`) + return newTerminalInfo + } + + getTerminals(busy) { + return Array.from(this.terminalIds) + .map((id) => this.registry.getTerminal(id)) + .filter((t) => t && t.busy === busy) + .map((t) => ({ id: t.id, lastCommand: t.lastCommand })) + } + + getUnretrievedOutput(terminalId) { + if (!this.terminalIds.has(terminalId)) { + return "" + } + const process = this.processes.get(terminalId) + return process ? process.getUnretrievedOutput() : "" + } + + isProcessHot(terminalId) { + const process = this.processes.get(terminalId) + return process ? process.isHot : false + } + + processOutput(outputLines) { + if (outputLines.length > this.terminalOutputLineLimit) { + const halfLimit = Math.floor(this.terminalOutputLineLimit / 2) + const start = outputLines.slice(0, halfLimit) + const end = outputLines.slice(outputLines.length - halfLimit) + return `${start.join("\n")}\n... (output truncated) ...\n${end.join("\n")}`.trim() + } + return outputLines.join("\n").trim() + } + + disposeAll() { + // Terminate all processes + for (const [terminalId, process] of this.processes) { + if (process && process.terminate) { + process.terminate() + } + } + + // Clear all tracking + this.terminalIds.clear() + this.processes.clear() + + // Dispose all terminals + for (const terminalInfo of this.registry.getAllTerminals()) { + terminalInfo.terminal.dispose() + } + + console.log(`[StandaloneTerminalManager] Disposed all terminals`) + } + + // Set shell integration timeout (compatibility method) + setShellIntegrationTimeout(timeout) { + this.shellIntegrationTimeout = timeout + console.log(`[StandaloneTerminalManager] Set shell integration timeout to ${timeout}ms`) + } + + // Set terminal reuse enabled (compatibility method) + setTerminalReuseEnabled(enabled) { + this.terminalReuseEnabled = enabled + console.log(`[StandaloneTerminalManager] Set terminal reuse enabled to ${enabled}`) + } + + // Set terminal output line limit (compatibility method) + setTerminalOutputLineLimit(limit) { + this.terminalOutputLineLimit = limit + console.log(`[StandaloneTerminalManager] Set terminal output line limit to ${limit}`) + } + + // Set default terminal profile (compatibility method) + setDefaultTerminalProfile(profile) { + this.defaultTerminalProfile = profile + console.log(`[StandaloneTerminalManager] Set default terminal profile to ${profile}`) + } + + // Helper to merge process and promise (similar to execa) + mergePromise(process, promise) { + const nativePromisePrototype = (async () => {})().constructor.prototype + const descriptors = ["then", "catch", "finally"].map((property) => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), + ]) + + for (const [property, descriptor] of descriptors) { + if (descriptor) { + const value = descriptor.value.bind(promise) + Reflect.defineProperty(process, property, { ...descriptor, value }) + } + } + + return process + } +} + +module.exports = { + StandaloneTerminal, + StandaloneTerminalProcess, + StandaloneTerminalRegistry, + StandaloneTerminalManager, +} diff --git a/standalone/runtime-files/vscode/vscode-impls.js b/standalone/runtime-files/vscode/vscode-impls.js index db340d4ecd..f872c71e02 100644 --- a/standalone/runtime-files/vscode/vscode-impls.js +++ b/standalone/runtime-files/vscode/vscode-impls.js @@ -2,8 +2,19 @@ console.log("Loading stub impls...") const { createStub } = require("./stub-utils") const open = require("open").default +const fs = require("fs") +const path = require("path") +const { StandaloneTerminalManager } = require("./enhanced-terminal") +// Import the base vscode object from stubs +const vscode = require("./vscode-stubs.js") + +// Create global terminal manager instance +const globalTerminalManager = new StandaloneTerminalManager() + +// Extend the existing window object from stubs rather than overwriting it vscode.window = { + ...vscode.window, // Keep existing properties from stubs showInformationMessage: (...args) => { console.log("Stubbed showInformationMessage:", ...args) return Promise.resolve(undefined) @@ -28,9 +39,210 @@ vscode.window = { console.log("Stubbed showSaveDialog:", options) return undefined }, - showTextDocument: async (...args) => { - console.log("Stubbed showTextDocument:", ...args) - return {} + showTextDocument: async (uri, options) => { + console.log("Stubbed showTextDocument:", uri, options) + + // Extract file path from URI + let filePath = uri.path || uri.fsPath || uri + if (typeof filePath !== "string") { + filePath = uri.toString() + } + + // Remove file:// prefix if present + if (filePath.startsWith("file://")) { + filePath = filePath.substring(7) + } + + // Create a function that always reads the current file content + const getCurrentFileContent = async () => { + try { + const content = await fs.promises.readFile(filePath, "utf8") + console.log(`getCurrentFileContent: Read file ${filePath} (${content.length} chars)`) + return content + } catch (error) { + console.log(`getCurrentFileContent: Could not read file ${filePath}:`, error.message) + return "" + } + } + + // Try to read the initial file content + let fileContent = await getCurrentFileContent() + let lineCount = fileContent.split("\n").length + + // Check if we already have an active editor for this file path + const existingEditor = vscode.window._documentEditors && vscode.window._documentEditors[filePath] + if (existingEditor) { + console.log(`showTextDocument: Updating existing editor for ${filePath}`) + // Update the existing editor's content + fileContent = await getCurrentFileContent() + lineCount = fileContent.split("\n").length + + // Update the document's getText method to return current content + existingEditor.document.getText = (range) => { + // Always read fresh content for getText calls + const currentContent = require("fs").readFileSync(filePath, "utf8") + if (!range) { + return currentContent + } + // Handle range-based getText with current content + const lines = currentContent.split("\n") + const startLine = Math.max(0, range.start.line) + const endLine = Math.min(lines.length - 1, range.end.line) + + if (startLine === endLine) { + // Single line + const line = lines[startLine] || "" + const startChar = Math.max(0, range.start.character) + const endChar = Math.min(line.length, range.end.character) + return line.substring(startChar, endChar) + } else { + // Multiple lines + const result = [] + for (let i = startLine; i <= endLine; i++) { + const line = lines[i] || "" + if (i === startLine) { + result.push(line.substring(range.start.character)) + } else if (i === endLine) { + result.push(line.substring(0, range.end.character)) + } else { + result.push(line) + } + } + return result.join("\n") + } + } + + // Update other properties + existingEditor.document.lineCount = lineCount + existingEditor.document.fileName = filePath + + // Update the active text editor reference + vscode.window.activeTextEditor = existingEditor + + return existingEditor + } + + // Create a new mock text editor that always reads current file content + const mockEditor = { + document: { + uri: uri, + fileName: filePath, + isDirty: false, + lineCount: lineCount, + getText: (range) => { + // Always read fresh content for getText calls + try { + const currentContent = require("fs").readFileSync(filePath, "utf8") + console.log(`document.getText: Read fresh content (${currentContent.length} chars)`) + if (!range) { + return currentContent + } + // Handle range-based getText with current content + const lines = currentContent.split("\n") + const startLine = Math.max(0, range.start.line) + const endLine = Math.min(lines.length - 1, range.end.line) + + if (startLine === endLine) { + // Single line + const line = lines[startLine] || "" + const startChar = Math.max(0, range.start.character) + const endChar = Math.min(line.length, range.end.character) + return line.substring(startChar, endChar) + } else { + // Multiple lines + const result = [] + for (let i = startLine; i <= endLine; i++) { + const line = lines[i] || "" + if (i === startLine) { + result.push(line.substring(range.start.character)) + } else if (i === endLine) { + result.push(line.substring(0, range.end.character)) + } else { + result.push(line) + } + } + return result.join("\n") + } + } catch (error) { + console.error(`Error reading file in getText: ${error.message}`) + return "" + } + }, + save: async () => { + console.log("Called mock textDocument.save") + return true + }, + positionAt: (offset) => { + try { + const currentContent = require("fs").readFileSync(filePath, "utf8") + const lines = currentContent.split("\n") + let currentOffset = 0 + for (let line = 0; line < lines.length; line++) { + const lineLength = lines[line].length + 1 // +1 for newline + if (currentOffset + lineLength > offset) { + return { line: line, character: offset - currentOffset } + } + currentOffset += lineLength + } + return { line: lines.length - 1, character: lines[lines.length - 1]?.length || 0 } + } catch (error) { + return { line: 0, character: 0 } + } + }, + offsetAt: (position) => { + try { + const currentContent = require("fs").readFileSync(filePath, "utf8") + const lines = currentContent.split("\n") + let offset = 0 + for (let i = 0; i < position.line && i < lines.length; i++) { + offset += lines[i].length + 1 // +1 for newline + } + offset += Math.min(position.character, lines[position.line]?.length || 0) + return offset + } catch (error) { + return 0 + } + }, + }, + selection: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, + selections: [], + visibleRanges: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }], + options: {}, + viewColumn: 1, + edit: async (callback) => { + console.log("Called mock textEditor.edit") + return true + }, + insertSnippet: async () => true, + setDecorations: () => {}, + revealRange: () => {}, + show: () => {}, + hide: () => {}, + } + + // Store the editor by file path for future reference + if (!vscode.window._documentEditors) { + vscode.window._documentEditors = {} + } + vscode.window._documentEditors[filePath] = mockEditor + + // Update the active text editor + vscode.window.activeTextEditor = mockEditor + + // Trigger onDidChangeActiveTextEditor listeners + if (vscode.window._activeTextEditorListeners) { + setTimeout(() => { + vscode.window._activeTextEditorListeners.forEach((listener) => { + try { + listener(mockEditor) + } catch (error) { + console.error("Error calling onDidChangeActiveTextEditor listener:", error) + } + }) + }, 10) // Small delay to simulate async behavior + } + + return mockEditor }, createOutputChannel: (name) => { console.log("Stubbed createOutputChannel:", name) @@ -41,20 +253,60 @@ vscode.window = { } }, createTerminal: (...args) => { - console.log("Stubbed createTerminal:", ...args) - return { - sendText: console.log, - show: () => {}, - dispose: () => {}, + console.log("Enhanced createTerminal:", ...args) + + // Extract options from arguments + let options = {} + if (args.length > 0) { + if (typeof args[0] === "string") { + // Called with (name, shellPath, shellArgs) + options = { + name: args[0], + shellPath: args[1], + shellArgs: args[2], + } + } else if (typeof args[0] === "object") { + // Called with options object + options = args[0] + } } + + // Use our enhanced terminal manager to create a terminal + const terminalInfo = globalTerminalManager.registry.createTerminal({ + name: options.name || `Terminal ${Date.now()}`, + cwd: options.cwd || process.cwd(), + shellPath: options.shellPath, + }) + + // Store reference for tracking + vscode.window.terminals.push(terminalInfo.terminal) + if (!vscode.window.activeTerminal) { + vscode.window.activeTerminal = terminalInfo.terminal + } + + console.log(`Enhanced terminal created: ${terminalInfo.id}`) + return terminalInfo.terminal }, activeTextEditor: undefined, visibleTextEditors: [], tabGroups: { - all: [], - close: async () => {}, - onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"), - activeTabGroup: { tabs: [] }, + all: [ + { + tabs: [], + isActive: true, + viewColumn: 1, + }, + ], + activeTabGroup: { + tabs: [], + isActive: true, + viewColumn: 1, + }, + close: async (tab) => { + console.log("Stubbed tabGroups.close:", tab) + return true + }, + onDidChangeTabs: createStub("vscode.window.tabGroups.onDidChangeTabs"), }, withProgress: async (_options, task) => { console.log("Stubbed withProgress") @@ -62,14 +314,56 @@ vscode.window = { }, registerUriHandler: () => ({ dispose: () => {} }), registerWebviewViewProvider: () => ({ dispose: () => {} }), - onDidChangeActiveTextEditor: () => ({ dispose: () => {} }), - createTextEditorDecorationType: () => ({ dispose: () => {} }), - createWebviewPanel: (..._args) => { - throw new Error("WebviewPanel is not supported in standalone app.") + onDidChangeActiveTextEditor: (listener) => { + console.log("Called vscode.window.onDidChangeActiveTextEditor") + // Store the listener so we can call it when showTextDocument is called + vscode.window._activeTextEditorListeners = vscode.window._activeTextEditorListeners || [] + vscode.window._activeTextEditorListeners.push(listener) + return { + dispose: () => { + console.log("Disposed onDidChangeActiveTextEditor listener") + const index = vscode.window._activeTextEditorListeners.indexOf(listener) + if (index > -1) { + vscode.window._activeTextEditorListeners.splice(index, 1) + } + }, + } }, + createTextEditorDecorationType: () => ({ dispose: () => {} }), + createWebviewPanel: (...args) => { + console.log("Stubbed createWebviewPanel:", ...args) + return { + webview: {}, + reveal: () => {}, + dispose: () => {}, + } + }, + onDidChangeTerminalState: (listener) => { + console.log("Called vscode.window.onDidChangeTerminalState") + return { + dispose: () => { + console.log("Disposed onDidChangeTerminalState listener") + }, + } + }, + onDidChangeTextEditorVisibleRanges: (listener) => { + console.log("Called vscode.window.onDidChangeTextEditorVisibleRanges") + return { + dispose: () => { + console.log("Disposed onDidChangeTextEditorVisibleRanges listener") + }, + } + }, + terminals: [], + activeTerminal: null, } -vscode.env = { +// Initialize env object if it doesn't exist, then extend it +if (!vscode.env) { + vscode.env = {} +} + +Object.assign(vscode.env, { uriScheme: "vscode", appName: "Visual Studio Code", appRoot: "/tmp/vscode/appRoot", @@ -79,17 +373,26 @@ vscode.env = { sessionId: "stub-session-id", shell: "/bin/bash", + // Add the stub functions that were missing clipboard: createStub("vscode.env.clipboard"), - openExternal: createStub("vscode.env.openExternal"), getQueryParameter: createStub("vscode.env.getQueryParameter"), onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"), isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"), telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"), onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"), createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"), +}) + +// Override the openExternal function with actual implementation +vscode.env.openExternal = async (uri) => { + const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "") + console.log("Opening browser:", url) + await open(url) + return true } -vscode.Uri = { +// Extend Uri object with improved implementations +Object.assign(vscode.Uri, { parse: (uriString) => { const url = new URL(uriString) return { @@ -134,13 +437,405 @@ vscode.Uri = { const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/") return vscode.Uri.file("/" + joined.replace(/\/+/g, "/")) }, +}) + +// Extend workspace object with file system operations +Object.assign(vscode.workspace, { + fs: { + readFile: async function (uri) { + console.log(`Called vscode.workspace.fs.readFile with uri:`, uri) + try { + // Extract file path from URI + let filePath = uri.path || uri.fsPath || uri + if (typeof filePath !== "string") { + filePath = uri.toString() + } + + // Remove file:// prefix if present + if (filePath.startsWith("file://")) { + filePath = filePath.substring(7) + } + + console.log(`Reading file: ${filePath}`) + const content = await fs.promises.readFile(filePath, "utf8") + console.log( + `File content read (${content.length} chars):`, + content.substring(0, 100) + (content.length > 100 ? "..." : ""), + ) + return new Uint8Array(Buffer.from(content, "utf8")) + } catch (error) { + console.error(`Error reading file:`, error) + throw error + } + }, + + writeFile: async function (uri, content) { + console.log(`Called vscode.workspace.fs.writeFile with uri:`, uri) + try { + // Extract file path from URI + let filePath = uri.path || uri.fsPath || uri + if (typeof filePath !== "string") { + filePath = uri.toString() + } + + // Remove file:// prefix if present + if (filePath.startsWith("file://")) { + filePath = filePath.substring(7) + } + + console.log(`Writing file: ${filePath}`) + + // Ensure directory exists + const dir = path.dirname(filePath) + await fs.promises.mkdir(dir, { recursive: true }) + + // Write the file + await fs.promises.writeFile(filePath, content) + } catch (error) { + console.error(`Error writing file:`, error) + throw error + } + }, + }, + + // Add workspace folder configuration + rootPath: process.cwd(), + workspaceFolders: [ + { + uri: vscode.Uri.file(process.cwd()), + name: path.basename(process.cwd()), + index: 0, + }, + ], + name: path.basename(process.cwd()), + workspaceFile: vscode.Uri.file(path.join(process.cwd(), ".vscode", "workspace.json")), + + // Add other workspace methods as stubs + getConfiguration: () => ({ + get: () => undefined, + update: () => Promise.resolve(), + has: () => false, + }), + getWorkspaceFolder: (uri) => { + console.log("Called vscode.workspace.getWorkspaceFolder with:", uri) + // Return the first workspace folder for any URI in standalone mode + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + return vscode.workspace.workspaceFolders[0] + } + return undefined + }, + createFileSystemWatcher: () => ({ + onDidChange: () => ({ dispose: () => {} }), + onDidCreate: () => ({ dispose: () => {} }), + onDidDelete: () => ({ dispose: () => {} }), + dispose: () => {}, + }), + onDidChangeConfiguration: () => ({ dispose: () => {} }), + onDidChangeWorkspaceFolders: () => ({ dispose: () => {} }), + onDidCreateFiles: createStub("vscode.workspace.onDidCreateFiles"), + onDidDeleteFiles: createStub("vscode.workspace.onDidDeleteFiles"), + onDidRenameFiles: createStub("vscode.workspace.onDidRenameFiles"), + onWillCreateFiles: createStub("vscode.workspace.onWillCreateFiles"), + onWillDeleteFiles: createStub("vscode.workspace.onWillDeleteFiles"), + onWillRenameFiles: createStub("vscode.workspace.onWillRenameFiles"), + textDocuments: { + find: (predicate) => { + console.log("Called vscode.workspace.textDocuments.find") + // Return a mock text document that behaves like VSCode expects + return { + uri: { fsPath: "/tmp/mock-document" }, + fileName: "/tmp/mock-document", + isDirty: false, + save: async () => { + console.log("Called mock textDocument.save") + return true + }, + getText: () => "", + lineCount: 0, + } + }, + forEach: (callback) => { + console.log("Called vscode.workspace.textDocuments.forEach") + // No documents to iterate over in standalone mode + }, + length: 0, + [Symbol.iterator]: function* () { + // Empty iterator for standalone mode + }, + }, + + // Add the crucial applyEdit method + applyEdit: async (workspaceEdit) => { + console.log("Called vscode.workspace.applyEdit", workspaceEdit) + + // For standalone mode, we'll simulate applying the edit by actually writing to files + try { + // WorkspaceEdit can contain multiple types of edits + if (workspaceEdit._edits) { + for (const edit of workspaceEdit._edits) { + if (edit._type === 1) { + // TextEdit + const uri = edit._uri + const edits = edit._edits + + let filePath = uri.path || uri.fsPath + if (filePath.startsWith("file://")) { + filePath = filePath.substring(7) + } + + console.log(`Applying text edits to: ${filePath}`) + + // Read current content if file exists + let currentContent = "" + try { + currentContent = await fs.promises.readFile(filePath, "utf8") + } catch (e) { + // File doesn't exist, start with empty content + console.log(`File ${filePath} doesn't exist, starting with empty content`) + } + + // Apply edits in reverse order (from end to beginning) to maintain positions + const sortedEdits = edits.sort((a, b) => { + const aStart = a.range.start.line * 1000000 + a.range.start.character + const bStart = b.range.start.line * 1000000 + b.range.start.character + return bStart - aStart + }) + + let lines = currentContent.split("\n") + + for (const edit of sortedEdits) { + const startLine = edit.range.start.line + const startChar = edit.range.start.character + const endLine = edit.range.end.line + const endChar = edit.range.end.character + const newText = edit.newText + + console.log(`Applying edit: ${startLine}:${startChar} - ${endLine}:${endChar} -> "${newText}"`) + + // Handle the edit + if (startLine === endLine) { + // Single line edit + const line = lines[startLine] || "" + lines[startLine] = line.substring(0, startChar) + newText + line.substring(endChar) + } else { + // Multi-line edit + const firstLine = lines[startLine] || "" + const lastLine = lines[endLine] || "" + const newFirstLine = firstLine.substring(0, startChar) + newText + lastLine.substring(endChar) + + // Replace the range with the new content + lines.splice(startLine, endLine - startLine + 1, newFirstLine) + } + } + + const newContent = lines.join("\n") + + // Ensure directory exists + const dir = path.dirname(filePath) + await fs.promises.mkdir(dir, { recursive: true }) + + // Write the updated content + await fs.promises.writeFile(filePath, newContent, "utf8") + console.log(`Successfully applied edits to: ${filePath}`) + } + } + } + + return true + } catch (error) { + console.error("Error applying workspace edit:", error) + return false + } + }, +}) + +// Fix CodeActionKind to have static properties instead of being a class +vscode.CodeActionKind = { + Empty: "", + QuickFix: "quickfix", + Refactor: "refactor", + RefactorExtract: "refactor.extract", + RefactorInline: "refactor.inline", + RefactorRewrite: "refactor.rewrite", + Source: "source", + SourceOrganizeImports: "source.organizeImports", + SourceFixAll: "source.fixAll", } -vscode.env.openExternal = async (uri) => { - const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "") - console.log("Opening browser:", url) - await open(url) - return true +// Add missing commands implementation +if (!vscode.commands) { + vscode.commands = {} } +Object.assign(vscode.commands, { + executeCommand: async (command, ...args) => { + console.log(`Called vscode.commands.executeCommand: ${command}`, args) + + // Handle the vscode.diff command specifically + if (command === "vscode.diff") { + const [originalUri, modifiedUri, title, options] = args + console.log("Opening diff view:", { originalUri, modifiedUri, title }) + + // For standalone mode, just open the modified file directly + // since we can't show a proper diff view + const editor = await vscode.window.showTextDocument(modifiedUri, { + preserveFocus: options?.preserveFocus || false, + preview: false, + }) + + // Ensure the onDidChangeActiveTextEditor event fires with a slight delay + // This is crucial for DiffViewProvider.openDiffEditor() to work properly + setTimeout(() => { + if (vscode.window._activeTextEditorListeners) { + vscode.window._activeTextEditorListeners.forEach((listener) => { + try { + listener(editor) + } catch (error) { + console.error("Error calling onDidChangeActiveTextEditor listener in vscode.diff:", error) + } + }) + } + }, 50) // Slightly longer delay to ensure proper event ordering + + return editor + } + + // For other commands, just return a resolved promise + return Promise.resolve() + }, + registerCommand: (command, callback) => { + console.log(`Registered command: ${command}`) + return { dispose: () => {} } + }, + getCommands: async () => { + return [] + }, +}) + +// Add missing TabInput classes +vscode.TabInputText = class TabInputText { + constructor(uri) { + this.uri = uri + } +} + +vscode.TabInputTextDiff = class TabInputTextDiff { + constructor(original, modified) { + this.original = original + this.modified = modified + } +} + +// Add missing WorkspaceEdit and related classes +vscode.WorkspaceEdit = class WorkspaceEdit { + constructor() { + this._edits = [] + } + + replace(uri, range, newText) { + console.log("WorkspaceEdit.replace:", uri, range, newText) + this._edits.push({ + _type: 1, // TextEdit + _uri: uri, + _edits: [ + { + range: range, + newText: newText, + }, + ], + }) + } + + insert(uri, position, newText) { + console.log("WorkspaceEdit.insert:", uri, position, newText) + this.replace(uri, new vscode.Range(position, position), newText) + } + + delete(uri, range) { + console.log("WorkspaceEdit.delete:", uri, range) + this.replace(uri, range, "") + } +} + +vscode.Range = class Range { + constructor(startLine, startCharacter, endLine, endCharacter) { + if (typeof startLine === "object") { + // Called with Position objects + this.start = startLine + this.end = startCharacter + } else { + // Called with line/character numbers + this.start = new vscode.Position(startLine, startCharacter) + this.end = new vscode.Position(endLine, endCharacter) + } + } +} + +vscode.Position = class Position { + constructor(line, character) { + this.line = line + this.character = character + } +} + +vscode.Selection = class Selection extends vscode.Range { + constructor(anchorLine, anchorCharacter, activeLine, activeCharacter) { + if (typeof anchorLine === "object") { + // Called with Position objects + super(anchorLine, anchorCharacter) + this.anchor = anchorLine + this.active = anchorCharacter + } else { + // Called with line/character numbers + super(anchorLine, anchorCharacter, activeLine, activeCharacter) + this.anchor = new vscode.Position(anchorLine, anchorCharacter) + this.active = new vscode.Position(activeLine, activeCharacter) + } + } +} + +// Add TextEditorRevealType enum +vscode.TextEditorRevealType = { + Default: 0, + InCenter: 1, + InCenterIfOutsideViewport: 2, + AtTop: 3, +} + +// Add missing languages API +if (!vscode.languages) { + vscode.languages = {} +} + +Object.assign(vscode.languages, { + getDiagnostics: (uri) => { + console.log("Called vscode.languages.getDiagnostics") + // Return empty diagnostics for standalone mode + if (uri) { + return [] + } else { + // Return all diagnostics as empty array + return [] + } + }, + registerCodeActionsProvider: () => ({ dispose: () => {} }), + createDiagnosticCollection: () => ({ + set: () => {}, + delete: () => {}, + clear: () => {}, + dispose: () => {}, + }), +}) + console.log("Finished loading stub impls...") + +// Export the terminal manager globally for Cline core to use +global.standaloneTerminalManager = globalTerminalManager + +// Override the TerminalManager to use our standalone implementation +if (typeof global !== "undefined") { + // Replace the TerminalManager class with our standalone implementation + global.StandaloneTerminalManagerClass = require("./enhanced-terminal").StandaloneTerminalManager +} + +module.exports = vscode