@@ -24,6 +24,15 @@ import {
2424 invalidateConversationsList ,
2525 NEW_CONVERSATION_ID ,
2626} from "./utils" ;
27+ import { EventEmitter } from "@rilldata/web-common/lib/event-emitter.ts" ;
28+
29+ type ConversationEvents = {
30+ "conversation-created" : string ;
31+ "stream-start" : void ;
32+ message : V1Message ;
33+ "stream-complete" : string ;
34+ error : string ;
35+ } ;
2736
2837/**
2938 * Individual conversation state management.
@@ -37,25 +46,23 @@ export class Conversation {
3746 public readonly isStreaming = writable ( false ) ;
3847 public readonly streamError = writable < string | null > ( null ) ;
3948
49+ private readonly events = new EventEmitter < ConversationEvents > ( ) ;
50+ public readonly on = this . events . on . bind (
51+ this . events ,
52+ ) as typeof this . events . on ;
53+ public readonly once = this . events . once . bind (
54+ this . events ,
55+ ) as typeof this . events . once ;
56+
4057 // Private state
4158 private sseClient : SSEFetchClient | null = null ;
4259 private hasReceivedFirstMessage = false ;
4360
4461 constructor (
4562 private readonly instanceId : string ,
4663 public conversationId : string ,
47- private readonly options : {
48- agent ?: string ;
49- onStreamStart ?: ( ) => void ;
50- onConversationCreated ?: ( conversationId : string ) => void ;
51- } = {
52- agent : ToolName . ANALYST_AGENT , // Hardcoded default for now
53- } ,
54- ) {
55- if ( this . options ) {
56- this . options . agent ??= ToolName . ANALYST_AGENT ;
57- }
58- }
64+ private readonly agent : string = ToolName . ANALYST_AGENT , // Hardcoded default for now
65+ ) { }
5966
6067 // ===== PUBLIC API =====
6168
@@ -116,12 +123,7 @@ export class Conversation {
116123 */
117124 public async sendMessage (
118125 context : RuntimeServiceCompleteBody ,
119- options ?: {
120- onStreamStart ?: ( ) => void ;
121- onMessage ?: ( message : V1Message ) => void ;
122- onStreamComplete ?: ( conversationId : string ) => void ;
123- onError ?: ( error : string ) => void ;
124- } ,
126+ options ?: { onStreamStart ?: ( ) => void } ,
125127 ) : Promise < void > {
126128 // Prevent concurrent message sending
127129 if ( get ( this . isStreaming ) ) {
@@ -141,19 +143,16 @@ export class Conversation {
141143 const userMessage = this . addOptimisticUserMessage ( prompt ) ;
142144
143145 try {
144- options ?. onStreamStart ?.( ) ;
146+ options ?. onStreamStart ?.( ) ; // Callback for direct callers
147+ this . events . emit ( "stream-start" ) ; // Event for external listeners
145148 // Start streaming - this establishes the connection
146- const streamPromise = this . startStreaming (
147- prompt ,
148- context ,
149- options ?. onMessage ,
150- ) ;
149+ const streamPromise = this . startStreaming ( prompt , context ) ;
151150
152151 // Wait for streaming to complete
153152 await streamPromise ;
154153
155154 // Stream has completed successfully
156- options ?. onStreamComplete ?. ( this . conversationId ) ;
155+ this . events . emit ( "stream-complete" , this . conversationId ) ;
157156
158157 // Temporary fix to make sure the title of the conversation is updated.
159158 void invalidateConversationsList ( this . instanceId ) ;
@@ -171,7 +170,7 @@ export class Conversation {
171170 userMessage ,
172171 this . hasReceivedFirstMessage ,
173172 ) ;
174- options ?. onError ?. ( this . formatTransportError ( error ) ) ;
173+ this . events . emit ( "error" , this . formatTransportError ( error ) ) ;
175174 } finally {
176175 this . isStreaming . set ( false ) ;
177176 }
@@ -200,6 +199,8 @@ export class Conversation {
200199 this . sseClient . cleanup ( ) ;
201200 this . sseClient = null ;
202201 }
202+
203+ this . events . clearListeners ( ) ;
203204 }
204205
205206 // ===== PRIVATE IMPLEMENTATION =====
@@ -213,7 +214,6 @@ export class Conversation {
213214 private async startStreaming (
214215 prompt : string ,
215216 context : RuntimeServiceCompleteBody | undefined ,
216- onMessage : ( ( message : V1Message ) => void ) | undefined ,
217217 ) : Promise < void > {
218218 // Initialize SSE client if not already done
219219 if ( ! this . sseClient ) {
@@ -238,7 +238,7 @@ export class Conversation {
238238 message . data ,
239239 ) ;
240240 this . processStreamingResponse ( response ) ;
241- if ( response . message ) onMessage ?. ( response . message ) ;
241+ if ( response . message ) this . events . emit ( "message" , response . message ) ;
242242 } catch ( error ) {
243243 console . error ( "Failed to parse streaming response:" , error ) ;
244244 this . streamError . set ( "Failed to process server response" ) ;
@@ -276,12 +276,12 @@ export class Conversation {
276276 ? undefined
277277 : this . conversationId ,
278278 prompt,
279- agent : this . options ?. agent ,
279+ agent : this . agent ,
280280 ...context ,
281281 } ;
282282
283283 // Notify that streaming is about to start (for concurrent stream management)
284- this . options ?. onStreamStart ?. ( ) ;
284+ this . events . emit ( "stream-start" ) ;
285285
286286 // Start streaming - this will establish the connection and then stream until completion
287287 await this . sseClient . start ( baseUrl , {
@@ -360,7 +360,7 @@ export class Conversation {
360360 this . conversationId = realConversationId ;
361361
362362 // Notify that conversation was created
363- this . options ?. onConversationCreated ?. ( realConversationId ) ;
363+ this . events . emit ( "conversation-created" , realConversationId ) ;
364364 }
365365
366366 // ----- Cache Management -----
0 commit comments