11import { ModelDescription , SerializedContinueConfig } from "core" ;
2- // import Mock from "core/llm/llms/Mock .js";
2+ import { IDE } from "core/index .js" ;
33import { FromIdeProtocol , ToIdeProtocol } from "core/protocol/index.js" ;
44import { IMessenger } from "core/protocol/messenger" ;
55import FileSystemIde from "core/util/filesystem" ;
@@ -15,7 +15,115 @@ import {
1515 CoreBinaryTcpMessenger ,
1616} from "../src/IpcMessenger" ;
1717
18- // jest.setTimeout(100_000);
18+ /**
19+ * Handles IDE messages from the binary subprocess, responding with plain data
20+ * matching the Kotlin CoreMessenger format: { messageType, data, messageId }.
21+ *
22+ * This bypasses the JS _handleLine auto-wrapper which would double-wrap
23+ * responses in { done, content, status }.
24+ */
25+ class BinaryIdeHandler {
26+ private ide : IDE ;
27+ private subprocess : ChildProcessWithoutNullStreams ;
28+ private handlers : Record < string , ( data : any ) => Promise < any > | any > = { } ;
29+ private unfinishedLine : string | undefined ;
30+
31+ constructor ( subprocess : ChildProcessWithoutNullStreams , ide : IDE ) {
32+ this . ide = ide ;
33+ this . subprocess = subprocess ;
34+ this . registerHandlers ( ) ;
35+
36+ // Listen on stdout alongside CoreBinaryMessenger (EventEmitter allows multiple listeners)
37+ // Use setEncoding so split multibyte UTF-8 characters are decoded correctly
38+ subprocess . stdout . setEncoding ( "utf8" ) ;
39+ subprocess . stdout . on ( "data" , ( data : string ) => this . handleData ( data ) ) ;
40+ }
41+
42+ private registerHandlers ( ) {
43+ const ide = this . ide ;
44+ const h = this . handlers ;
45+ h [ "getIdeInfo" ] = ( ) => ide . getIdeInfo ( ) ;
46+ h [ "getIdeSettings" ] = ( ) => ide . getIdeSettings ( ) ;
47+ h [ "getControlPlaneSessionInfo" ] = ( ) => undefined ;
48+ h [ "getWorkspaceDirs" ] = ( ) => ide . getWorkspaceDirs ( ) ;
49+ h [ "readFile" ] = ( d ) => ide . readFile ( d . filepath ) ;
50+ h [ "writeFile" ] = ( d ) => ide . writeFile ( d . path , d . contents ) ;
51+ h [ "fileExists" ] = ( d ) => ide . fileExists ( d . filepath ) ;
52+ h [ "showLines" ] = ( d ) => ide . showLines ( d . filepath , d . startLine , d . endLine ) ;
53+ h [ "openFile" ] = ( d ) => ide . openFile ( d . path ) ;
54+ h [ "openUrl" ] = ( d ) => ide . openUrl ( d . url ) ;
55+ h [ "runCommand" ] = ( d ) => ide . runCommand ( d . command ) ;
56+ h [ "saveFile" ] = ( d ) => ide . saveFile ( d . filepath ) ;
57+ h [ "readRangeInFile" ] = ( d ) => ide . readRangeInFile ( d . filepath , d . range ) ;
58+ h [ "getFileStats" ] = ( d ) => ide . getFileStats ( d . files ) ;
59+ h [ "getGitRootPath" ] = ( d ) => ide . getGitRootPath ( d . dir ) ;
60+ h [ "listDir" ] = ( d ) => ide . listDir ( d . dir ) ;
61+ h [ "getRepoName" ] = ( d ) => ide . getRepoName ( d . dir ) ;
62+ h [ "getTags" ] = ( d ) => ide . getTags ( d ) ;
63+ h [ "isTelemetryEnabled" ] = ( ) => ide . isTelemetryEnabled ( ) ;
64+ h [ "isWorkspaceRemote" ] = ( ) => false ;
65+ h [ "getUniqueId" ] = ( ) => ide . getUniqueId ( ) ;
66+ h [ "getDiff" ] = ( d ) => ide . getDiff ( d . includeUnstaged ) ;
67+ h [ "getTerminalContents" ] = ( ) => ide . getTerminalContents ( ) ;
68+ h [ "getOpenFiles" ] = ( ) => ide . getOpenFiles ( ) ;
69+ h [ "getCurrentFile" ] = ( ) => ide . getCurrentFile ( ) ;
70+ h [ "getPinnedFiles" ] = ( ) => ide . getPinnedFiles ( ) ;
71+ h [ "getSearchResults" ] = ( d ) => ide . getSearchResults ( d . query , d . maxResults ) ;
72+ h [ "getFileResults" ] = ( d ) => ide . getFileResults ( d . pattern ) ;
73+ h [ "getProblems" ] = ( d ) => ide . getProblems ( d . filepath ) ;
74+ h [ "getBranch" ] = ( d ) => ide . getBranch ( d . dir ) ;
75+ h [ "subprocess" ] = ( d ) => ide . subprocess ( d . command , d . cwd ) ;
76+ h [ "getDebugLocals" ] = ( d ) => ide . getDebugLocals ( d . threadIndex ) ;
77+ h [ "getAvailableThreads" ] = ( ) => ide . getAvailableThreads ( ) ;
78+ h [ "getTopLevelCallStackSources" ] = ( d ) =>
79+ ide . getTopLevelCallStackSources ( d . threadIndex , d . stackDepth ) ;
80+ h [ "showToast" ] = ( ) => { } ;
81+ h [ "readSecrets" ] = ( d ) => ide . readSecrets ( d . keys ) ;
82+ h [ "writeSecrets" ] = ( d ) => ide . writeSecrets ( d . secrets ) ;
83+ h [ "removeFile" ] = ( d ) => ide . removeFile ( d . path ) ;
84+ }
85+
86+ private handleData ( data : string ) {
87+ const d = data ;
88+ const lines = d . split ( / \r \n / ) . filter ( ( line ) => line . trim ( ) !== "" ) ;
89+ if ( lines . length === 0 ) return ;
90+
91+ if ( this . unfinishedLine ) {
92+ lines [ 0 ] = this . unfinishedLine + lines [ 0 ] ;
93+ this . unfinishedLine = undefined ;
94+ }
95+ if ( ! d . endsWith ( "\r\n" ) ) {
96+ this . unfinishedLine = lines . pop ( ) ;
97+ }
98+ lines . forEach ( ( line ) => this . handleLine ( line ) ) ;
99+ }
100+
101+ private async handleLine ( line : string ) {
102+ let msg : { messageType : string ; messageId : string ; data ?: any } ;
103+ try {
104+ msg = JSON . parse ( line ) ;
105+ } catch {
106+ return ; // not JSON, ignore
107+ }
108+
109+ const handler = this . handlers [ msg . messageType ] ;
110+ if ( ! handler ) return ; // not an IDE message, let CoreBinaryMessenger handle it
111+
112+ try {
113+ const result = await handler ( msg . data ) ;
114+ this . respond ( msg . messageType , result , msg . messageId ) ;
115+ } catch ( e ) {
116+ this . respond ( msg . messageType , undefined , msg . messageId ) ;
117+ }
118+ }
119+
120+ private respond ( messageType : string , data : any , messageId : string ) {
121+ const response = JSON . stringify ( { messageType, data, messageId } ) ;
122+ this . subprocess . stdin . write ( response + "\r\n" ) ;
123+ }
124+ }
125+
126+ jest . setTimeout ( 30_000 ) ;
19127
20128const USE_TCP = false ;
21129
@@ -122,6 +230,11 @@ describe("Test Suite", () => {
122230 console . error ( "Error spawning subprocess:" , error ) ;
123231 throw error ;
124232 }
233+
234+ subprocess . stderr . on ( "data" , ( data : Buffer ) => {
235+ console . error ( `[stderr] ${ data . toString ( ) } ` ) ;
236+ } ) ;
237+
125238 messenger = new CoreBinaryMessenger < ToIdeProtocol , FromIdeProtocol > (
126239 subprocess ,
127240 ) ;
@@ -132,7 +245,9 @@ describe("Test Suite", () => {
132245 fs . mkdirSync ( testDir ) ;
133246 }
134247 const ide = new FileSystemIde ( testDir ) ;
135- // const reverseIde = new ReverseMessageIde(messenger.on.bind(messenger), ide);
248+ if ( ! USE_TCP && subprocess ) {
249+ new BinaryIdeHandler ( subprocess , ide ) ;
250+ }
136251
137252 // Wait for core to set itself up
138253 await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
@@ -151,28 +266,25 @@ describe("Test Suite", () => {
151266 }
152267 } ) ;
153268
269+ // Binary responses are wrapped in { done, content, status } by _handleLine.
270+ // This helper unwraps them, matching how the Kotlin CoreMessenger reads responses.
271+ async function request ( messageType : string , data : any ) : Promise < any > {
272+ const resp = await messenger . request ( messageType as any , data ) ;
273+ return resp ?. content !== undefined ? resp . content : resp ;
274+ }
275+
154276 it ( "should respond to ping with pong" , async ( ) => {
155- const resp = await messenger . request ( "ping" , "ping" ) ;
277+ const resp = await request ( "ping" , "ping" ) ;
156278 expect ( resp ) . toBe ( "pong" ) ;
157279 } ) ;
158280
159281 it ( "should create .continue directory at the specified location with expected files" , async ( ) => {
160282 expect ( fs . existsSync ( CONTINUE_GLOBAL_DIR ) ) . toBe ( true ) ;
161283
162284 // Many of the files are only created when trying to load the config
163- const config = await messenger . request (
164- "config/getSerializedProfileInfo" ,
165- undefined ,
166- ) ;
285+ await request ( "config/getSerializedProfileInfo" , undefined ) ;
167286
168- const expectedFiles = [
169- "config.json" ,
170- "config.ts" ,
171- "package.json" ,
172- "logs/core.log" ,
173- "index/autocompleteCache.sqlite" ,
174- "types/core/index.d.ts" ,
175- ] ;
287+ const expectedFiles = [ "logs/core.log" , "index/autocompleteCache.sqlite" ] ;
176288
177289 const missingFiles = expectedFiles . filter ( ( file ) => {
178290 const filePath = path . join ( CONTINUE_GLOBAL_DIR , file ) ;
@@ -186,38 +298,36 @@ describe("Test Suite", () => {
186298 } ) ;
187299
188300 it ( "should return valid config object" , async ( ) => {
189- const { result } = await messenger . request (
301+ const { result } = await request (
190302 "config/getSerializedProfileInfo" ,
191303 undefined ,
192304 ) ;
193305 const { config } = result ;
194- expect ( config ) . toHaveProperty ( "models" ) ;
195- expect ( config ) . toHaveProperty ( "embeddingsProvider" ) ;
306+ expect ( config ) . toHaveProperty ( "modelsByRole" ) ;
196307 expect ( config ) . toHaveProperty ( "contextProviders" ) ;
197308 expect ( config ) . toHaveProperty ( "slashCommands" ) ;
198309 } ) ;
199310
200311 it ( "should properly handle history requests" , async ( ) => {
201312 const sessionId = "test-session-id" ;
202- await messenger . request ( "history/save" , {
313+ await request ( "history/save" , {
203314 history : [ ] ,
204315 sessionId,
205316 title : "test-title" ,
206-
207317 workspaceDirectory : "test-workspace-directory" ,
208318 } ) ;
209- const sessions = await messenger . request ( "history/list" , { } ) ;
319+ const sessions = await request ( "history/list" , { } ) ;
210320 expect ( sessions . length ) . toBeGreaterThan ( 0 ) ;
211321
212- const session = await messenger . request ( "history/load" , {
322+ const session = await request ( "history/load" , {
213323 id : sessionId ,
214324 } ) ;
215325 expect ( session ) . toHaveProperty ( "history" ) ;
216326
217- await messenger . request ( "history/delete" , {
327+ await request ( "history/delete" , {
218328 id : sessionId ,
219329 } ) ;
220- const sessionsAfterDelete = await messenger . request ( "history/list" , { } ) ;
330+ const sessionsAfterDelete = await request ( "history/list" , { } ) ;
221331 expect ( sessionsAfterDelete . length ) . toBe ( sessions . length - 1 ) ;
222332 } ) ;
223333
@@ -228,23 +338,21 @@ describe("Test Suite", () => {
228338 model : "gpt-3.5-turbo" ,
229339 underlyingProviderName : "openai" ,
230340 } ;
231- await messenger . request ( "config/addModel" , {
232- model,
233- } ) ;
341+ await request ( "config/addModel" , { model } ) ;
234342 const {
235343 result : { config } ,
236- } = await messenger . request ( "config/getSerializedProfileInfo" , undefined ) ;
344+ } = await request ( "config/getSerializedProfileInfo" , undefined ) ;
237345
238346 expect (
239347 config ! . modelsByRole . chat . some (
240348 ( m : ModelDescription ) => m . title === model . title ,
241349 ) ,
242350 ) . toBe ( true ) ;
243351
244- await messenger . request ( "config/deleteModel" , { title : model . title } ) ;
352+ await request ( "config/deleteModel" , { title : model . title } ) ;
245353 const {
246354 result : { config : configAfterDelete } ,
247- } = await messenger . request ( "config/getSerializedProfileInfo" , undefined ) ;
355+ } = await request ( "config/getSerializedProfileInfo" , undefined ) ;
248356 expect (
249357 configAfterDelete ! . modelsByRole . chat . some (
250358 ( m : ModelDescription ) => m . title === model . title ,
@@ -259,11 +367,9 @@ describe("Test Suite", () => {
259367 model : "gpt-3.5-turbo" ,
260368 underlyingProviderName : "mock" ,
261369 } ;
262- await messenger . request ( "config/addModel" , {
263- model,
264- } ) ;
370+ await request ( "config/addModel" , { model } ) ;
265371
266- const resp = await messenger . request ( "llm/complete" , {
372+ const resp = await request ( "llm/complete" , {
267373 prompt : "Say 'Hello' and nothing else" ,
268374 completionOptions : { } ,
269375 title : "Test Model" ,
0 commit comments