@@ -22,13 +22,16 @@ import { chatToRestoreEvents } from './chat-restore';
2222import { handleOutbound , type OutboundContext } from './outbound-handler' ;
2323import { SSEClient , type SSEEvent } from './sse' ;
2424import type {
25+ ChatEntry ,
26+ ChatListChangeCallback ,
2527 MCPServerUpdatedParams ,
2628 RemoteChat ,
2729 SessionConfig ,
2830 SessionState ,
2931 SSEChatStatusPayload ,
3032 SSESessionConnectedPayload ,
3133 SSESessionMessagePayload ,
34+ SSETrustUpdatedPayload ,
3235} from './types' ;
3336
3437/** Timeout for the initial SSE handshake (session:connected). */
@@ -43,6 +46,15 @@ export class WebBridge {
4346 private outboundListener : ( ( e : Event ) => void ) | null = null ;
4447 private mcpServers : MCPServerUpdatedParams [ ] = [ ] ;
4548
49+ /**
50+ * Lightweight chat index exposed to the React shell for the sidebar.
51+ * Kept in sync as chat events flow through the bridge.
52+ */
53+ private chatEntries : ChatEntry [ ] = [ ] ;
54+
55+ /** Callback invoked whenever the chat list or selection changes. */
56+ private onChatListChange : ChatListChangeCallback | null = null ;
57+
4658 /**
4759 * True after `disconnect()` has been called. Checked after every async
4860 * boundary in `connect()` so that orphaned bridges (e.g. from React
@@ -101,6 +113,48 @@ export class WebBridge {
101113 return this . connected ;
102114 }
103115
116+ // ---------------------------------------------------------------------------
117+ // Chat list API (for the sidebar)
118+ // ---------------------------------------------------------------------------
119+
120+ /** Register a listener for chat list changes (used by the sidebar). */
121+ onChatListChanged ( cb : ChatListChangeCallback ) : void {
122+ this . onChatListChange = cb ;
123+ // Immediately fire with current state
124+ cb ( [ ...this . chatEntries ] , this . currentChatId ) ;
125+ }
126+
127+ /** Get the current chat entries snapshot. */
128+ getChatEntries ( ) : ChatEntry [ ] {
129+ return [ ...this . chatEntries ] ;
130+ }
131+
132+ /** Get the currently selected chat ID. */
133+ getSelectedChatId ( ) : string | null {
134+ return this . currentChatId ;
135+ }
136+
137+ /** Select a chat by ID — dispatches to the webview and updates tracking. */
138+ selectChat ( chatId : string ) : void {
139+ this . currentChatId = chatId ;
140+ this . dispatch ( 'chat/selectChat' , chatId ) ;
141+ this . notifyChatListChange ( ) ;
142+ }
143+
144+ /** Create a new chat — dispatches to the webview. */
145+ newChat ( ) : void {
146+ this . dispatch ( 'chat/createNewChat' , undefined ) ;
147+ }
148+
149+ /** Delete a chat by ID — calls the REST API. */
150+ async deleteChatFromSidebar ( chatId : string ) : Promise < void > {
151+ try {
152+ await this . api . deleteChat ( chatId ) ;
153+ } catch ( err ) {
154+ console . error ( '[Bridge] Failed to delete chat:' , err ) ;
155+ }
156+ }
157+
104158 // ---------------------------------------------------------------------------
105159 // SSE connection
106160 // ---------------------------------------------------------------------------
@@ -165,6 +219,7 @@ export class WebBridge {
165219 mcpServers : data . mcpServers ,
166220 chats : data . chats ,
167221 config : this . buildSessionConfig ( data ) ,
222+ trust : data . trust ?? false ,
168223 } ;
169224 } catch ( err ) {
170225 console . error ( '[Bridge] Failed to parse session:connected' , err ) ;
@@ -185,6 +240,14 @@ export class WebBridge {
185240 switch ( event . event ) {
186241 case 'chat:content-received' :
187242 this . dispatch ( 'chat/contentReceived' , data ) ;
243+ // Track new chats and title updates for the sidebar
244+ if ( data . chatId ) {
245+ this . upsertChatEntry ( data . chatId , { } ) ;
246+ if ( data . content ?. type === 'metadata' && data . content ?. title ) {
247+ this . upsertChatEntry ( data . chatId , { title : data . content . title } ) ;
248+ }
249+ this . notifyChatListChange ( ) ;
250+ }
188251 break ;
189252
190253 case 'chat:cleared' :
@@ -193,10 +256,14 @@ export class WebBridge {
193256
194257 case 'chat:deleted' :
195258 this . dispatch ( 'chat/deleted' , data . chatId ) ;
259+ this . removeChatEntry ( data . chatId ) ;
260+ this . notifyChatListChange ( ) ;
196261 break ;
197262
198263 case 'chat:status-changed' : {
199264 const status = data as SSEChatStatusPayload ;
265+ this . upsertChatEntry ( status . chatId , { status : status . status } ) ;
266+ this . notifyChatListChange ( ) ;
200267 if ( status . status === 'idle' ) {
201268 this . dispatch ( 'chat/contentReceived' , {
202269 chatId : status . chatId ,
@@ -227,6 +294,12 @@ export class WebBridge {
227294 break ;
228295 }
229296
297+ case 'trust:updated' : {
298+ const trustData = data as SSETrustUpdatedPayload ;
299+ this . dispatch ( 'server/setTrust' , trustData . trust ) ;
300+ break ;
301+ }
302+
230303 case 'session:disconnecting' :
231304 console . warn ( '[Bridge] Server shutting down:' , data . reason ) ;
232305 this . dispatch ( 'server/statusChanged' , 'Stopped' ) ;
@@ -262,6 +335,9 @@ export class WebBridge {
262335 this . dispatch ( 'tool/serversUpdated' , this . mcpServers ) ;
263336 }
264337
338+ // Sync trust mode from server state
339+ this . dispatch ( 'server/setTrust' , this . sessionState . trust ?? false ) ;
340+
265341 await this . restoreChats ( ) ;
266342 } finally {
267343 this . restoring = false ;
@@ -314,11 +390,18 @@ export class WebBridge {
314390 if ( ! chat ?. id ) continue ;
315391 this . currentChatId = chat . id ;
316392
393+ // Track in sidebar
394+ this . upsertChatEntry ( chat . id , {
395+ title : chat . title ?? `Chat` ,
396+ status : chat . status ?? 'idle' ,
397+ } ) ;
398+
317399 const events = chatToRestoreEvents ( chat ) ;
318400 for ( const event of events ) {
319401 this . dispatch ( 'chat/contentReceived' , event ) ;
320402 }
321403 }
404+ this . notifyChatListChange ( ) ;
322405 }
323406
324407 // ---------------------------------------------------------------------------
@@ -392,4 +475,35 @@ export class WebBridge {
392475 private dispatch ( type : string , data : unknown ) : void {
393476 window . postMessage ( { type, data } , '*' ) ;
394477 }
478+
479+ // ---------------------------------------------------------------------------
480+ // Chat entry tracking (for sidebar)
481+ // ---------------------------------------------------------------------------
482+
483+ /** Notify the sidebar callback of chat list changes. */
484+ private notifyChatListChange ( ) : void {
485+ this . onChatListChange ?.( [ ...this . chatEntries ] , this . currentChatId ) ;
486+ }
487+
488+ /** Upsert a chat entry by ID (creates if missing, updates if present). */
489+ private upsertChatEntry ( id : string , partial : Partial < Omit < ChatEntry , 'id' > > ) : void {
490+ const idx = this . chatEntries . findIndex ( ( e ) => e . id === id ) ;
491+ if ( idx >= 0 ) {
492+ this . chatEntries = [
493+ ...this . chatEntries . slice ( 0 , idx ) ,
494+ { ...this . chatEntries [ idx ] , ...partial } ,
495+ ...this . chatEntries . slice ( idx + 1 ) ,
496+ ] ;
497+ } else {
498+ this . chatEntries = [
499+ ...this . chatEntries ,
500+ { id, title : partial . title ?? `Chat` , status : partial . status ?? 'idle' } ,
501+ ] ;
502+ }
503+ }
504+
505+ /** Remove a chat entry by ID. */
506+ private removeChatEntry ( id : string ) : void {
507+ this . chatEntries = this . chatEntries . filter ( ( e ) => e . id !== id ) ;
508+ }
395509}
0 commit comments