-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathextension-client.ts
More file actions
569 lines (506 loc) · 14.9 KB
/
Copy pathextension-client.ts
File metadata and controls
569 lines (506 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
/**
* Roo Code Client
*
* This is the main entry point for the client library. It provides a high-level
* API for:
* - Processing messages from the extension host
* - Querying the current agent state
* - Subscribing to state change events
* - Sending responses back to the extension
*
* The client is designed to be transport-agnostic. You provide a way to send
* messages to the extension, and you feed incoming messages to the client.
*
* Architecture:
* ```
* ┌───────────────────────────────────────────────┐
* │ ExtensionClient │
* │ │
* Extension ──────▶ │ MessageProcessor ──▶ StateStore │
* Messages │ │ │ │
* │ ▼ ▼ │
* │ TypedEventEmitter ◀── State/Events │
* │ │ │
* │ ▼ │
* │ Your Event Handlers │
* └───────────────────────────────────────────────┘
* ```
*/
import type { ExtensionMessage, WebviewMessage, ClineAskResponse, ClineMessage, ClineAsk } from "@roo-code/types"
import { StateStore } from "./state-store.js"
import { MessageProcessor, parseExtensionMessage } from "./message-processor.js"
import {
TypedEventEmitter,
type ClientEventMap,
type AgentStateChangeEvent,
type WaitingForInputEvent,
type ModeChangedEvent,
} from "./events.js"
import { AgentLoopState, type AgentStateInfo } from "./agent-state.js"
// =============================================================================
// Extension Client Configuration
// =============================================================================
/**
* Configuration options for the ExtensionClient.
*/
export interface ExtensionClientConfig {
/**
* Function to send messages to the extension host.
* This is how the client communicates back to the extension.
*
* Example implementations:
* - VSCode webview: (msg) => vscode.postMessage(msg)
* - WebSocket: (msg) => socket.send(JSON.stringify(msg))
* - IPC: (msg) => process.send(msg)
*/
sendMessage: (message: WebviewMessage) => void
/**
* Whether to emit events for all state changes or only significant ones.
* Default: true
*/
emitAllStateChanges?: boolean
/**
* Enable debug logging.
* Default: false
*/
debug?: boolean
/**
* Maximum state history size (for debugging).
* Set to 0 to disable history tracking.
* Default: 0
*/
maxHistorySize?: number
}
// =============================================================================
// Main Client Class
// =============================================================================
/**
* ExtensionClient is the main interface for interacting with the Roo Code extension.
*
* Basic usage:
* ```typescript
* // Create client with message sender
* const client = new ExtensionClient({
* sendMessage: (msg) => vscode.postMessage(msg)
* })
*
* // Subscribe to state changes
* client.on('stateChange', (event) => {
* console.log('State:', event.currentState.state)
* })
*
* // Subscribe to specific events
* client.on('waitingForInput', (event) => {
* console.log('Waiting for:', event.ask)
* })
*
* // Feed messages from extension
* window.addEventListener('message', (e) => {
* client.handleMessage(e.data)
* })
*
* // Query state at any time
* const state = client.getAgentState()
* if (state.isWaitingForInput) {
* // Show approval UI
* }
*
* // Send responses
* client.approve() // or client.reject() or client.respond('answer')
* ```
*/
export class ExtensionClient {
private store: StateStore
private processor: MessageProcessor
private emitter: TypedEventEmitter
private sendMessage: (message: WebviewMessage) => void
private debug: boolean
constructor(config: ExtensionClientConfig) {
this.sendMessage = config.sendMessage
this.debug = config.debug ?? false
this.store = new StateStore({ maxHistorySize: config.maxHistorySize ?? 0 })
this.emitter = new TypedEventEmitter()
this.processor = new MessageProcessor(this.store, this.emitter, {
emitAllStateChanges: config.emitAllStateChanges ?? true,
debug: config.debug ?? false,
})
}
// ===========================================================================
// Message Handling
// ===========================================================================
/**
* Handle an incoming message from the extension host.
*
* Call this method whenever you receive a message from the extension.
* The client will parse, validate, and process the message, updating
* internal state and emitting appropriate events.
*
* @param message - The raw message (can be ExtensionMessage or JSON string)
*/
handleMessage(message: ExtensionMessage | string): void {
let parsed: ExtensionMessage | undefined
if (typeof message === "string") {
parsed = parseExtensionMessage(message)
if (!parsed) {
if (this.debug) {
console.log("[ExtensionClient] Failed to parse message:", message)
}
return
}
} else {
parsed = message
}
this.processor.processMessage(parsed)
}
/**
* Handle multiple messages at once.
*/
handleMessages(messages: (ExtensionMessage | string)[]): void {
for (const message of messages) {
this.handleMessage(message)
}
}
// ===========================================================================
// State Queries - Always know the current state
// ===========================================================================
/**
* Get the complete agent state information.
*
* This returns everything you need to know about the current state:
* - The high-level state (running, streaming, waiting, idle, etc.)
* - Whether input is needed
* - The specific ask type if waiting
* - What action is required
* - Human-readable description
*/
getAgentState(): AgentStateInfo {
return this.store.getAgentState()
}
/**
* Get just the current state enum value.
*/
getCurrentState(): AgentLoopState {
return this.store.getCurrentState()
}
/**
* Check if the agent is waiting for user input.
*/
isWaitingForInput(): boolean {
return this.store.isWaitingForInput()
}
/**
* Check if the agent is actively running.
*/
isRunning(): boolean {
return this.store.isRunning()
}
/**
* Check if content is currently streaming.
*/
isStreaming(): boolean {
return this.store.isStreaming()
}
/**
* Check if there is an active task.
*/
hasActiveTask(): boolean {
return this.store.getCurrentState() !== AgentLoopState.NO_TASK
}
/**
* Get all messages in the current task.
*/
getMessages(): ClineMessage[] {
return this.store.getMessages()
}
/**
* Get the last message.
*/
getLastMessage(): ClineMessage | undefined {
return this.store.getLastMessage()
}
/**
* Get the current ask type if the agent is waiting for input.
*/
getCurrentAsk(): ClineAsk | undefined {
return this.store.getAgentState().currentAsk
}
/**
* Check if the client has received any state from the extension.
*/
isInitialized(): boolean {
return this.store.isInitialized()
}
/**
* Get the current mode (e.g., "code", "architect", "ask").
* Returns undefined if no mode has been received yet.
*/
getCurrentMode(): string | undefined {
return this.store.getCurrentMode()
}
// ===========================================================================
// Event Subscriptions - Realtime notifications
// ===========================================================================
/**
* Subscribe to an event.
*
* Returns an unsubscribe function for easy cleanup.
*
* @param event - The event to subscribe to
* @param listener - The callback function
* @returns Unsubscribe function
*
* @example
* ```typescript
* const unsubscribe = client.on('stateChange', (event) => {
* console.log(event.currentState)
* })
*
* // Later, to unsubscribe:
* unsubscribe()
* ```
*/
on<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): () => void {
return this.emitter.on(event, listener)
}
/**
* Subscribe to an event, triggered only once.
*/
once<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): void {
this.emitter.once(event, listener)
}
/**
* Unsubscribe from an event.
*/
off<K extends keyof ClientEventMap>(event: K, listener: (payload: ClientEventMap[K]) => void): void {
this.emitter.off(event, listener)
}
/**
* Remove all listeners for an event, or all events.
*/
removeAllListeners<K extends keyof ClientEventMap>(event?: K): void {
this.emitter.removeAllListeners(event)
}
/**
* Convenience method: Subscribe only to state changes.
*/
onStateChange(listener: (event: AgentStateChangeEvent) => void): () => void {
return this.on("stateChange", listener)
}
/**
* Convenience method: Subscribe only to waiting events.
*/
onWaitingForInput(listener: (event: WaitingForInputEvent) => void): () => void {
return this.on("waitingForInput", listener)
}
/**
* Convenience method: Subscribe only to mode changes.
*/
onModeChanged(listener: (event: ModeChangedEvent) => void): () => void {
return this.on("modeChanged", listener)
}
// ===========================================================================
// Response Methods - Send actions to the extension
// ===========================================================================
/**
* Approve the current action (tool, command, browser, MCP).
*
* Use when the agent is waiting for approval (interactive asks).
*/
approve(): void {
this.sendResponse("yesButtonClicked")
}
/**
* Reject the current action.
*
* Use when you want to deny a tool, command, or other action.
*/
reject(): void {
this.sendResponse("noButtonClicked")
}
/**
* Send a text response.
*
* Use for:
* - Answering follow-up questions
* - Providing additional context
* - Giving feedback on completion
*
* @param text - The response text
* @param images - Optional base64-encoded images
*/
respond(text: string, images?: string[]): void {
this.sendResponse("messageResponse", text, images)
}
/**
* Generic method to send any ask response.
*
* @param response - The response type
* @param text - Optional text content
* @param images - Optional images
*/
sendResponse(response: ClineAskResponse, text?: string, images?: string[]): void {
const message: WebviewMessage = {
type: "askResponse",
askResponse: response,
text,
images,
}
this.sendMessage(message)
}
// ===========================================================================
// Task Control Methods
// ===========================================================================
/**
* Start a new task with the given prompt.
*
* @param text - The task description/prompt
* @param images - Optional base64-encoded images
*/
newTask(text: string, images?: string[]): void {
const message: WebviewMessage = {
type: "newTask",
text,
images,
}
this.sendMessage(message)
}
/**
* Clear the current task.
*
* This ends the current task and resets to a fresh state.
*/
clearTask(): void {
const message: WebviewMessage = {
type: "clearTask",
}
this.sendMessage(message)
this.processor.notifyTaskCleared()
}
/**
* Cancel a running task.
*
* Use this to interrupt a task that is currently processing.
*/
cancelTask(): void {
const message: WebviewMessage = {
type: "cancelTask",
}
this.sendMessage(message)
}
/**
* Resume a paused task.
*
* Use when the agent state is RESUMABLE (resume_task ask).
*/
resumeTask(): void {
this.approve() // Resume uses the same response as approve
}
/**
* Retry a failed API request.
*
* Use when the agent state shows api_req_failed.
*/
retryApiRequest(): void {
this.approve() // Retry uses the same response as approve
}
// ===========================================================================
// Terminal Operation Methods
// ===========================================================================
/**
* Continue terminal output (don't wait for more output).
*
* Use when the agent is showing command_output and you want to proceed.
*/
continueTerminal(): void {
const message: WebviewMessage = {
type: "terminalOperation",
terminalOperation: "continue",
}
this.sendMessage(message)
}
/**
* Abort terminal command.
*
* Use when you want to kill a running terminal command.
*/
abortTerminal(): void {
const message: WebviewMessage = {
type: "terminalOperation",
terminalOperation: "abort",
}
this.sendMessage(message)
}
// ===========================================================================
// Utility Methods
// ===========================================================================
/**
* Reset the client state.
*
* This clears all internal state and history.
* Useful when disconnecting or starting fresh.
*/
reset(): void {
this.store.reset()
this.emitter.removeAllListeners()
}
/**
* Get the state history (if history tracking is enabled).
*/
getStateHistory() {
return this.store.getHistory()
}
/**
* Enable or disable debug mode.
*/
setDebug(enabled: boolean): void {
this.debug = enabled
this.processor.setDebug(enabled)
}
// ===========================================================================
// Advanced: Direct Store Access
// ===========================================================================
/**
* Get direct access to the state store.
*
* This is for advanced use cases where you need more control.
* Most users should use the methods above instead.
*/
getStore(): StateStore {
return this.store
}
/**
* Get direct access to the event emitter.
*/
getEmitter(): TypedEventEmitter {
return this.emitter
}
}
// =============================================================================
// Factory Functions
// =============================================================================
/**
* Create a mock client for testing.
*
* @internal
* The mock client captures all sent messages for verification.
*
* @returns An object with the client and captured messages
*/
export function createMockClient(): {
client: ExtensionClient
sentMessages: WebviewMessage[]
clearMessages: () => void
} {
const sentMessages: WebviewMessage[] = []
const client = new ExtensionClient({
sendMessage: (message) => sentMessages.push(message),
debug: false,
})
return {
client,
sentMessages,
clearMessages: () => {
sentMessages.length = 0
},
}
}