/
outbreak
/
kilocode
Обзор
Документация
Войти
/
outbreak
/
kilocode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/src/host/VSCode.ts
2 541 строка
70 KB
Brian Carlson
Fix code search on CLI
22 ноя 2025, 01:44
22 ноя 2025, 01:44
8b9eb7f
Код
Авторство
О чём код?
import * as fs from "fs" import * as path from "path" import { logs } from "../services/logs.js" import { KiloCodePaths } from "../utils/paths.js" import { Package } from "../constants/package.js" // Identity information for VSCode environment export interface IdentityInfo { machineId: string sessionId: string cliUserId?: string } // Basic VSCode API types and enums export type Thenable<T> = Promise<T> // TextDocument interface for VSCode API export interface TextDocument { uri: Uri fileName: string languageId: string version: number isDirty: boolean isClosed: boolean lineCount: number getText(range?: Range): string lineAt(line: number): TextLine offsetAt(position: Position): number positionAt(offset: number): Position save(): Thenable<boolean> validateRange(range: Range): Range validatePosition(position: Position): Position } export interface TextLine { text: string range: Range rangeIncludingLineBreak: Range firstNonWhitespaceCharacterIndex: number isEmptyOrWhitespace: boolean } export interface WorkspaceFoldersChangeEvent { added: WorkspaceFolder[] removed: WorkspaceFolder[] } export interface TextDocumentChangeEvent { document: TextDocument contentChanges: readonly TextDocumentContentChangeEvent[] } export interface TextDocumentContentChangeEvent { range: Range rangeOffset: number rangeLength: number text: string } export interface ConfigurationChangeEvent { affectsConfiguration(section: string, scope?: Uri): boolean } export interface ConfigurationInspect<T> { key: string defaultValue?: T globalValue?: T workspaceValue?: T workspaceFolderValue?: T } export interface TextDocumentContentProvider { provideTextDocumentContent(uri: Uri, token: CancellationToken): Thenable<string> onDidChange?: (listener: (e: Uri) => void) => Disposable } export interface FileSystemWatcher extends Disposable { onDidChange: (listener: (e: Uri) => void) => Disposable onDidCreate: (listener: (e: Uri) => void) => Disposable onDidDelete: (listener: (e: Uri) => void) => Disposable } export interface RelativePattern { base: string pattern: string } // TextEditor and related interfaces export interface TextEditor { document: TextDocument selection: Selection selections: Selection[] visibleRanges: Range[] options: TextEditorOptions viewColumn?: ViewColumn edit(callback: (editBuilder: TextEditorEdit) => void): Thenable<boolean> insertSnippet( snippet: unknown, location?: Position | Range | readonly Position[] | readonly Range[], ): Thenable<boolean> setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: readonly Range[]): void revealRange(range: Range, revealType?: TextEditorRevealType): void show(column?: ViewColumn): void hide(): void } export interface TextEditorOptions { tabSize?: number insertSpaces?: boolean cursorStyle?: number lineNumbers?: number } export interface TextEditorEdit { replace(location: Position | Range | Selection, value: string): void insert(location: Position, value: string): void delete(location: Range | Selection): void setEndOfLine(endOfLine: EndOfLine): void } export interface TextEditorSelectionChangeEvent { textEditor: TextEditor selections: readonly Selection[] kind?: number } export interface TextDocumentShowOptions { viewColumn?: ViewColumn preserveFocus?: boolean preview?: boolean selection?: Range } export interface DecorationRenderOptions { backgroundColor?: string | ThemeColor border?: string borderColor?: string | ThemeColor borderRadius?: string borderSpacing?: string borderStyle?: string borderWidth?: string color?: string | ThemeColor cursor?: string fontStyle?: string fontWeight?: string gutterIconPath?: string | Uri gutterIconSize?: string isWholeLine?: boolean letterSpacing?: string opacity?: string outline?: string outlineColor?: string | ThemeColor outlineStyle?: string outlineWidth?: string overviewRulerColor?: string | ThemeColor overviewRulerLane?: OverviewRulerLane rangeBehavior?: DecorationRangeBehavior textDecoration?: string } export interface Terminal { name: string processId: Thenable<number | undefined> creationOptions: Readonly<TerminalOptions> exitStatus: TerminalExitStatus | undefined state: TerminalState sendText(text: string, addNewLine?: boolean): void show(preserveFocus?: boolean): void hide(): void dispose(): void } export interface TerminalOptions { name?: string shellPath?: string shellArgs?: string[] | string cwd?: string | Uri env?: { [key: string]: string | null | undefined } iconPath?: Uri | ThemeIcon hideFromUser?: boolean message?: string strictEnv?: boolean } export interface TerminalExitStatus { code: number | undefined reason: number } export interface TerminalState { isInteractedWith: boolean } export interface TerminalDimensionsChangeEvent { terminal: Terminal dimensions: TerminalDimensions } export interface TerminalDimensions { columns: number rows: number } export interface TerminalDataWriteEvent { terminal: Terminal data: string } export interface WebviewViewProvider { resolveWebviewView( webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken, ): Thenable<void> | void } export interface WebviewView { webview: Webview viewType: string title?: string description?: string badge?: ViewBadge show(preserveFocus?: boolean): void onDidChangeVisibility: (listener: () => void) => Disposable onDidDispose: (listener: () => void) => Disposable visible: boolean } export interface Webview { html: string options: WebviewOptions cspSource: string postMessage(message: unknown): Thenable<boolean> onDidReceiveMessage: (listener: (message: unknown) => void) => Disposable asWebviewUri(localResource: Uri): Uri } export interface WebviewOptions { enableScripts?: boolean enableForms?: boolean localResourceRoots?: readonly Uri[] portMapping?: readonly WebviewPortMapping[] } export interface WebviewPortMapping { webviewPort: number extensionHostPort: number } export interface ViewBadge { tooltip: string value: number } export interface WebviewViewResolveContext { state?: unknown } export interface WebviewViewProviderOptions { retainContextWhenHidden?: boolean } export interface UriHandler { handleUri(uri: Uri): void } export interface QuickPickOptions { placeHolder?: string canPickMany?: boolean ignoreFocusOut?: boolean matchOnDescription?: boolean matchOnDetail?: boolean } export interface InputBoxOptions { value?: string valueSelection?: [number, number] prompt?: string placeHolder?: string password?: boolean ignoreFocusOut?: boolean validateInput?(value: string): string | undefined | null | Thenable<string | undefined | null> } export interface OpenDialogOptions { defaultUri?: Uri openLabel?: string canSelectFiles?: boolean canSelectFolders?: boolean canSelectMany?: boolean filters?: { [name: string]: string[] } title?: string } // VSCode EventEmitter implementation export interface Disposable { dispose(): void } type Listener<T> = (e: T) => void export class EventEmitter<T> { readonly #listeners = new Set<Listener<T>>() /** * The event listeners can subscribe to. */ event = (listener: (e: T) => void, thisArgs?: unknown, disposables?: Disposable[]): Disposable => { const fn = thisArgs ? listener.bind(thisArgs) : listener this.#listeners.add(fn) const disposable = { dispose: () => { this.#listeners.delete(fn) }, } if (disposables) { disposables.push(disposable) } return disposable } /** * Notify all subscribers of the event. Failure * of one or more listener will not fail this function call. * * @param data The event object. */ fire = (data: T): void => { for (const listener of this.#listeners) { try { listener(data) } catch { // ignore } } } /** * Dispose this object and free resources. */ dispose = (): void => { this.#listeners.clear() } } export enum ConfigurationTarget { Global = 1, Workspace = 2, WorkspaceFolder = 3, } export enum ViewColumn { Active = -1, Beside = -2, One = 1, Two = 2, Three = 3, } export enum TextEditorRevealType { Default = 0, InCenter = 1, InCenterIfOutsideViewport = 2, AtTop = 3, } export enum StatusBarAlignment { Left = 1, Right = 2, } export enum DiagnosticSeverity { Error = 0, Warning = 1, Information = 2, Hint = 3, } // Position class export class Position { constructor( public line: number, public character: number, ) {} isEqual(other: Position): boolean { return this.line === other.line && this.character === other.character } isBefore(other: Position): boolean { if (this.line < other.line) { return true } if (this.line === other.line) { return this.character < other.character } return false } isBeforeOrEqual(other: Position): boolean { return this.isBefore(other) || this.isEqual(other) } isAfter(other: Position): boolean { return !this.isBeforeOrEqual(other) } isAfterOrEqual(other: Position): boolean { return !this.isBefore(other) } compareTo(other: Position): number { if (this.line < other.line) { return -1 } if (this.line > other.line) { return 1 } if (this.character < other.character) { return -1 } if (this.character > other.character) { return 1 } return 0 } translate(lineDelta?: number, characterDelta?: number): Position translate(change: { lineDelta?: number; characterDelta?: number }): Position translate( lineDeltaOrChange?: number | { lineDelta?: number; characterDelta?: number }, characterDelta?: number, ): Position { if (typeof lineDeltaOrChange === "object") { return new Position( this.line + (lineDeltaOrChange.lineDelta || 0), this.character + (lineDeltaOrChange.characterDelta || 0), ) } return new Position(this.line + (lineDeltaOrChange || 0), this.character + (characterDelta || 0)) } with(line?: number, character?: number): Position with(change: { line?: number; character?: number }): Position with(lineOrChange?: number | { line?: number; character?: number }, character?: number): Position { if (typeof lineOrChange === "object") { return new Position( lineOrChange.line !== undefined ? lineOrChange.line : this.line, lineOrChange.character !== undefined ? lineOrChange.character : this.character, ) } return new Position( lineOrChange !== undefined ? lineOrChange : this.line, character !== undefined ? character : this.character, ) } } // Range class export class Range { public start: Position public end: Position constructor(start: Position, end: Position) constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number) constructor( startOrStartLine: Position | number, endOrStartCharacter: Position | number, endLine?: number, endCharacter?: number, ) { if (typeof startOrStartLine === "number") { this.start = new Position(startOrStartLine, endOrStartCharacter as number) this.end = new Position(endLine!, endCharacter!) } else { this.start = startOrStartLine this.end = endOrStartCharacter as Position } } get isEmpty(): boolean { return this.start.isEqual(this.end) } get isSingleLine(): boolean { return this.start.line === this.end.line } contains(positionOrRange: Position | Range): boolean { if (positionOrRange instanceof Range) { return this.contains(positionOrRange.start) && this.contains(positionOrRange.end) } return positionOrRange.isAfterOrEqual(this.start) && positionOrRange.isBeforeOrEqual(this.end) } isEqual(other: Range): boolean { return this.start.isEqual(other.start) && this.end.isEqual(other.end) } intersection(other: Range): Range | undefined { const start = this.start.isAfter(other.start) ? this.start : other.start const end = this.end.isBefore(other.end) ? this.end : other.end if (start.isAfter(end)) { return undefined } return new Range(start, end) } union(other: Range): Range { const start = this.start.isBefore(other.start) ? this.start : other.start const end = this.end.isAfter(other.end) ? this.end : other.end return new Range(start, end) } with(start?: Position, end?: Position): Range with(change: { start?: Position; end?: Position }): Range with(startOrChange?: Position | { start?: Position; end?: Position }, end?: Position): Range { if (startOrChange instanceof Position) { return new Range(startOrChange, end || this.end) } if (typeof startOrChange === "object") { return new Range(startOrChange.start || this.start, startOrChange.end || this.end) } return new Range(this.start, this.end) } } // Selection class (extends Range) export class Selection extends Range { public anchor: Position public active: Position constructor(anchor: Position, active: Position) constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number) constructor( anchorOrAnchorLine: Position | number, activeOrAnchorCharacter: Position | number, activeLine?: number, activeCharacter?: number, ) { let anchor: Position let active: Position if (typeof anchorOrAnchorLine === "number") { anchor = new Position(anchorOrAnchorLine, activeOrAnchorCharacter as number) active = new Position(activeLine!, activeCharacter!) } else { anchor = anchorOrAnchorLine active = activeOrAnchorCharacter as Position } super(anchor, active) this.anchor = anchor this.active = active } get isReversed(): boolean { return this.anchor.isAfter(this.active) } } // Location class export class Location { constructor( public uri: Uri, public range: Range | Position, ) {} } // Diagnostic-related classes export enum DiagnosticTag { Unnecessary = 1, Deprecated = 2, } export class DiagnosticRelatedInformation { constructor( public location: Location, public message: string, ) {} } export class Diagnostic { range: Range message: string severity: DiagnosticSeverity source?: string code?: string | number | { value: string | number; target: Uri } relatedInformation?: DiagnosticRelatedInformation[] tags?: DiagnosticTag[] constructor(range: Range, message: string, severity?: DiagnosticSeverity) { this.range = range this.message = message this.severity = severity !== undefined ? severity : DiagnosticSeverity.Error } } // DiagnosticCollection interface export interface DiagnosticCollection extends Disposable { name: string set(uri: Uri, diagnostics: Diagnostic[] | undefined): void set(entries: [Uri, Diagnostic[] | undefined][]): void delete(uri: Uri): void clear(): void forEach( callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => void, thisArg?: unknown, ): void get(uri: Uri): Diagnostic[] | undefined has(uri: Uri): boolean } // TextEdit class export class TextEdit { range: Range newText: string constructor(range: Range, newText: string) { this.range = range this.newText = newText } static replace(range: Range, newText: string): TextEdit { return new TextEdit(range, newText) } static insert(position: Position, newText: string): TextEdit { return new TextEdit(new Range(position, position), newText) } static delete(range: Range): TextEdit { return new TextEdit(range, "") } static setEndOfLine(): TextEdit { // Simplified implementation return new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), "") } } // EndOfLine enum export enum EndOfLine { LF = 1, CRLF = 2, } // WorkspaceEdit class export class WorkspaceEdit { private _edits: Map<string, TextEdit[]> = new Map() set(uri: Uri, edits: TextEdit[]): void { this._edits.set(uri.toString(), edits) } get(uri: Uri): TextEdit[] { return this._edits.get(uri.toString()) || [] } has(uri: Uri): boolean { return this._edits.has(uri.toString()) } delete(uri: Uri, range: Range): void { const key = uri.toString() if (!this._edits.has(key)) { this._edits.set(key, []) } this._edits.get(key)!.push(TextEdit.delete(range)) } insert(uri: Uri, position: Position, newText: string): void { const key = uri.toString() if (!this._edits.has(key)) { this._edits.set(key, []) } this._edits.get(key)!.push(TextEdit.insert(position, newText)) } replace(uri: Uri, range: Range, newText: string): void { const key = uri.toString() if (!this._edits.has(key)) { this._edits.set(key, []) } this._edits.get(key)!.push(TextEdit.replace(range, newText)) } get size(): number { return this._edits.size } entries(): [Uri, TextEdit[]][] { return Array.from(this._edits.entries()).map(([uriString, edits]) => [Uri.parse(uriString), edits]) } } // UI Kind enum export enum UIKind { Desktop = 1, Web = 2, } // Extension Mode enum export enum ExtensionMode { Production = 1, Development = 2, Test = 3, } // Code Action Kind mock export class CodeActionKind { static readonly Empty = new CodeActionKind("") static readonly QuickFix = new CodeActionKind("quickfix") static readonly Refactor = new CodeActionKind("refactor") static readonly RefactorExtract = new CodeActionKind("refactor.extract") static readonly RefactorInline = new CodeActionKind("refactor.inline") static readonly RefactorRewrite = new CodeActionKind("refactor.rewrite") static readonly Source = new CodeActionKind("source") static readonly SourceOrganizeImports = new CodeActionKind("source.organizeImports") constructor(public value: string) {} append(parts: string): CodeActionKind { return new CodeActionKind(this.value ? `${this.value}.${parts}` : parts) } intersects(other: CodeActionKind): boolean { return this.contains(other) || other.contains(this) } contains(other: CodeActionKind): boolean { return this.value === other.value || other.value.startsWith(this.value + ".") } } // Theme Color mock export class ThemeColor { constructor(public id: string) {} } // Theme Icon mock export class ThemeIcon { constructor( public id: string, public color?: ThemeColor, ) {} } // Cancellation Token mock export interface CancellationToken { isCancellationRequested: boolean onCancellationRequested: (listener: (e: unknown) => void) => Disposable } export class CancellationTokenSource { private _token: CancellationToken private _isCancelled = false private _onCancellationRequestedEmitter = new EventEmitter<void>() constructor() { this._token = { isCancellationRequested: false, onCancellationRequested: this._onCancellationRequestedEmitter.event, } } get token(): CancellationToken { return this._token } cancel(): void { if (!this._isCancelled) { this._isCancelled = true // Type assertion needed to modify readonly property ;(this._token as { isCancellationRequested: boolean }).isCancellationRequested = true this._onCancellationRequestedEmitter.fire(undefined) } } dispose(): void { this.cancel() this._onCancellationRequestedEmitter.dispose() } } // CodeLens mock export class CodeLens { public range: Range public command?: { command: string; title: string; arguments?: unknown[] } | undefined public isResolved: boolean = false constructor(range: Range, command?: { command: string; title: string; arguments?: unknown[] } | undefined) { this.range = range this.command = command } } // Language Model API mocks (for VSCode LM API) export class LanguageModelTextPart { constructor(public value: string) {} } export class LanguageModelToolCallPart { constructor( public callId: string, public name: string, public input: unknown, ) {} } export class LanguageModelToolResultPart { constructor( public callId: string, public content: unknown[], ) {} } // Decoration Range Behavior mock export enum DecorationRangeBehavior { OpenOpen = 0, ClosedClosed = 1, OpenClosed = 2, ClosedOpen = 3, } // Overview Ruler Lane mock export enum OverviewRulerLane { Left = 1, Center = 2, Right = 4, Full = 7, } // URI class mock export class Uri { public scheme: string public authority: string public path: string public query: string public fragment: string constructor(scheme: string, authority: string, path: string, query: string, fragment: string) { this.scheme = scheme this.authority = authority this.path = path this.query = query this.fragment = fragment } static file(path: string): Uri { return new Uri("file", "", path, "", "") } static parse(value: string): Uri { const url = new URL(value) return new Uri(url.protocol.slice(0, -1), url.hostname, url.pathname, url.search.slice(1), url.hash.slice(1)) } static joinPath(base: Uri, ...pathSegments: string[]): Uri { const joinedPath = path.join(base.path, ...pathSegments) return new Uri(base.scheme, base.authority, joinedPath, base.query, base.fragment) } with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri { return new Uri( change.scheme !== undefined ? change.scheme : this.scheme, change.authority !== undefined ? change.authority : this.authority, change.path !== undefined ? change.path : this.path, change.query !== undefined ? change.query : this.query, change.fragment !== undefined ? change.fragment : this.fragment, ) } get fsPath(): string { return this.path } toString(): string { return `${this.scheme}://${this.authority}${this.path}${this.query ? "?" + this.query : ""}${this.fragment ? "#" + this.fragment : ""}` } } // Output Channel mock export class OutputChannel implements Disposable { private _name: string constructor(name: string) { this._name = name } get name(): string { return this._name } append(value: string): void { logs.info(`[${this._name}] ${value}`, "VSCode.OutputChannel") } appendLine(value: string): void { logs.info(`[${this._name}] ${value}`, "VSCode.OutputChannel") } clear(): void { // No-op for CLI } show(): void { // No-op for CLI } hide(): void { // No-op for CLI } dispose(): void { // No-op for CLI } } // Extension Context mock export class ExtensionContext { public subscriptions: Disposable[] = [] public workspaceState: Memento public globalState: Memento & { setKeysForSync(keys: readonly string[]): void } public secrets: SecretStorage public extensionUri: Uri public extensionPath: string public environmentVariableCollection: Record<string, unknown> = {} public storageUri: Uri | undefined public storagePath: string | undefined public globalStorageUri: Uri public globalStoragePath: string public logUri: Uri public logPath: string public extensionMode: ExtensionMode = ExtensionMode.Production constructor(extensionPath: string, workspacePath: string) { this.extensionPath = extensionPath this.extensionUri = Uri.file(extensionPath) // Setup storage paths using centralized path utility // Initialize workspace to ensure all directories exist KiloCodePaths.initializeWorkspace(workspacePath) const globalStoragePath = KiloCodePaths.getGlobalStorageDir() const workspaceStoragePath = KiloCodePaths.getWorkspaceStorageDir(workspacePath) const logPath = KiloCodePaths.getLogsDir() this.globalStoragePath = globalStoragePath this.globalStorageUri = Uri.file(globalStoragePath) this.storagePath = workspaceStoragePath this.storageUri = Uri.file(workspaceStoragePath) this.logPath = logPath this.logUri = Uri.file(logPath) // Ensure directories exist this.ensureDirectoryExists(globalStoragePath) this.ensureDirectoryExists(workspaceStoragePath) this.ensureDirectoryExists(logPath) // Initialize state storage this.workspaceState = new MemoryMemento(path.join(workspaceStoragePath, "workspace-state.json")) const globalMemento = new MemoryMemento(path.join(globalStoragePath, "global-state.json")) this.globalState = Object.assign(globalMemento, { setKeysForSync: () => {}, // No-op for CLI }) this.secrets = new MockSecretStorage(globalStoragePath) } private ensureDirectoryExists(dirPath: string): void { try { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) } } catch (error) { logs.warn(`Failed to create directory ${dirPath}`, "VSCode.ExtensionContext", { error }) } } } // Memento (state storage) implementation export interface Memento { get<T>(key: string): T | undefined get<T>(key: string, defaultValue: T): T update(key: string, value: unknown): Thenable<void> keys(): readonly string[] } class MemoryMemento implements Memento { private data: Record<string, unknown> = {} private filePath: string constructor(filePath: string) { this.filePath = filePath this.loadFromFile() } private loadFromFile(): void { try { if (fs.existsSync(this.filePath)) { const content = fs.readFileSync(this.filePath, "utf-8") this.data = JSON.parse(content) } } catch (error) { logs.warn(`Failed to load state from ${this.filePath}`, "VSCode.Memento", { error }) this.data = {} } } private saveToFile(): void { try { // Ensure directory exists const dir = path.dirname(this.filePath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2)) } catch (error) { logs.warn(`Failed to save state to ${this.filePath}`, "VSCode.Memento", { error }) } } get<T>(key: string, defaultValue?: T): T | undefined { const value = this.data[key] return value !== undefined && value !== null ? (value as T) : defaultValue } async update(key: string, value: unknown): Promise<void> { if (value === undefined) { delete this.data[key] } else { this.data[key] = value } this.saveToFile() } keys(): readonly string[] { return Object.keys(this.data) } } // Secret Storage mock export interface SecretStorage { get(key: string): Thenable<string | undefined> store(key: string, value: string): Thenable<void> delete(key: string): Thenable<void> } class MockSecretStorage implements SecretStorage { private secrets: Record<string, string> = {} private _onDidChange = new EventEmitter<{ key: string }>() private filePath: string constructor(storagePath: string) { this.filePath = path.join(storagePath, "secrets.json") this.loadFromFile() } private loadFromFile(): void { try { if (fs.existsSync(this.filePath)) { const content = fs.readFileSync(this.filePath, "utf-8") this.secrets = JSON.parse(content) } } catch (error) { logs.warn(`Failed to load secrets from ${this.filePath}`, "VSCode.MockSecretStorage", { error }) this.secrets = {} } } private saveToFile(): void { try { // Ensure directory exists const dir = path.dirname(this.filePath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } fs.writeFileSync(this.filePath, JSON.stringify(this.secrets, null, 2)) } catch (error) { logs.warn(`Failed to save secrets to ${this.filePath}`, "VSCode.MockSecretStorage", { error }) } } async get(key: string): Promise<string | undefined> { return this.secrets[key] } async store(key: string, value: string): Promise<void> { this.secrets[key] = value this.saveToFile() this._onDidChange.fire({ key }) } async delete(key: string): Promise<void> { delete this.secrets[key] this.saveToFile() this._onDidChange.fire({ key }) } get onDidChange() { return this._onDidChange.event } } // FileSystem API mock export enum FileType { Unknown = 0, File = 1, Directory = 2, SymbolicLink = 64, } // FileSystemError class mock export class FileSystemError extends Error { public code: string constructor(message: string, code: string = "Unknown") { super(message) this.name = "FileSystemError" this.code = code } static FileNotFound(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `File not found: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "FileNotFound") } static FileExists(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `File exists: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "FileExists") } static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `File is not a directory: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "FileNotADirectory") } static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `File is a directory: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "FileIsADirectory") } static NoPermissions(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `No permissions: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "NoPermissions") } static Unavailable(messageOrUri?: string | Uri): FileSystemError { const message = typeof messageOrUri === "string" ? messageOrUri : `Unavailable: ${messageOrUri?.fsPath || "unknown"}` return new FileSystemError(message, "Unavailable") } } export interface FileStat { type: FileType ctime: number mtime: number size: number } export class FileSystemAPI { async stat(uri: Uri): Promise<FileStat> { try { const stats = fs.statSync(uri.fsPath) return { type: stats.isDirectory() ? FileType.Directory : FileType.File, ctime: stats.ctimeMs, mtime: stats.mtimeMs, size: stats.size, } } catch { // If file doesn't exist, assume it's a file for CLI purposes return { type: FileType.File, ctime: Date.now(), mtime: Date.now(), size: 0, } } } async readFile(uri: Uri): Promise<Uint8Array> { try { const content = fs.readFileSync(uri.fsPath) return new Uint8Array(content) } catch (error) { // Check if it's a file not found error (ENOENT) if ((error as NodeJS.ErrnoException).code === "ENOENT") { throw FileSystemError.FileNotFound(uri) } // For other errors, throw a generic FileSystemError throw new FileSystemError(`Failed to read file: ${uri.fsPath}`) } } async writeFile(uri: Uri, content: Uint8Array): Promise<void> { try { fs.writeFileSync(uri.fsPath, content) } catch { throw new Error(`Failed to write file: ${uri.fsPath}`) } } async delete(uri: Uri): Promise<void> { try { fs.unlinkSync(uri.fsPath) } catch { throw new Error(`Failed to delete file: ${uri.fsPath}`) } } async createDirectory(uri: Uri): Promise<void> { try { fs.mkdirSync(uri.fsPath, { recursive: true }) } catch { throw new Error(`Failed to create directory: ${uri.fsPath}`) } } } // Workspace API mock export class WorkspaceAPI { public workspaceFolders: WorkspaceFolder[] | undefined public name: string | undefined public workspaceFile: Uri | undefined public fs: FileSystemAPI public textDocuments: TextDocument[] = [] private _onDidChangeWorkspaceFolders = new EventEmitter<WorkspaceFoldersChangeEvent>() private _onDidOpenTextDocument = new EventEmitter<TextDocument>() private _onDidChangeTextDocument = new EventEmitter<TextDocumentChangeEvent>() private _onDidCloseTextDocument = new EventEmitter<TextDocument>() private workspacePath: string private context: ExtensionContext constructor(workspacePath: string, context: ExtensionContext) { this.workspacePath = workspacePath this.context = context this.workspaceFolders = [ { uri: Uri.file(workspacePath), name: path.basename(workspacePath), index: 0, }, ] this.name = path.basename(workspacePath) this.fs = new FileSystemAPI() } asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { const fsPath = typeof pathOrUri === "string" ? pathOrUri : pathOrUri.fsPath // If no workspace folders, return the original path if (!this.workspaceFolders || this.workspaceFolders.length === 0) { return fsPath } // Try to find a workspace folder that contains this path for (const folder of this.workspaceFolders) { const workspacePath = folder.uri.fsPath // Normalize paths for comparison (handle different path separators) const normalizedFsPath = path.normalize(fsPath) const normalizedWorkspacePath = path.normalize(workspacePath) // Check if the path is within this workspace folder if (normalizedFsPath.startsWith(normalizedWorkspacePath)) { // Get the relative path let relativePath = path.relative(normalizedWorkspacePath, normalizedFsPath) // If includeWorkspaceFolder is true and there are multiple workspace folders, // prepend the workspace folder name if (includeWorkspaceFolder && this.workspaceFolders.length > 1) { relativePath = path.join(folder.name, relativePath) } return relativePath } } // If not within any workspace folder, return the original path return fsPath } onDidChangeWorkspaceFolders(listener: (event: WorkspaceFoldersChangeEvent) => void): Disposable { return this._onDidChangeWorkspaceFolders.event(listener) } onDidChangeConfiguration(listener: (event: ConfigurationChangeEvent) => void): Disposable { // Create a mock configuration change event emitter const emitter = new EventEmitter<ConfigurationChangeEvent>() return emitter.event(listener) } onDidChangeTextDocument(listener: (event: TextDocumentChangeEvent) => void): Disposable { return this._onDidChangeTextDocument.event(listener) } onDidOpenTextDocument(listener: (event: TextDocument) => void): Disposable { logs.debug("Registering onDidOpenTextDocument listener", "VSCode.Workspace") return this._onDidOpenTextDocument.event(listener) } onDidCloseTextDocument(listener: (event: TextDocument) => void): Disposable { return this._onDidCloseTextDocument.event(listener) } getConfiguration(section?: string): WorkspaceConfiguration { return new MockWorkspaceConfiguration(section, this.context) } findFiles(_include: string, _exclude?: string): Thenable<Uri[]> { // Basic implementation - could be enhanced with glob patterns return Promise.resolve([]) } async openTextDocument(uri: Uri): Promise<TextDocument> { logs.debug(`openTextDocument called for: ${uri.fsPath}`, "VSCode.Workspace") // Read file content let content = "" try { content = fs.readFileSync(uri.fsPath, "utf-8") logs.debug(`File content read successfully, length: ${content.length}`, "VSCode.Workspace") } catch (error) { logs.warn(`Failed to read file: ${uri.fsPath}`, "VSCode.Workspace", { error }) } const lines = content.split("\n") const document = { uri, fileName: uri.fsPath, languageId: "plaintext", version: 1, isDirty: false, isClosed: false, lineCount: lines.length, getText: (range?: Range) => { if (!range) { return content } return lines.slice(range.start.line, range.end.line + 1).join("\n") }, lineAt: (line: number) => { const text = lines[line] || "" return { text, range: new Range(new Position(line, 0), new Position(line, text.length)), rangeIncludingLineBreak: new Range(new Position(line, 0), new Position(line + 1, 0)), firstNonWhitespaceCharacterIndex: text.search(/\S/), isEmptyOrWhitespace: text.trim().length === 0, } }, offsetAt: (position: Position) => { let offset = 0 for (let i = 0; i < position.line && i < lines.length; i++) { offset += (lines[i]?.length || 0) + 1 // +1 for newline } offset += position.character return offset }, positionAt: (offset: number) => { let currentOffset = 0 for (let i = 0; i < lines.length; i++) { const lineLength = (lines[i]?.length || 0) + 1 // +1 for newline if (currentOffset + lineLength > offset) { return new Position(i, offset - currentOffset) } currentOffset += lineLength } return new Position(lines.length - 1, lines[lines.length - 1]?.length || 0) }, save: () => Promise.resolve(true), validateRange: (range: Range) => range, validatePosition: (position: Position) => position, } // Add to textDocuments array this.textDocuments.push(document) logs.debug(`Document added to textDocuments array, total: ${this.textDocuments.length}`, "VSCode.Workspace") // Fire the event after a small delay to ensure listeners are fully registered logs.debug("Waiting before firing onDidOpenTextDocument", "VSCode.Workspace") await new Promise((resolve) => setTimeout(resolve, 10)) logs.debug("Firing onDidOpenTextDocument event", "VSCode.Workspace") this._onDidOpenTextDocument.fire(document) logs.debug("onDidOpenTextDocument event fired", "VSCode.Workspace") return document } async applyEdit(edit: WorkspaceEdit): Promise<boolean> { // In CLI mode, we need to apply the edits to the actual files try { for (const [uri, edits] of edit.entries()) { let filePath = uri.fsPath // On Windows, strip leading slash if present (e.g., /C:/path becomes C:/path) if (process.platform === "win32" && filePath.startsWith("/")) { filePath = filePath.slice(1) } let content = "" // Read existing content if file exists try { content = fs.readFileSync(filePath, "utf-8") } catch { // File doesn't exist, start with empty content } // Apply edits in reverse order to maintain correct positions const sortedEdits = edits.sort((a, b) => { const lineDiff = b.range.start.line - a.range.start.line if (lineDiff !== 0) return lineDiff return b.range.start.character - a.range.start.character }) const lines = content.split("\n") for (const textEdit of sortedEdits) { const startLine = textEdit.range.start.line const startChar = textEdit.range.start.character const endLine = textEdit.range.end.line const endChar = textEdit.range.end.character if (startLine === endLine) { // Single line edit const line = lines[startLine] || "" lines[startLine] = line.substring(0, startChar) + textEdit.newText + line.substring(endChar) } else { // Multi-line edit const firstLine = lines[startLine] || "" const lastLine = lines[endLine] || "" const newContent = firstLine.substring(0, startChar) + textEdit.newText + lastLine.substring(endChar) lines.splice(startLine, endLine - startLine + 1, newContent) } } // Write back to file const newContent = lines.join("\n") fs.writeFileSync(filePath, newContent, "utf-8") // Update the in-memory document object to reflect the new content // This is critical for CLI mode where DiffViewProvider reads from the document object const document = this.textDocuments.find((doc: TextDocument) => doc.uri.fsPath === filePath) if (document) { const newLines = newContent.split("\n") // Update document properties with new content document.lineCount = newLines.length document.getText = (range?: Range) => { if (!range) { return newContent } return newLines.slice(range.start.line, range.end.line + 1).join("\n") } document.lineAt = (line: number) => { const text = newLines[line] || "" return { text, range: new Range(new Position(line, 0), new Position(line, text.length)), rangeIncludingLineBreak: new Range(new Position(line, 0), new Position(line + 1, 0)), firstNonWhitespaceCharacterIndex: text.search(/\S/), isEmptyOrWhitespace: text.trim().length === 0, } } document.offsetAt = (position: Position) => { let offset = 0 for (let i = 0; i < position.line && i < newLines.length; i++) { offset += (newLines[i]?.length || 0) + 1 // +1 for newline } offset += position.character return offset } document.positionAt = (offset: number) => { let currentOffset = 0 for (let i = 0; i < newLines.length; i++) { const lineLength = (newLines[i]?.length || 0) + 1 // +1 for newline if (currentOffset + lineLength > offset) { return new Position(i, offset - currentOffset) } currentOffset += lineLength } return new Position(newLines.length - 1, newLines[newLines.length - 1]?.length || 0) } } } return true } catch (error) { logs.error("Failed to apply workspace edit", "VSCode.Workspace", { error }) return false } } createFileSystemWatcher( _globPattern: string | RelativePattern, _ignoreCreateEvents?: boolean, _ignoreChangeEvents?: boolean, _ignoreDeleteEvents?: boolean, ): FileSystemWatcher { const emitter = new EventEmitter<Uri>() return { onDidChange: (listener: (e: Uri) => void) => emitter.event(listener), onDidCreate: (listener: (e: Uri) => void) => emitter.event(listener), onDidDelete: (listener: (e: Uri) => void) => emitter.event(listener), dispose: () => emitter.dispose(), } } registerTextDocumentContentProvider(_scheme: string, _provider: TextDocumentContentProvider): Disposable { return { dispose: () => {} } } } export interface WorkspaceFolder { uri: Uri name: string index: number } export interface WorkspaceConfiguration { get<T>(section: string): T | undefined get<T>(section: string, defaultValue: T): T has(section: string): boolean inspect(section: string): ConfigurationInspect<unknown> | undefined update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Thenable<void> } export class MockWorkspaceConfiguration implements WorkspaceConfiguration { private section: string | undefined private globalMemento: MemoryMemento private workspaceMemento: MemoryMemento constructor(section?: string, context?: ExtensionContext) { this.section = section if (context) { // Use the extension context's mementos this.globalMemento = context.globalState as unknown as MemoryMemento this.workspaceMemento = context.workspaceState as unknown as MemoryMemento } else { // Fallback: create our own mementos (shouldn't happen in normal usage) const globalStoragePath = KiloCodePaths.getGlobalStorageDir() const workspaceStoragePath = KiloCodePaths.getWorkspaceStorageDir(process.cwd()) this.ensureDirectoryExists(globalStoragePath) this.ensureDirectoryExists(workspaceStoragePath) this.globalMemento = new MemoryMemento(path.join(globalStoragePath, "configuration.json")) this.workspaceMemento = new MemoryMemento(path.join(workspaceStoragePath, "configuration.json")) } } private ensureDirectoryExists(dirPath: string): void { try { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) } } catch (error) { logs.warn(`Failed to create directory ${dirPath}`, "VSCode.MockWorkspaceConfiguration", { error }) } } get<T>(section: string, defaultValue?: T): T | undefined { const fullSection = this.section ? `${this.section}.${section}` : section // Check workspace configuration first (higher priority) const workspaceValue = this.workspaceMemento.get(fullSection) if (workspaceValue !== undefined && workspaceValue !== null) { return workspaceValue as T } // Check global configuration const globalValue = this.globalMemento.get(fullSection) if (globalValue !== undefined && globalValue !== null) { return globalValue as T } // Return default value return defaultValue } has(section: string): boolean { const fullSection = this.section ? `${this.section}.${section}` : section return this.workspaceMemento.get(fullSection) !== undefined || this.globalMemento.get(fullSection) !== undefined } inspect(section: string): ConfigurationInspect<unknown> | undefined { const fullSection = this.section ? `${this.section}.${section}` : section const workspaceValue = this.workspaceMemento.get(fullSection) const globalValue = this.globalMemento.get(fullSection) if (workspaceValue !== undefined || globalValue !== undefined) { return { key: fullSection, defaultValue: undefined, globalValue: globalValue, workspaceValue: workspaceValue, workspaceFolderValue: undefined, } } return undefined } async update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Promise<void> { const fullSection = this.section ? `${this.section}.${section}` : section try { // Determine which memento to use based on configuration target const memento = configurationTarget === ConfigurationTarget.Workspace ? this.workspaceMemento : this.globalMemento const scope = configurationTarget === ConfigurationTarget.Workspace ? "workspace" : "global" // Update the memento (this automatically persists to disk) await memento.update(fullSection, value) logs.debug( `Configuration updated: ${fullSection} = ${JSON.stringify(value)} (${scope})`, "VSCode.MockWorkspaceConfiguration", ) } catch (error) { logs.error(`Failed to update configuration: ${fullSection}`, "VSCode.MockWorkspaceConfiguration", { error, }) throw error } } // Additional method to reload configuration from disk public reload(): void { // MemoryMemento automatically loads from disk, so we don't need to do anything special logs.debug("Configuration reload requested", "VSCode.MockWorkspaceConfiguration") } // Method to get all configuration data (useful for debugging and generic config loading) public getAllConfig(): Record<string, unknown> { const globalKeys = this.globalMemento.keys() const workspaceKeys = this.workspaceMemento.keys() const allConfig: Record<string, unknown> = {} // Add global settings first for (const key of globalKeys) { const value = this.globalMemento.get(key) if (value !== undefined && value !== null) { allConfig[key] = value } } // Add workspace settings (these override global) for (const key of workspaceKeys) { const value = this.workspaceMemento.get(key) if (value !== undefined && value !== null) { allConfig[key] = value } } return allConfig } } // Text Editor Decoration Type mock export class TextEditorDecorationType implements Disposable { public key: string constructor(key: string) { this.key = key } dispose(): void { // No-op for CLI } } // StatusBarItem mock export class StatusBarItem implements Disposable { private _text: string = "" private _tooltip: string | undefined private _command: string | undefined private _color: string | undefined private _backgroundColor: string | undefined private _isVisible: boolean = false constructor( public readonly alignment: StatusBarAlignment, public readonly priority?: number, ) {} get text(): string { return this._text } set text(value: string) { this._text = value } get tooltip(): string | undefined { return this._tooltip } set tooltip(value: string | undefined) { this._tooltip = value } get command(): string | undefined { return this._command } set command(value: string | undefined) { this._command = value } get color(): string | undefined { return this._color } set color(value: string | undefined) { this._color = value } get backgroundColor(): string | undefined { return this._backgroundColor } set backgroundColor(value: string | undefined) { this._backgroundColor = value } show(): void { this._isVisible = true } hide(): void { this._isVisible = false } dispose(): void { this._isVisible = false } } // Tab and TabGroup interfaces for VSCode API export interface Tab { input: TabInputText | unknown label: string isActive: boolean isDirty: boolean } export interface TabInputText { uri: Uri } export interface TabGroup { tabs: Tab[] } // TabGroups API mock export class TabGroupsAPI { private _onDidChangeTabs = new EventEmitter<void>() private _tabGroups: TabGroup[] = [] get all(): TabGroup[] { return this._tabGroups } onDidChangeTabs(listener: () => void): Disposable { return this._onDidChangeTabs.event(listener) } async close(tab: Tab): Promise<boolean> { // Find and remove the tab from all groups for (const group of this._tabGroups) { const index = group.tabs.indexOf(tab) if (index !== -1) { group.tabs.splice(index, 1) this._onDidChangeTabs.fire() return true } } return false } // Internal method to simulate tab changes for CLI _simulateTabChange(): void { this._onDidChangeTabs.fire() } dispose(): void { this._onDidChangeTabs.dispose() } } // Window API mock export class WindowAPI { public tabGroups: TabGroupsAPI public visibleTextEditors: TextEditor[] = [] public _onDidChangeVisibleTextEditors = new EventEmitter<TextEditor[]>() private _workspace?: WorkspaceAPI constructor() { this.tabGroups = new TabGroupsAPI() } setWorkspace(workspace: WorkspaceAPI) { this._workspace = workspace } createOutputChannel(name: string): OutputChannel { return new OutputChannel(name) } createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem createStatusBarItem(id?: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem createStatusBarItem( idOrAlignment?: string | StatusBarAlignment, alignmentOrPriority?: StatusBarAlignment | number, priority?: number, ): StatusBarItem { // Handle overloaded signatures let actualAlignment: StatusBarAlignment let actualPriority: number | undefined if (typeof idOrAlignment === "string") { // Called with id, alignment, priority actualAlignment = (alignmentOrPriority as StatusBarAlignment) ?? StatusBarAlignment.Left actualPriority = priority } else { // Called with alignment, priority actualAlignment = (idOrAlignment as StatusBarAlignment) ?? StatusBarAlignment.Left actualPriority = alignmentOrPriority as number | undefined } return new StatusBarItem(actualAlignment, actualPriority) } createTextEditorDecorationType(_options: DecorationRenderOptions): TextEditorDecorationType { return new TextEditorDecorationType(`decoration-${Date.now()}`) } createTerminal(options?: { name?: string shellPath?: string shellArgs?: string[] cwd?: string env?: { [key: string]: string | null | undefined } iconPath?: ThemeIcon hideFromUser?: boolean message?: string strictEnv?: boolean }): Terminal { // Return a mock terminal object return { name: options?.name || "Terminal", processId: Promise.resolve(undefined), creationOptions: options || {}, exitStatus: undefined, state: { isInteractedWith: false }, sendText: (text: string, _addNewLine?: boolean) => { logs.debug(`Terminal sendText: ${text}`, "VSCode.Terminal") }, show: (_preserveFocus?: boolean) => { logs.debug("Terminal show called", "VSCode.Terminal") }, hide: () => { logs.debug("Terminal hide called", "VSCode.Terminal") }, dispose: () => { logs.debug("Terminal disposed", "VSCode.Terminal") }, } } showInformationMessage(message: string, ..._items: string[]): Thenable<string | undefined> { logs.info(message, "VSCode.Window") return Promise.resolve(undefined) } showWarningMessage(message: string, ..._items: string[]): Thenable<string | undefined> { logs.warn(message, "VSCode.Window") return Promise.resolve(undefined) } showErrorMessage(message: string, ..._items: string[]): Thenable<string | undefined> { logs.error(message, "VSCode.Window") return Promise.resolve(undefined) } showQuickPick(items: string[], _options?: QuickPickOptions): Thenable<string | undefined> { // Return first item for CLI return Promise.resolve(items[0]) } showInputBox(_options?: InputBoxOptions): Thenable<string | undefined> { // Return empty string for CLI return Promise.resolve("") } showOpenDialog(_options?: OpenDialogOptions): Thenable<Uri[] | undefined> { // Return empty array for CLI return Promise.resolve([]) } async showTextDocument( documentOrUri: TextDocument | Uri, columnOrOptions?: ViewColumn | TextDocumentShowOptions, _preserveFocus?: boolean, ): Promise<TextEditor> { // Mock implementation for CLI // In a real VSCode environment, this would open the document in an editor const uri = documentOrUri instanceof Uri ? documentOrUri : documentOrUri.uri logs.debug(`showTextDocument called for: ${uri?.toString() || "unknown"}`, "VSCode.Window") // Create a placeholder editor first so it's in visibleTextEditors when onDidOpenTextDocument fires const placeholderEditor: TextEditor = { document: { uri } as TextDocument, selection: new Selection(new Position(0, 0), new Position(0, 0)), selections: [new Selection(new Position(0, 0), new Position(0, 0))], visibleRanges: [new Range(new Position(0, 0), new Position(0, 0))], options: {}, viewColumn: typeof columnOrOptions === "number" ? columnOrOptions : ViewColumn.One, edit: () => Promise.resolve(true), insertSnippet: () => Promise.resolve(true), setDecorations: () => {}, revealRange: () => {}, show: () => {}, hide: () => {}, } // Add placeholder to visible editors BEFORE opening document this.visibleTextEditors.push(placeholderEditor) logs.debug( `Placeholder editor added to visibleTextEditors, total: ${this.visibleTextEditors.length}`, "VSCode.Window", ) // If we have a URI, open the document (this will fire onDidOpenTextDocument) let document = documentOrUri if (documentOrUri instanceof Uri && this._workspace) { logs.debug("Opening document via workspace.openTextDocument", "VSCode.Window") document = await this._workspace.openTextDocument(uri) logs.debug("Document opened successfully", "VSCode.Window") // Update the placeholder editor with the real document placeholderEditor.document = document } // Fire events immediately using setImmediate setImmediate(() => { logs.debug("Firing onDidChangeVisibleTextEditors event", "VSCode.Window") this._onDidChangeVisibleTextEditors.fire(this.visibleTextEditors) logs.debug("onDidChangeVisibleTextEditors event fired", "VSCode.Window") }) logs.debug("Returning editor from showTextDocument", "VSCode.Window") return placeholderEditor } registerWebviewViewProvider( viewId: string, provider: WebviewViewProvider, _options?: WebviewViewProviderOptions, ): Disposable { // Store the provider for later use by ExtensionHost if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { const extensionHost = ( global as unknown as { __extensionHost: { registerWebviewProvider: (viewId: string, provider: WebviewViewProvider) => void isInInitialSetup: () => boolean markWebviewReady: () => void } } ).__extensionHost extensionHost.registerWebviewProvider(viewId, provider) // Set up webview mock that captures messages from the extension const mockWebview = { postMessage: (message: unknown): Thenable<boolean> => { // Forward extension messages to ExtensionHost for CLI consumption if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { ;( global as unknown as { __extensionHost: { emit: (event: string, message: unknown) => void } } ).__extensionHost.emit("extensionWebviewMessage", message) } return Promise.resolve(true) }, onDidReceiveMessage: (listener: (message: unknown) => void) => { // This is how the extension listens for messages from the webview // We need to connect this to our message bridge if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { ;( global as unknown as { __extensionHost: { on: (event: string, listener: (message: unknown) => void) => void } } ).__extensionHost.on("webviewMessage", listener) } return { dispose: () => {} } }, asWebviewUri: (uri: Uri) => { // Convert file URIs to webview-compatible URIs // For CLI, we can just return a mock webview URI return Uri.parse(`vscode-webview://webview/${uri.path}`) }, html: "", options: {}, cspSource: "vscode-webview:", } // Provide the mock webview to the provider if (provider.resolveWebviewView) { const mockWebviewView = { webview: mockWebview as Webview, viewType: viewId, title: viewId, description: undefined as string | undefined, badge: undefined as ViewBadge | undefined, show: () => {}, onDidChangeVisibility: () => ({ dispose: () => {} }), onDidDispose: () => ({ dispose: () => {} }), visible: true, } // Call resolveWebviewView immediately with initialization context // No setTimeout needed - use event-based synchronization instead ;(async () => { try { // Pass isInitialSetup flag in context to prevent task abortion const context = { preserveFocus: false, isInitialSetup: extensionHost.isInInitialSetup(), } logs.debug( `Calling resolveWebviewView with isInitialSetup=${context.isInitialSetup}`, "VSCode.Window", ) // Await the result to ensure webview is fully initialized before marking ready await provider.resolveWebviewView(mockWebviewView as WebviewView, {}, {} as CancellationToken) // Mark webview as ready after resolution completes extensionHost.markWebviewReady() logs.debug("Webview resolution complete, marked as ready", "VSCode.Window") } catch (error) { logs.error("Error resolving webview view", "VSCode.Window", { error }) } })() } } return { dispose: () => { if ((global as unknown as { __extensionHost?: unknown }).__extensionHost) { ;( global as unknown as { __extensionHost: { unregisterWebviewProvider: (viewId: string) => void } } ).__extensionHost.unregisterWebviewProvider(viewId) } }, } } registerUriHandler(_handler: UriHandler): Disposable { // Store the URI handler for later use return { dispose: () => {}, } } onDidChangeTextEditorSelection(listener: (event: TextEditorSelectionChangeEvent) => void): Disposable { const emitter = new EventEmitter<TextEditorSelectionChangeEvent>() return emitter.event(listener) } onDidChangeActiveTextEditor(listener: (event: TextEditor | undefined) => void): Disposable { const emitter = new EventEmitter<TextEditor | undefined>() return emitter.event(listener) } onDidChangeVisibleTextEditors(listener: (editors: TextEditor[]) => void): Disposable { return this._onDidChangeVisibleTextEditors.event(listener) } // Terminal event handlers onDidCloseTerminal(_listener: (terminal: Terminal) => void): Disposable { return { dispose: () => {} } } onDidOpenTerminal(_listener: (terminal: Terminal) => void): Disposable { return { dispose: () => {} } } onDidChangeActiveTerminal(_listener: (terminal: Terminal | undefined) => void): Disposable { return { dispose: () => {} } } onDidChangeTerminalDimensions(_listener: (event: TerminalDimensionsChangeEvent) => void): Disposable { return { dispose: () => {} } } onDidWriteTerminalData(_listener: (event: TerminalDataWriteEvent) => void): Disposable { return { dispose: () => {} } } get activeTerminal(): Terminal | undefined { return undefined } get terminals(): Terminal[] { return [] } } export interface WorkspaceFolder { uri: Uri name: string index: number } export interface WorkspaceConfiguration { get<T>(section: string): T | undefined get<T>(section: string, defaultValue: T): T has(section: string): boolean inspect(_section: string): ConfigurationInspect<unknown> | undefined update(section: string, value: unknown, configurationTarget?: ConfigurationTarget): Thenable<void> } // Commands API mock // Commands API mock export class CommandsAPI { private commands: Map<string, (...args: unknown[]) => unknown> = new Map() registerCommand(command: string, callback: (...args: unknown[]) => unknown): Disposable { this.commands.set(command, callback) return { dispose: () => { this.commands.delete(command) }, } } executeCommand<T = unknown>(command: string, ...rest: unknown[]): Thenable<T> { const handler = this.commands.get(command) if (handler) { try { const result = handler(...rest) return Promise.resolve(result as T) } catch (error) { return Promise.reject(error) } } // Handle built-in commands switch (command) { case "workbench.action.files.saveFiles": case "workbench.action.closeWindow": case "workbench.action.reloadWindow": return Promise.resolve(undefined as T) case "vscode.diff": // Simulate opening a diff view for the CLI // The extension's DiffViewProvider expects this to create a diff editor return this.handleDiffCommand( rest[0] as Uri, rest[1] as Uri, rest[2] as string | undefined, rest[3], ) as Thenable<T> default: logs.warn(`Unknown command: ${command}`, "VSCode.Commands") return Promise.resolve(undefined as T) } } private async handleDiffCommand( originalUri: Uri, modifiedUri: Uri, title?: string, _options?: unknown, ): Promise<void> { // The DiffViewProvider is waiting for the modified document to appear in visibleTextEditors // We need to simulate this by opening the document and adding it to visible editors logs.info(`[DIFF] Handling vscode.diff command`, "VSCode.Commands", { originalUri: originalUri?.toString(), modifiedUri: modifiedUri?.toString(), title, }) if (!modifiedUri) { logs.warn("[DIFF] vscode.diff called without modified URI", "VSCode.Commands") return } // Get the workspace API to open the document const workspace = (global as unknown as { vscode?: { workspace?: WorkspaceAPI } }).vscode?.workspace const window = (global as unknown as { vscode?: { window?: WindowAPI } }).vscode?.window if (!workspace || !window) { logs.warn("[DIFF] VSCode APIs not available for diff command", "VSCode.Commands") return } logs.info( `[DIFF] Current visibleTextEditors count: ${window.visibleTextEditors?.length || 0}`, "VSCode.Commands", ) try { // The document should already be open from the showTextDocument call // Find it in the existing textDocuments logs.info(`[DIFF] Looking for already-opened document: ${modifiedUri.fsPath}`, "VSCode.Commands") let document = workspace.textDocuments.find((doc: TextDocument) => doc.uri.fsPath === modifiedUri.fsPath) if (!document) { // If not found, open it now logs.info(`[DIFF] Document not found, opening: ${modifiedUri.fsPath}`, "VSCode.Commands") document = await workspace.openTextDocument(modifiedUri) logs.info(`[DIFF] Document opened successfully, lineCount: ${document.lineCount}`, "VSCode.Commands") } else { logs.info(`[DIFF] Found existing document, lineCount: ${document.lineCount}`, "VSCode.Commands") } // Create a mock editor for the diff view const mockEditor = { document, selection: new Selection(new Position(0, 0), new Position(0, 0)), selections: [new Selection(new Position(0, 0), new Position(0, 0))], visibleRanges: [new Range(new Position(0, 0), new Position(0, 0))], options: {}, viewColumn: ViewColumn.One, edit: async (callback: (editBuilder: TextEditorEdit) => void) => { // Create a mock edit builder const editBuilder: TextEditorEdit = { replace: (_range: Range, _text: string) => { // In CLI mode, we don't actually edit here // The DiffViewProvider will handle the actual edits logs.debug("Mock edit builder replace called", "VSCode.Commands") }, insert: (_position: Position, _text: string) => { logs.debug("Mock edit builder insert called", "VSCode.Commands") }, delete: (_range: Range) => { logs.debug("Mock edit builder delete called", "VSCode.Commands") }, setEndOfLine: (_endOfLine: EndOfLine) => { logs.debug("Mock edit builder setEndOfLine called", "VSCode.Commands") }, } callback(editBuilder) return true }, insertSnippet: () => Promise.resolve(true), setDecorations: () => {}, revealRange: () => {}, show: () => {}, hide: () => {}, } // Add the editor to visible editors if (!window.visibleTextEditors) { window.visibleTextEditors = [] } // Check if this editor is already in visibleTextEditors (from showTextDocument) const existingEditor = window.visibleTextEditors.find( (e: TextEditor) => e.document.uri.fsPath === modifiedUri.fsPath, ) if (existingEditor) { logs.info(`[DIFF] Editor already in visibleTextEditors, updating it`, "VSCode.Commands") // Update the existing editor with the mock editor properties Object.assign(existingEditor, mockEditor) } else { logs.info(`[DIFF] Adding new mock editor to visibleTextEditors`, "VSCode.Commands") window.visibleTextEditors.push(mockEditor) } logs.info(`[DIFF] visibleTextEditors count: ${window.visibleTextEditors.length}`, "VSCode.Commands") // The onDidChangeVisibleTextEditors event was already fired by showTextDocument // We don't need to fire it again here logs.info( `[DIFF] Diff view simulation complete (events already fired by showTextDocument)`, "VSCode.Commands", ) } catch (error) { logs.error("[DIFF] Error simulating diff view", "VSCode.Commands", { error }) } } } // Main VSCode API mock export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: string, identity?: IdentityInfo) { const context = new ExtensionContext(extensionRootPath, workspacePath) const workspace = new WorkspaceAPI(workspacePath, context) const window = new WindowAPI() const commands = new CommandsAPI() // Link window and workspace for cross-API calls window.setWorkspace(workspace) // Environment mock with identity values const env = { appName: `wrapper|cli|cli|${Package.version}`, appRoot: import.meta.dirname, language: "en", machineId: identity?.machineId || "cli-machine-id", sessionId: identity?.sessionId || "cli-session-id", remoteName: undefined, shell: process.env.SHELL || "/bin/bash", uriScheme: "vscode", uiKind: 1, // Desktop openExternal: async (uri: Uri): Promise<boolean> => { logs.info(`Would open external URL: ${uri.toString()}`, "VSCode.Env") return true }, clipboard: { readText: async (): Promise<string> => { logs.debug("Clipboard read requested", "VSCode.Clipboard") return "" }, writeText: async (text: string): Promise<void> => { logs.debug( `Clipboard write: ${text.substring(0, 100)}${text.length > 100 ? "..." : ""}`, "VSCode.Clipboard", ) }, }, } return { version: "1.84.0", Uri, EventEmitter, ConfigurationTarget, ViewColumn, TextEditorRevealType, StatusBarAlignment, DiagnosticSeverity, DiagnosticTag, Position, Range, Selection, Location, Diagnostic, DiagnosticRelatedInformation, TextEdit, WorkspaceEdit, EndOfLine, UIKind, ExtensionMode, CodeActionKind, ThemeColor, ThemeIcon, DecorationRangeBehavior, OverviewRulerLane, StatusBarItem, CancellationToken: class CancellationTokenClass implements CancellationToken { isCancellationRequested = false onCancellationRequested = (_listener: (e: unknown) => void) => ({ dispose: () => {} }) }, CancellationTokenSource, CodeLens, LanguageModelTextPart, LanguageModelToolCallPart, LanguageModelToolResultPart, ExtensionContext, FileType, FileSystemError, Disposable: class DisposableClass implements Disposable { dispose(): void { // No-op for CLI } static from(...disposables: Disposable[]): Disposable { return { dispose: () => { disposables.forEach((d) => d.dispose()) }, } } }, TabInputText: class TabInputText { constructor(public uri: Uri) {} }, TabInputTextDiff: class TabInputTextDiff { constructor( public original: Uri, public modified: Uri, ) {} }, workspace, window, commands, env, context, // Add more APIs as needed languages: { registerCodeActionsProvider: () => ({ dispose: () => {} }), registerCodeLensProvider: () => ({ dispose: () => {} }), registerCompletionItemProvider: () => ({ dispose: () => {} }), registerHoverProvider: () => ({ dispose: () => {} }), registerDefinitionProvider: () => ({ dispose: () => {} }), registerReferenceProvider: () => ({ dispose: () => {} }), registerDocumentSymbolProvider: () => ({ dispose: () => {} }), registerWorkspaceSymbolProvider: () => ({ dispose: () => {} }), registerRenameProvider: () => ({ dispose: () => {} }), registerDocumentFormattingEditProvider: () => ({ dispose: () => {} }), registerDocumentRangeFormattingEditProvider: () => ({ dispose: () => {} }), registerSignatureHelpProvider: () => ({ dispose: () => {} }), getDiagnostics: (uri?: Uri): [Uri, Diagnostic[]][] | Diagnostic[] => { // In CLI mode, we don't have real diagnostics // Return empty array or empty diagnostics for the specific URI if (uri) { return [] } return [] }, createDiagnosticCollection: (name?: string): DiagnosticCollection => { const diagnostics = new Map<string, Diagnostic[]>() const collection: DiagnosticCollection = { name: name || "default", set: ( uriOrEntries: Uri | [Uri, Diagnostic[] | undefined][], diagnosticsOrUndefined?: Diagnostic[] | undefined, ) => { if (Array.isArray(uriOrEntries)) { // Handle array of entries for (const [uri, diags] of uriOrEntries) { if (diags === undefined) { diagnostics.delete(uri.toString()) } else { diagnostics.set(uri.toString(), diags) } } } else { // Handle single URI if (diagnosticsOrUndefined === undefined) { diagnostics.delete(uriOrEntries.toString()) } else { diagnostics.set(uriOrEntries.toString(), diagnosticsOrUndefined) } } }, delete: (uri: Uri) => { diagnostics.delete(uri.toString()) }, clear: () => { diagnostics.clear() }, forEach: ( callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => void, thisArg?: unknown, ) => { diagnostics.forEach((diags, uriString) => { callback.call(thisArg, Uri.parse(uriString), diags, collection) }) }, get: (uri: Uri) => { return diagnostics.get(uri.toString()) }, has: (uri: Uri) => { return diagnostics.has(uri.toString()) }, dispose: () => { diagnostics.clear() }, } return collection }, }, debug: { onDidStartDebugSession: () => ({ dispose: () => {} }), onDidTerminateDebugSession: () => ({ dispose: () => {} }), }, tasks: { onDidStartTask: () => ({ dispose: () => {} }), onDidEndTask: () => ({ dispose: () => {} }), }, extensions: { all: [], getExtension: (extensionId: string) => { // Mock the extension object with extensionUri for theme loading if (extensionId === "kilocode.kilo-code") { return { id: extensionId, extensionUri: context.extensionUri, extensionPath: context.extensionPath, isActive: true, packageJSON: {}, exports: undefined, activate: () => Promise.resolve(), } } return undefined }, onDidChange: () => ({ dispose: () => {} }), }, // Add file system watcher FileSystemWatcher: class { onDidChange = () => ({ dispose: () => {} }) onDidCreate = () => ({ dispose: () => {} }) onDidDelete = () => ({ dispose: () => {} }) dispose = () => {} }, // Add relative pattern RelativePattern: class implements RelativePattern { constructor( public base: string, public pattern: string, ) {} }, // Add progress location ProgressLocation: { SourceControl: 1, Window: 10, Notification: 15, }, // Add URI handler UriHandler: class implements UriHandler { handleUri = (_uri: Uri) => {} }, } }