@@ -31,6 +31,7 @@ interface PersistedSession {
3131
3232let mainWindow : BrowserWindow ;
3333const activePtyProcesses = new Map < string , pty . IPty > ( ) ;
34+ const mcpPollerPtyProcesses = new Map < string , pty . IPty > ( ) ;
3435const store = new Store ( ) ;
3536
3637// Helper functions for session management
@@ -48,6 +49,108 @@ function getNextSessionNumber(): number {
4849 return Math . max ( ...sessions . map ( s => s . number ) ) + 1 ;
4950}
5051
52+ // Spawn headless PTY for MCP polling
53+ function spawnMcpPoller ( sessionId : string , worktreePath : string ) {
54+ const shell = os . platform ( ) === "darwin" ? "zsh" : "bash" ;
55+ const ptyProcess = pty . spawn ( shell , [ "-l" ] , {
56+ name : "xterm-color" ,
57+ cols : 80 ,
58+ rows : 30 ,
59+ cwd : worktreePath ,
60+ env : process . env ,
61+ } ) ;
62+
63+ mcpPollerPtyProcesses . set ( sessionId , ptyProcess ) ;
64+
65+ let outputBuffer = "" ;
66+ const serverMap = new Map < string , any > ( ) ;
67+
68+ ptyProcess . onData ( ( data ) => {
69+ // Accumulate output without displaying it
70+ outputBuffer += data ;
71+
72+ // Parse output whenever we have MCP server entries
73+ // Match lines like: "servername: url (type) - ✓ Connected" or "servername: command (stdio) - ✓ Connected"
74+ // Pattern handles both SSE (with URLs) and stdio (with commands/paths)
75+ const mcpServerLineRegex = / ^ [ \w - ] + : .+ \( (?: S S E | s t d i o ) \) \s + - \s + [ ✓ ⚠ ] / m;
76+
77+ if ( mcpServerLineRegex . test ( data ) || data . includes ( "No MCP servers configured" ) ) {
78+ try {
79+ const servers = parseMcpOutput ( outputBuffer ) ;
80+
81+ // Merge servers into the map (upsert by name)
82+ servers . forEach ( server => {
83+ serverMap . set ( server . name , server ) ;
84+ } ) ;
85+
86+ const allServers = Array . from ( serverMap . values ( ) ) ;
87+
88+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
89+ mainWindow . webContents . send ( "mcp-servers-updated" , sessionId , allServers ) ;
90+ }
91+ } catch ( error ) {
92+ console . error ( "Error parsing MCP output:" , error ) ;
93+ }
94+ }
95+
96+ // Clear buffer when we see the shell prompt (command finished)
97+ if ( ( data . includes ( "% " ) || data . includes ( "$ " ) || data . includes ( "➜ " ) ) &&
98+ outputBuffer . includes ( "claude mcp list" ) ) {
99+ outputBuffer = "" ;
100+ }
101+ } ) ;
102+
103+ // Wait for shell to be ready, then start polling loop
104+ setTimeout ( ( ) => {
105+ // Run `claude mcp list` every 60 seconds
106+ const pollMcp = ( ) => {
107+ if ( mcpPollerPtyProcesses . has ( sessionId ) ) {
108+ ptyProcess . write ( "claude mcp list\r" ) ;
109+ setTimeout ( pollMcp , 60000 ) ;
110+ }
111+ } ;
112+ pollMcp ( ) ;
113+ } , 2000 ) ; // Wait 2s for shell to initialize
114+ }
115+
116+ // Parse MCP server list output
117+ function parseMcpOutput ( output : string ) : any [ ] {
118+ const servers = [ ] ;
119+
120+ if ( output . includes ( "No MCP servers configured" ) ) {
121+ return [ ] ;
122+ }
123+
124+ const lines = output . trim ( ) . split ( "\n" ) . filter ( line => line . trim ( ) ) ;
125+
126+ for ( const line of lines ) {
127+ // Skip header lines, empty lines, and status messages
128+ if ( line . includes ( "MCP servers" ) ||
129+ line . includes ( "---" ) ||
130+ line . includes ( "Checking" ) ||
131+ line . includes ( "health" ) ||
132+ line . includes ( "claude mcp list" ) ||
133+ ! line . trim ( ) ) {
134+ continue ;
135+ }
136+
137+ // Only parse lines that match the MCP server format
138+ // Must have: "name: something (SSE|stdio) - status"
139+ const serverMatch = line . match ( / ^ ( [ \w - ] + ) : .+ \( (?: S S E | s t d i o ) \) \s + - \s + [ ✓ ⚠ ] / ) ;
140+ if ( serverMatch ) {
141+ const serverName = serverMatch [ 1 ] ;
142+ const isConnected = line . includes ( "✓" ) || line . includes ( "Connected" ) ;
143+
144+ servers . push ( {
145+ name : serverName ,
146+ connected : isConnected
147+ } ) ;
148+ }
149+ }
150+
151+ return servers ;
152+ }
153+
51154// Helper function to spawn PTY and setup coding agent
52155function spawnSessionPty (
53156 sessionId : string ,
@@ -68,6 +171,7 @@ function spawnSessionPty(
68171 activePtyProcesses . set ( sessionId , ptyProcess ) ;
69172
70173 let terminalReady = false ;
174+ let claudeReady = false ;
71175 let dataBuffer = "" ;
72176
73177 ptyProcess . onData ( ( data ) => {
@@ -115,6 +219,18 @@ function spawnSessionPty(
115219 }
116220 }
117221 }
222+
223+ // Detect when Claude is ready (shows "> " prompt)
224+ if ( terminalReady && ! claudeReady && config . codingAgent === "claude" ) {
225+ if ( data . includes ( "> " ) ) {
226+ claudeReady = true ;
227+ // Spawn MCP poller now that Claude is authenticated and ready
228+ // Check if poller doesn't already exist to prevent duplicates
229+ if ( ! mcpPollerPtyProcesses . has ( sessionId ) ) {
230+ spawnMcpPoller ( sessionId , worktreePath ) ;
231+ }
232+ }
233+ }
118234 } ) ;
119235
120236 return ptyProcess ;
@@ -159,12 +275,14 @@ async function ensureFleetcodeExcluded(projectDir: string) {
159275 }
160276}
161277
162- async function createWorktree ( projectDir : string , parentBranch : string , sessionNumber : number ) : Promise < string > {
278+ async function createWorktree ( projectDir : string , parentBranch : string , sessionNumber : number , sessionUuid : string ) : Promise < string > {
163279 const git = simpleGit ( projectDir ) ;
164280 const fleetcodeDir = path . join ( projectDir , ".fleetcode" ) ;
165281 const worktreeName = `session${ sessionNumber } ` ;
166282 const worktreePath = path . join ( fleetcodeDir , worktreeName ) ;
167- const branchName = `fleetcode/session${ sessionNumber } ` ;
283+ // Include short UUID to ensure branch uniqueness across deletes/recreates
284+ const shortUuid = sessionUuid . split ( '-' ) [ 0 ] ;
285+ const branchName = `fleetcode/session${ sessionNumber } -${ shortUuid } ` ;
168286
169287 // Create .fleetcode directory if it doesn't exist
170288 if ( ! fs . existsSync ( fleetcodeDir ) ) {
@@ -250,14 +368,14 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
250368 const sessionId = `session-${ Date . now ( ) } ` ;
251369 const sessionName = `Session ${ sessionNumber } ` ;
252370
371+ // Generate UUID for this session (before creating worktree)
372+ const sessionUuid = uuidv4 ( ) ;
373+
253374 // Ensure .fleetcode is excluded (async, don't wait)
254375 ensureFleetcodeExcluded ( config . projectDir ) ;
255376
256- // Create git worktree
257- const worktreePath = await createWorktree ( config . projectDir , config . parentBranch , sessionNumber ) ;
258-
259- // Generate UUID for this session
260- const sessionUuid = uuidv4 ( ) ;
377+ // Create git worktree with unique branch name
378+ const worktreePath = await createWorktree ( config . projectDir , config . parentBranch , sessionNumber , sessionUuid ) ;
261379
262380 // Create persisted session metadata
263381 const persistedSession : PersistedSession = {
@@ -332,6 +450,13 @@ ipcMain.on("close-session", (_event, sessionId: string) => {
332450 ptyProcess . kill ( ) ;
333451 activePtyProcesses . delete ( sessionId ) ;
334452 }
453+
454+ // Kill MCP poller if active
455+ const mcpPoller = mcpPollerPtyProcesses . get ( sessionId ) ;
456+ if ( mcpPoller ) {
457+ mcpPoller . kill ( ) ;
458+ mcpPollerPtyProcesses . delete ( sessionId ) ;
459+ }
335460} ) ;
336461
337462// Delete session (kill PTY, remove worktree, delete from store)
@@ -343,6 +468,13 @@ ipcMain.on("delete-session", async (_event, sessionId: string) => {
343468 activePtyProcesses . delete ( sessionId ) ;
344469 }
345470
471+ // Kill MCP poller if active
472+ const mcpPoller = mcpPollerPtyProcesses . get ( sessionId ) ;
473+ if ( mcpPoller ) {
474+ mcpPoller . kill ( ) ;
475+ mcpPollerPtyProcesses . delete ( sessionId ) ;
476+ }
477+
346478 // Find and remove from persisted sessions
347479 const sessions = getPersistedSessions ( ) ;
348480 const sessionIndex = sessions . findIndex ( s => s . id === sessionId ) ;
@@ -476,9 +608,15 @@ async function getMcpServerDetails(name: string) {
476608 }
477609}
478610
479- ipcMain . handle ( "list-mcp-servers" , async ( ) => {
611+ ipcMain . handle ( "list-mcp-servers" , async ( _event , sessionId : string ) => {
480612 try {
481- return await listMcpServers ( ) ;
613+ // Trigger an immediate MCP list command in the session's poller
614+ const mcpPoller = mcpPollerPtyProcesses . get ( sessionId ) ;
615+ if ( mcpPoller ) {
616+ mcpPoller . write ( "claude mcp list\r" ) ;
617+ }
618+ // Return empty array - actual results will come via mcp-servers-updated event
619+ return [ ] ;
482620 } catch ( error ) {
483621 console . error ( "Error listing MCP servers:" , error ) ;
484622 return [ ] ;
@@ -541,20 +679,22 @@ const createWindow = () => {
541679 }
542680 } ) ;
543681 activePtyProcesses . clear ( ) ;
682+
683+ // Kill all MCP poller processes
684+ mcpPollerPtyProcesses . forEach ( ( ptyProcess , sessionId ) => {
685+ try {
686+ ptyProcess . kill ( ) ;
687+ } catch ( error ) {
688+ console . error ( `Error killing MCP poller for session ${ sessionId } :` , error ) ;
689+ }
690+ } ) ;
691+ mcpPollerPtyProcesses . clear ( ) ;
544692 } ) ;
545693} ;
546694
547695app . whenReady ( ) . then ( ( ) => {
548696 createWindow ( ) ;
549697
550- // Refresh MCP server list every minute and broadcast to all windows
551- setInterval ( async ( ) => {
552- const servers = await listMcpServers ( ) ;
553- BrowserWindow . getAllWindows ( ) . forEach ( window => {
554- window . webContents . send ( "mcp-servers-updated" , servers ) ;
555- } ) ;
556- } , 60000 ) ; // 60000ms = 1 minute
557-
558698 app . on ( "activate" , ( ) => {
559699 if ( BrowserWindow . getAllWindows ( ) . length === 0 ) {
560700 createWindow ( ) ;
0 commit comments