Compare commits

...

4 Commits

Author SHA1 Message Date
Andrei Eternal 03f2871851 format 2025-06-26 14:38:22 -07:00
Andrei Eternal 480c7ed58e add workspace host config 2025-06-26 14:37:46 -07:00
Andrei Eternal 4234176de9 weird errors 2025-06-26 14:31:17 -07:00
Andrei Eternal 61c5df5194 experimental vscode impls 2025-06-26 14:20:46 -07:00
4 changed files with 1280 additions and 69 deletions
+29
View File
@@ -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
}
+18 -45
View File
@@ -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<string, ServiceHandlerConfig> = {${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
@@ -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,
}
+719 -24
View File
@@ -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