extension.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. /**
  2. * Extension Service for CLI
  3. *
  4. * This module provides CLI-typed wrappers around @kilocode/agent-runtime components.
  5. * Uses composition to bridge runtime's generic types to CLI's specific types.
  6. *
  7. * Type handling: agent-runtime uses generic types (index signatures) while CLI
  8. * uses specific types. We use composition with type assertions at the boundary.
  9. */
  10. import {
  11. ExtensionService as RuntimeExtensionService,
  12. type ExtensionServiceOptions as RuntimeExtensionServiceOptions,
  13. setLogger,
  14. ExtensionHost as RuntimeExtensionHost,
  15. type ExtensionHostOptions as RuntimeExtensionHostOptions,
  16. MessageBridge as RuntimeMessageBridge,
  17. IPCChannel as RuntimeIPCChannel,
  18. createMessageBridge as runtimeCreateMessageBridge,
  19. createVSCodeAPIMock as runtimeCreateVSCodeAPIMock,
  20. type WebviewMessage as RuntimeWebviewMessage,
  21. type ExtensionMessage as RuntimeExtensionMessage,
  22. type ExtensionState as RuntimeExtensionState,
  23. } from "@kilocode/agent-runtime"
  24. import { logs } from "./logs.js"
  25. import { TelemetryService } from "./telemetry/TelemetryService.js"
  26. import type { ExtensionMessage, WebviewMessage, ExtensionState, ModeConfig } from "../types/messages.js"
  27. import type { IdentityInfo } from "@kilocode/agent-runtime"
  28. import { EventEmitter } from "events"
  29. // Configure agent-runtime to use CLI's logger
  30. setLogger({
  31. debug: (message: string, context?: string, meta?: Record<string, unknown>) => {
  32. logs.debug(message, context, meta)
  33. },
  34. info: (message: string, context?: string, meta?: Record<string, unknown>) => {
  35. logs.info(message, context, meta)
  36. },
  37. warn: (message: string, context?: string, meta?: Record<string, unknown>) => {
  38. logs.warn(message, context, meta)
  39. },
  40. error: (message: string, context?: string, meta?: Record<string, unknown>) => {
  41. logs.error(message, context, meta)
  42. },
  43. })
  44. /**
  45. * Configuration options for ExtensionService
  46. */
  47. export interface ExtensionServiceOptions {
  48. workspace?: string
  49. mode?: string
  50. customModes?: ModeConfig[]
  51. extensionBundlePath?: string
  52. extensionRootPath?: string
  53. identity?: IdentityInfo
  54. appendSystemPrompt?: string
  55. }
  56. /**
  57. * Events emitted by ExtensionService
  58. */
  59. export interface ExtensionServiceEvents {
  60. ready: (api: ExtensionAPI) => void
  61. stateChange: (state: ExtensionState) => void
  62. message: (message: ExtensionMessage) => void
  63. error: (error: Error) => void
  64. warning: (warning: { context: string; error: Error }) => void
  65. disposed: () => void
  66. }
  67. /**
  68. * Extension API interface
  69. */
  70. export interface ExtensionAPI {
  71. getState(): ExtensionState | null
  72. sendWebviewMessage(message: WebviewMessage): Promise<void>
  73. injectConfiguration(config: Partial<ExtensionState>): Promise<void>
  74. }
  75. /**
  76. * Extension Host options
  77. */
  78. export interface ExtensionHostOptions {
  79. workspacePath: string
  80. extensionBundlePath: string
  81. extensionRootPath: string
  82. identity?: IdentityInfo
  83. customModes?: ModeConfig[]
  84. appendSystemPrompt?: string
  85. }
  86. /**
  87. * CLI-typed ExtensionService using composition pattern.
  88. * Wraps RuntimeExtensionService and provides CLI-specific types.
  89. */
  90. export class ExtensionService extends EventEmitter {
  91. private _runtime: RuntimeExtensionService
  92. constructor(options: ExtensionServiceOptions = {}) {
  93. super()
  94. this._runtime = new RuntimeExtensionService(options as RuntimeExtensionServiceOptions)
  95. // Forward events from runtime with type casting
  96. this._runtime.on("ready", (api) => this.emit("ready", this.wrapExtensionAPI(api)))
  97. this._runtime.on("stateChange", (state) => this.emit("stateChange", state as ExtensionState))
  98. this._runtime.on("message", (message) => {
  99. const extMessage = message as ExtensionMessage
  100. TelemetryService.getInstance().trackExtensionMessageReceived(extMessage.type)
  101. this.emit("message", extMessage)
  102. })
  103. this._runtime.on("error", (error) => this.emit("error", error))
  104. this._runtime.on("warning", (warning) => this.emit("warning", warning))
  105. this._runtime.on("disposed", () => this.emit("disposed"))
  106. }
  107. private wrapExtensionAPI(api: unknown): ExtensionAPI {
  108. const runtimeApi = api as {
  109. getState: () => unknown
  110. sendMessage: (message: unknown) => void
  111. updateState: (updates: unknown) => void
  112. }
  113. return {
  114. getState: () => runtimeApi.getState() as ExtensionState | null,
  115. sendWebviewMessage: async (msg: WebviewMessage) => {
  116. // Use the service's sendWebviewMessage for proper routing
  117. await this.sendWebviewMessage(msg)
  118. },
  119. injectConfiguration: async (config: Partial<ExtensionState>) => {
  120. const host = this._runtime.getExtensionHost()
  121. await host.injectConfiguration(config as unknown as Partial<RuntimeExtensionState>)
  122. },
  123. }
  124. }
  125. async initialize(): Promise<void> {
  126. return this._runtime.initialize()
  127. }
  128. async dispose(): Promise<void> {
  129. return this._runtime.dispose()
  130. }
  131. isReady(): boolean {
  132. return this._runtime.isReady()
  133. }
  134. getState(): ExtensionState | null {
  135. return this._runtime.getState() as ExtensionState | null
  136. }
  137. async sendWebviewMessage(message: WebviewMessage): Promise<void> {
  138. TelemetryService.getInstance().trackExtensionMessageSent(message.type)
  139. return this._runtime.sendWebviewMessage(message as unknown as RuntimeWebviewMessage)
  140. }
  141. async requestSingleCompletion(prompt: string, timeoutMs?: number): Promise<string> {
  142. return this._runtime.requestSingleCompletion(prompt, timeoutMs)
  143. }
  144. getExtensionAPI(): ExtensionAPI | null {
  145. const api = this._runtime.getExtensionAPI()
  146. if (!api) return null
  147. return this.wrapExtensionAPI(api)
  148. }
  149. getExtensionHost(): ExtensionHost {
  150. const host = this._runtime.getExtensionHost()
  151. return new ExtensionHost(host)
  152. }
  153. getMessageBridge(): MessageBridge {
  154. const bridge = this._runtime.getMessageBridge()
  155. return new MessageBridge(bridge)
  156. }
  157. // Type-safe event methods
  158. override on<K extends keyof ExtensionServiceEvents>(event: K, listener: ExtensionServiceEvents[K]): this {
  159. return super.on(event as string, listener as (...args: unknown[]) => void)
  160. }
  161. override once<K extends keyof ExtensionServiceEvents>(event: K, listener: ExtensionServiceEvents[K]): this {
  162. return super.once(event as string, listener as (...args: unknown[]) => void)
  163. }
  164. override emit<K extends keyof ExtensionServiceEvents>(
  165. event: K,
  166. ...args: Parameters<ExtensionServiceEvents[K]>
  167. ): boolean {
  168. return super.emit(event as string, ...args)
  169. }
  170. override off<K extends keyof ExtensionServiceEvents>(event: K, listener: ExtensionServiceEvents[K]): this {
  171. return super.off(event as string, listener as (...args: unknown[]) => void)
  172. }
  173. }
  174. /**
  175. * CLI-typed ExtensionHost using composition pattern.
  176. */
  177. export class ExtensionHost {
  178. private _runtime: RuntimeExtensionHost
  179. constructor(runtimeOrOptions: RuntimeExtensionHost | ExtensionHostOptions) {
  180. if (runtimeOrOptions instanceof RuntimeExtensionHost) {
  181. this._runtime = runtimeOrOptions
  182. } else {
  183. this._runtime = new RuntimeExtensionHost(runtimeOrOptions as RuntimeExtensionHostOptions)
  184. }
  185. }
  186. async activate(): Promise<ExtensionAPI> {
  187. const api = await this._runtime.activate()
  188. return this.wrapExtensionAPI(api)
  189. }
  190. async deactivate(): Promise<void> {
  191. return this._runtime.deactivate()
  192. }
  193. private wrapExtensionAPI(api: unknown): ExtensionAPI {
  194. const runtimeApi = api as {
  195. getState: () => unknown
  196. sendMessage: (message: unknown) => void
  197. updateState: (updates: unknown) => void
  198. }
  199. return {
  200. getState: () => runtimeApi.getState() as ExtensionState | null,
  201. sendWebviewMessage: async (msg: WebviewMessage) => {
  202. await this.sendWebviewMessage(msg)
  203. },
  204. injectConfiguration: async (config: Partial<ExtensionState>) => {
  205. await this.injectConfiguration(config)
  206. },
  207. }
  208. }
  209. getAPI(): ExtensionAPI {
  210. const api = this._runtime.getAPI()
  211. return this.wrapExtensionAPI(api)
  212. }
  213. async sendWebviewMessage(message: WebviewMessage): Promise<void> {
  214. return this._runtime.sendWebviewMessage(message as unknown as RuntimeWebviewMessage)
  215. }
  216. async injectConfiguration(config: Partial<ExtensionState>): Promise<void> {
  217. return this._runtime.injectConfiguration(config as unknown as Partial<RuntimeExtensionState>)
  218. }
  219. async syncConfigurationMessages(configState: Partial<ExtensionState>): Promise<void> {
  220. return this._runtime.syncConfigurationMessages(configState as unknown as Partial<RuntimeExtensionState>)
  221. }
  222. markWebviewReady(): void {
  223. this._runtime.markWebviewReady()
  224. }
  225. isWebviewReady(): boolean {
  226. return this._runtime.isWebviewReady()
  227. }
  228. isInInitialSetup(): boolean {
  229. return this._runtime.isInInitialSetup()
  230. }
  231. // Forward EventEmitter methods
  232. on(event: string, listener: (...args: unknown[]) => void): this {
  233. this._runtime.on(event, listener)
  234. return this
  235. }
  236. off(event: string, listener: (...args: unknown[]) => void): this {
  237. this._runtime.off(event, listener)
  238. return this
  239. }
  240. emit(event: string, ...args: unknown[]): boolean {
  241. return this._runtime.emit(event, ...args)
  242. }
  243. }
  244. /**
  245. * CLI-typed MessageBridge using composition pattern.
  246. */
  247. export class MessageBridge {
  248. private _runtime: RuntimeMessageBridge
  249. constructor(runtime: RuntimeMessageBridge) {
  250. this._runtime = runtime
  251. }
  252. async sendWebviewMessage(message: WebviewMessage): Promise<unknown> {
  253. return this._runtime.sendWebviewMessage(message as unknown as RuntimeWebviewMessage)
  254. }
  255. async sendExtensionMessage(message: ExtensionMessage): Promise<void> {
  256. return this._runtime.sendExtensionMessage(message as unknown as RuntimeExtensionMessage)
  257. }
  258. getTUIChannel(): RuntimeIPCChannel {
  259. return this._runtime.getTUIChannel()
  260. }
  261. getExtensionChannel(): RuntimeIPCChannel {
  262. return this._runtime.getExtensionChannel()
  263. }
  264. dispose(): void {
  265. this._runtime.dispose()
  266. }
  267. }
  268. // Re-export IPCChannel as-is
  269. export { RuntimeIPCChannel as IPCChannel }
  270. export type { IPCMessage, IPCOptions } from "@kilocode/agent-runtime"
  271. // Factory functions with CLI types
  272. export function createExtensionService(options: ExtensionServiceOptions = {}): ExtensionService {
  273. return new ExtensionService(options)
  274. }
  275. export function createExtensionHost(options: ExtensionHostOptions): ExtensionHost {
  276. return new ExtensionHost(options)
  277. }
  278. export function createMessageBridge(options?: { enableLogging?: boolean; timeout?: number }): MessageBridge {
  279. const bridge = runtimeCreateMessageBridge(options)
  280. return new MessageBridge(bridge)
  281. }
  282. export function createVSCodeAPIMock(extensionRootPath: string, workspacePath: string, identity?: IdentityInfo) {
  283. return runtimeCreateVSCodeAPIMock(extensionRootPath, workspacePath, identity)
  284. }
  285. // Re-export types
  286. export type { IdentityInfo, ExtensionContext } from "@kilocode/agent-runtime"