11import { contextBridge , ipcRenderer } from 'electron'
22
3+ // ── Single-listener fanout ────────────────────────────────────────────────────
4+ // Instead of registering one ipcRenderer listener per session (causing
5+ // MaxListenersExceededWarning when >10 sessions are open), we keep exactly ONE
6+ // ipcRenderer listener per channel and fan out to all registered callbacks.
7+ function makeFanout < T extends unknown [ ] > ( channel : string ) : ( cb : ( ...args : T ) => void ) => ( ) => void {
8+ const handlers = new Set < ( ...args : T ) => void > ( )
9+ ipcRenderer . on ( channel , ( _ : unknown , ...args : unknown [ ] ) => {
10+ handlers . forEach ( cb => cb ( ...( args as T ) ) )
11+ } )
12+ return ( cb ) => {
13+ handlers . add ( cb )
14+ return ( ) => handlers . delete ( cb )
15+ }
16+ }
17+
18+ const onSshData = makeFanout < [ string , string ] > ( 'ssh:data' )
19+ const onSshClosed = makeFanout < [ string ] > ( 'ssh:closed' )
20+ const onTelnetData = makeFanout < [ string , string ] > ( 'telnet:data' )
21+ const onTelnetClosed = makeFanout < [ string ] > ( 'telnet:closed' )
22+ const onSerialData = makeFanout < [ string , string ] > ( 'serial:data' )
23+ const onSerialClosed = makeFanout < [ string ] > ( 'serial:closed' )
24+ const onSerialError = makeFanout < [ string , string ] > ( 'serial:error' )
25+ const onSftpProgress = makeFanout < [ string , string , number , number ] > ( 'sftp:progress' )
26+ const onSftpClosed = makeFanout < [ string ] > ( 'sftp:closed' )
27+ const onAiChunk = makeFanout < [ string ] > ( 'ai:chunk' )
28+ const onAiDone = makeFanout < [ { inputTokens : number ; outputTokens : number } | undefined ] > ( 'ai:done' )
29+ const onAiToolCall = makeFanout < [ { id : string ; command : string ; reason : string ; targetSession ?: string } ] > ( 'ai:tool-call' )
30+ const onAiError = makeFanout < [ string ] > ( 'ai:error' )
31+ const onAiPlan = makeFanout < [ { objective : string ; steps : string [ ] } ] > ( 'ai:plan' )
32+ const onWindowMaximized = makeFanout < [ boolean ] > ( 'window:maximized-change' )
33+ const onUpdaterAvailable = makeFanout < [ { version : string ; releaseDate : string ; releaseNotes : string | null } ] > ( 'updater:update-available' )
34+ const onUpdaterError = makeFanout < [ string ] > ( 'updater:error' )
35+
336const api = {
437 // Store
538 store : {
@@ -39,16 +72,8 @@ const api = {
3972 forwardStart : ( payload : unknown ) => ipcRenderer . invoke ( 'ssh:forward-start' , payload ) ,
4073 forwardStop : ( forwardId : string ) => ipcRenderer . invoke ( 'ssh:forward-stop' , forwardId ) ,
4174 forwardStopSession : ( sessionId : string ) => ipcRenderer . invoke ( 'ssh:forward-stop-session' , sessionId ) ,
42- onData : ( cb : ( sessionId : string , data : string ) => void ) => {
43- const handler = ( _ : unknown , sessionId : string , data : string ) => cb ( sessionId , data )
44- ipcRenderer . on ( 'ssh:data' , handler )
45- return ( ) => ipcRenderer . removeListener ( 'ssh:data' , handler )
46- } ,
47- onClosed : ( cb : ( sessionId : string ) => void ) => {
48- const handler = ( _ : unknown , sessionId : string ) => cb ( sessionId )
49- ipcRenderer . on ( 'ssh:closed' , handler )
50- return ( ) => ipcRenderer . removeListener ( 'ssh:closed' , handler )
51- }
75+ onData : onSshData ,
76+ onClosed : onSshClosed ,
5277 } ,
5378
5479 // Telnet
@@ -58,16 +83,8 @@ const api = {
5883 resize : ( sessionId : string , cols : number , rows : number ) =>
5984 ipcRenderer . invoke ( 'telnet:resize' , sessionId , cols , rows ) ,
6085 disconnect : ( sessionId : string ) => ipcRenderer . invoke ( 'telnet:disconnect' , sessionId ) ,
61- onData : ( cb : ( sessionId : string , data : string ) => void ) => {
62- const handler = ( _ : unknown , sessionId : string , data : string ) => cb ( sessionId , data )
63- ipcRenderer . on ( 'telnet:data' , handler )
64- return ( ) => ipcRenderer . removeListener ( 'telnet:data' , handler )
65- } ,
66- onClosed : ( cb : ( sessionId : string ) => void ) => {
67- const handler = ( _ : unknown , sessionId : string ) => cb ( sessionId )
68- ipcRenderer . on ( 'telnet:closed' , handler )
69- return ( ) => ipcRenderer . removeListener ( 'telnet:closed' , handler )
70- }
86+ onData : onTelnetData ,
87+ onClosed : onTelnetClosed ,
7188 } ,
7289
7390 // Session Logging
@@ -112,27 +129,15 @@ const api = {
112129 maximize : ( ) => ipcRenderer . invoke ( 'window:maximize' ) ,
113130 close : ( ) => ipcRenderer . invoke ( 'window:close' ) ,
114131 isMaximized : ( ) => ipcRenderer . invoke ( 'window:is-maximized' ) ,
115- onMaximizedChange : ( cb : ( maximized : boolean ) => void ) => {
116- const handler = ( _ : unknown , maximized : boolean ) => cb ( maximized )
117- ipcRenderer . on ( 'window:maximized-change' , handler )
118- return ( ) => ipcRenderer . removeListener ( 'window:maximized-change' , handler )
119- } ,
132+ onMaximizedChange : onWindowMaximized ,
120133 } ,
121134
122135 // Auto-updater (check only — downloads open in browser)
123136 updater : {
124137 check : ( ) => ipcRenderer . invoke ( 'updater:check' ) ,
125138 openRelease : ( url : string ) => ipcRenderer . invoke ( 'updater:open-release' , url ) ,
126- onUpdateAvailable : ( cb : ( info : { version : string ; releaseDate : string ; releaseNotes : string | null } ) => void ) => {
127- const handler = ( _ : unknown , info : { version : string ; releaseDate : string ; releaseNotes : string | null } ) => cb ( info )
128- ipcRenderer . on ( 'updater:update-available' , handler )
129- return ( ) => ipcRenderer . removeListener ( 'updater:update-available' , handler )
130- } ,
131- onError : ( cb : ( message : string ) => void ) => {
132- const handler = ( _ : unknown , message : string ) => cb ( message )
133- ipcRenderer . on ( 'updater:error' , handler )
134- return ( ) => ipcRenderer . removeListener ( 'updater:error' , handler )
135- } ,
139+ onUpdateAvailable : onUpdaterAvailable ,
140+ onError : onUpdaterError ,
136141 } ,
137142
138143 // Serial
@@ -141,21 +146,9 @@ const api = {
141146 connect : ( payload : unknown ) => ipcRenderer . invoke ( 'serial:connect' , payload ) ,
142147 send : ( sessionId : string , data : string ) => ipcRenderer . send ( 'serial:send' , sessionId , data ) ,
143148 disconnect : ( sessionId : string ) => ipcRenderer . invoke ( 'serial:disconnect' , sessionId ) ,
144- onData : ( cb : ( sessionId : string , data : string ) => void ) => {
145- const handler = ( _ : unknown , sessionId : string , data : string ) => cb ( sessionId , data )
146- ipcRenderer . on ( 'serial:data' , handler )
147- return ( ) => ipcRenderer . removeListener ( 'serial:data' , handler )
148- } ,
149- onClosed : ( cb : ( sessionId : string ) => void ) => {
150- const handler = ( _ : unknown , sessionId : string ) => cb ( sessionId )
151- ipcRenderer . on ( 'serial:closed' , handler )
152- return ( ) => ipcRenderer . removeListener ( 'serial:closed' , handler )
153- } ,
154- onError : ( cb : ( sessionId : string , error : string ) => void ) => {
155- const handler = ( _ : unknown , sessionId : string , error : string ) => cb ( sessionId , error )
156- ipcRenderer . on ( 'serial:error' , handler )
157- return ( ) => ipcRenderer . removeListener ( 'serial:error' , handler )
158- }
149+ onData : onSerialData ,
150+ onClosed : onSerialClosed ,
151+ onError : onSerialError ,
159152 } ,
160153
161154 // Auth
@@ -196,17 +189,8 @@ const api = {
196189 rename : ( sessionId : string , oldPath : string , newPath : string ) => ipcRenderer . invoke ( 'sftp:rename' , sessionId , oldPath , newPath ) ,
197190 mkdir : ( sessionId : string , remotePath : string ) => ipcRenderer . invoke ( 'sftp:mkdir' , sessionId , remotePath ) ,
198191 disconnect : ( sessionId : string ) => ipcRenderer . invoke ( 'sftp:disconnect' , sessionId ) ,
199- onProgress : ( cb : ( sessionId : string , filePath : string , transferred : number , total : number ) => void ) => {
200- const handler = ( _ : unknown , sessionId : string , filePath : string , transferred : number , total : number ) =>
201- cb ( sessionId , filePath , transferred , total )
202- ipcRenderer . on ( 'sftp:progress' , handler )
203- return ( ) => ipcRenderer . removeListener ( 'sftp:progress' , handler )
204- } ,
205- onClosed : ( cb : ( sessionId : string ) => void ) => {
206- const handler = ( _ : unknown , sessionId : string ) => cb ( sessionId )
207- ipcRenderer . on ( 'sftp:closed' , handler )
208- return ( ) => ipcRenderer . removeListener ( 'sftp:closed' , handler )
209- } ,
192+ onProgress : onSftpProgress ,
193+ onClosed : onSftpClosed ,
210194 } ,
211195
212196 // AI Copilot
@@ -216,31 +200,11 @@ const api = {
216200 toolResult : ( callId : string , output : string ) => ipcRenderer . invoke ( 'ai:tool-result' , callId , output ) ,
217201 resetBlacklist : ( ) => ipcRenderer . invoke ( 'ai:reset-blacklist' ) ,
218202 exportMarkdown : ( payload : unknown ) => ipcRenderer . invoke ( 'ai:export-markdown' , payload ) ,
219- onChunk : ( cb : ( chunk : string ) => void ) => {
220- const handler = ( _ : unknown , chunk : string ) => cb ( chunk )
221- ipcRenderer . on ( 'ai:chunk' , handler )
222- return ( ) => ipcRenderer . removeListener ( 'ai:chunk' , handler )
223- } ,
224- onDone : ( cb : ( usage ?: { inputTokens : number ; outputTokens : number } ) => void ) => {
225- const handler = ( _ : unknown , usage ?: { inputTokens : number ; outputTokens : number } ) => cb ( usage )
226- ipcRenderer . on ( 'ai:done' , handler )
227- return ( ) => ipcRenderer . removeListener ( 'ai:done' , handler )
228- } ,
229- onToolCall : ( cb : ( call : { id : string ; command : string ; reason : string ; targetSession ?: string } ) => void ) => {
230- const handler = ( _ : unknown , call : { id : string ; command : string ; reason : string ; targetSession ?: string } ) => cb ( call )
231- ipcRenderer . on ( 'ai:tool-call' , handler )
232- return ( ) => ipcRenderer . removeListener ( 'ai:tool-call' , handler )
233- } ,
234- onError : ( cb : ( error : string ) => void ) => {
235- const handler = ( _ : unknown , error : string ) => cb ( error )
236- ipcRenderer . on ( 'ai:error' , handler )
237- return ( ) => ipcRenderer . removeListener ( 'ai:error' , handler )
238- } ,
239- onPlan : ( cb : ( plan : { objective : string ; steps : string [ ] } ) => void ) => {
240- const handler = ( _ : unknown , plan : { objective : string ; steps : string [ ] } ) => cb ( plan )
241- ipcRenderer . on ( 'ai:plan' , handler )
242- return ( ) => ipcRenderer . removeListener ( 'ai:plan' , handler )
243- } ,
203+ onChunk : onAiChunk ,
204+ onDone : onAiDone ,
205+ onToolCall : onAiToolCall ,
206+ onError : onAiError ,
207+ onPlan : onAiPlan ,
244208 } ,
245209
246210 connection : {
0 commit comments