diff --git a/package-lock.json b/package-lock.json index 9d666a89..f8b70286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -171,7 +171,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -537,7 +536,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -581,7 +579,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -625,7 +622,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1049,6 +1045,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1070,6 +1067,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1086,6 +1084,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1100,6 +1099,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2558,7 +2558,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2744,7 +2745,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2756,7 +2756,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3488,7 +3487,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4260,7 +4258,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4625,7 +4624,6 @@ "integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.4.0", "builder-util": "26.3.4", @@ -4733,7 +4731,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "10.1.0", @@ -4869,7 +4868,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^18.11.18", @@ -5083,6 +5081,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -5103,6 +5102,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6574,7 +6574,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -6604,7 +6603,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -7266,6 +7264,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9283,7 +9282,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9434,6 +9432,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -9451,6 +9450,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -9477,6 +9477,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9492,6 +9493,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9608,7 +9610,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9621,7 +9622,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9635,7 +9635,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -9941,6 +9942,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -10738,6 +10740,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -10801,6 +10804,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -10909,7 +10913,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11048,7 +11051,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -11824,7 +11826,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -12441,7 +12442,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12455,7 +12455,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/main/main.ts b/src/main/main.ts index 1dede924..e7dcab63 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -536,6 +536,7 @@ interface SessionState { allowedPaths: Set; // Per-session allowed out-of-scope paths (parent directories) isProcessing: boolean; // Whether the session is currently waiting for a response yoloMode: boolean; // Auto-approve all permission requests without prompting + unsubscribeEvents?: () => void; // Unsubscribe from session event forwarding } const sessions = new Map(); let activeSessionId: string | null = null; @@ -543,10 +544,14 @@ let sessionCounter = 0; // Registers event forwarding from a CopilotSession to the renderer via IPC. // Used after createSession and resumeSession to wire up the session. -function registerSessionEventForwarding(sessionId: string, session: CopilotSession): void { - session.on((event) => { +function registerSessionEventForwarding(sessionId: string, session: CopilotSession): () => void { + const unsubscribe = session.on((event) => { if (!mainWindow || mainWindow.isDestroyed()) return; + // Guard against stale handlers from previous session objects + const currentState = sessions.get(sessionId); + if (!currentState || currentState.session !== session) return; + console.log(`[${sessionId}] Event:`, event.type); if (event.type === 'assistant.message_delta') { @@ -626,6 +631,7 @@ function registerSessionEventForwarding(sessionId: string, session: CopilotSessi }); } }); + return unsubscribe; } // Keep-alive interval (5 minutes) to prevent session timeout @@ -694,11 +700,13 @@ async function resumeDisconnectedSession( handlePermissionRequest(request, invocation, sessionId), }); - registerSessionEventForwarding(sessionId, resumedSession); + sessionState.unsubscribeEvents?.(); + const unsubscribeEvents = registerSessionEventForwarding(sessionId, resumedSession); // Update session state with new session object sessionState.session = resumedSession; sessionState.client = client; + sessionState.unsubscribeEvents = unsubscribeEvents; log.info(`[${sessionId}] Session resumed successfully`); return resumedSession; @@ -798,46 +806,7 @@ async function startEarlySessionResumption(): Promise { }); // Set up event handler - session.on((event) => { - if (!mainWindow || mainWindow.isDestroyed()) return; - - console.log(`[${sessionId}] Event:`, event.type); - - if (event.type === 'assistant.message_delta') { - mainWindow.webContents.send('copilot:delta', { - sessionId, - content: event.data.deltaContent, - }); - } else if (event.type === 'assistant.message') { - mainWindow.webContents.send('copilot:message', { - sessionId, - content: event.data.content, - }); - } else if (event.type === 'session.idle') { - const currentSessionState = sessions.get(sessionId); - if (currentSessionState) currentSessionState.isProcessing = false; - mainWindow.webContents.send('copilot:idle', { sessionId }); - requestUserAttention(); - } else if (event.type === 'tool.execution_start') { - console.log(`[${sessionId}] Tool start FULL:`, JSON.stringify(event.data, null, 2)); - mainWindow.webContents.send('copilot:tool-start', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: event.data.toolName, - input: event.data.arguments || (event.data as Record), - }); - } else if (event.type === 'tool.execution_complete') { - console.log(`[${sessionId}] Tool end FULL:`, JSON.stringify(event.data, null, 2)); - const completeData = event.data as Record; - mainWindow.webContents.send('copilot:tool-end', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: completeData.toolName, - input: completeData.arguments || completeData, - output: event.data.result?.content || completeData.output, - }); - } - }); + const unsubscribeEvents = registerSessionEventForwarding(sessionId, session); // Store in sessions map const alwaysAllowedSet = new Set(storedAlwaysAllowed.map(normalizeAlwaysAllowed)); @@ -850,6 +819,7 @@ async function startEarlySessionResumption(): Promise { allowedPaths: new Set(), isProcessing: false, yoloMode: yoloMode || false, + unsubscribeEvents, }); console.log(`Early resumed session ${sessionId}`); @@ -1488,7 +1458,7 @@ Browser tools available: browser_navigate, browser_click, browser_fill, browser_ const sessionId = newSession.sessionId; // Use SDK's session ID - registerSessionEventForwarding(sessionId, newSession); + const unsubscribeEvents = registerSessionEventForwarding(sessionId, newSession); sessions.set(sessionId, { session: newSession, @@ -1499,6 +1469,7 @@ Browser tools available: browser_navigate, browser_click, browser_fill, browser_ allowedPaths: new Set(), isProcessing: false, yoloMode: false, + unsubscribeEvents, }); activeSessionId = sessionId; @@ -1711,46 +1682,7 @@ async function initCopilot(): Promise { }); // Set up event handler for resumed session - session.on((event) => { - if (!mainWindow || mainWindow.isDestroyed()) return; - - console.log(`[${sessionId}] Event:`, event.type); - - if (event.type === 'assistant.message_delta') { - mainWindow.webContents.send('copilot:delta', { - sessionId, - content: event.data.deltaContent, - }); - } else if (event.type === 'assistant.message') { - mainWindow.webContents.send('copilot:message', { - sessionId, - content: event.data.content, - }); - } else if (event.type === 'session.idle') { - const currentSessionState = sessions.get(sessionId); - if (currentSessionState) currentSessionState.isProcessing = false; - mainWindow.webContents.send('copilot:idle', { sessionId }); - requestUserAttention(); - } else if (event.type === 'tool.execution_start') { - console.log(`[${sessionId}] Tool start FULL:`, JSON.stringify(event.data, null, 2)); - mainWindow.webContents.send('copilot:tool-start', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: event.data.toolName, - input: event.data.arguments || (event.data as Record), - }); - } else if (event.type === 'tool.execution_complete') { - console.log(`[${sessionId}] Tool end FULL:`, JSON.stringify(event.data, null, 2)); - const completeData = event.data as Record; - mainWindow.webContents.send('copilot:tool-end', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: completeData.toolName, - input: completeData.arguments || completeData, - output: event.data.result?.content || completeData.output, - }); - } - }); + const unsubscribeEvents = registerSessionEventForwarding(sessionId, session); // Restore alwaysAllowed set from stored data (normalize legacy ids) const alwaysAllowedSet = new Set(storedAlwaysAllowed.map(normalizeAlwaysAllowed)); @@ -1763,6 +1695,7 @@ async function initCopilot(): Promise { allowedPaths: new Set(), isProcessing: false, yoloMode: storedSession?.yoloMode || false, + unsubscribeEvents, }); const resumed = { @@ -2399,41 +2332,29 @@ ipcMain.handle('copilot:setModel', async (_event, data: { sessionId: string; mod const sessionState = sessions.get(data.sessionId); if (sessionState) { - const { cwd, client } = sessionState; - - // Destroy local session state (conversation history is preserved on server) - console.log(`Destroying session ${data.sessionId} before model change to ${data.model}`); - await sessionState.session.destroy(); - sessions.delete(data.sessionId); + console.log(`Resuming session ${data.sessionId} with model change to ${data.model}`); const mcpConfig = await readMcpConfig(); const browserTools = createBrowserTools(data.sessionId); - // Resume the same session with the new model — preserves conversation context - const resumedSession = await client.resumeSession(data.sessionId, { + // Resume the session with the new model — preserves full conversation history + const resumedSession = await sessionState.client.resumeSession(data.sessionId, { model: data.model, mcpServers: mcpConfig.mcpServers, tools: browserTools, onPermissionRequest: (request, invocation) => - handlePermissionRequest(request, invocation, resumedSession.sessionId), + handlePermissionRequest(request, invocation, data.sessionId), }); - const resumedSessionId = resumedSession.sessionId; - registerSessionEventForwarding(resumedSessionId, resumedSession); + sessionState.unsubscribeEvents?.(); + const unsubscribeEvents = registerSessionEventForwarding(data.sessionId, resumedSession); - sessions.set(resumedSessionId, { - session: resumedSession, - client, - model: data.model, - cwd, - alwaysAllowed: new Set(sessionState.alwaysAllowed), - allowedPaths: new Set(sessionState.allowedPaths), - isProcessing: false, - }); - activeSessionId = resumedSessionId; + sessionState.session = resumedSession; + sessionState.model = data.model; + sessionState.unsubscribeEvents = unsubscribeEvents; - console.log(`Session ${resumedSessionId} resumed with model ${data.model}`); - return { sessionId: resumedSessionId, model: data.model, cwd }; + console.log(`Session ${data.sessionId} resumed with model ${data.model}`); + return { sessionId: data.sessionId, model: data.model, cwd: sessionState.cwd }; } return { model: data.model }; @@ -3945,88 +3866,7 @@ ipcMain.handle('copilot:resumePreviousSession', async (_event, sessionId: string }); // Set up event handler - session.on((event) => { - if (!mainWindow || mainWindow.isDestroyed()) return; - - console.log(`[${sessionId}] Event:`, event.type); - - if (event.type === 'assistant.message_delta') { - mainWindow.webContents.send('copilot:delta', { sessionId, content: event.data.deltaContent }); - } else if (event.type === 'assistant.message') { - mainWindow.webContents.send('copilot:message', { sessionId, content: event.data.content }); - } else if (event.type === 'session.idle') { - const currentSessionState = sessions.get(sessionId); - if (currentSessionState) currentSessionState.isProcessing = false; - mainWindow.webContents.send('copilot:idle', { sessionId }); - requestUserAttention(); - } else if (event.type === 'tool.execution_start') { - console.log(`[${sessionId}] Tool start FULL:`, JSON.stringify(event.data, null, 2)); - mainWindow.webContents.send('copilot:tool-start', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: event.data.toolName, - input: event.data.arguments || (event.data as Record), - }); - } else if (event.type === 'tool.execution_complete') { - console.log(`[${sessionId}] Tool end FULL:`, JSON.stringify(event.data, null, 2)); - const completeData = event.data as Record; - mainWindow.webContents.send('copilot:tool-end', { - sessionId, - toolCallId: event.data.toolCallId, - toolName: completeData.toolName, - input: completeData.arguments || completeData, - output: event.data.result?.content || completeData.output, - }); - } else if (event.type === 'session.error') { - console.log(`[${sessionId}] Session error:`, event.data); - const errorMessage = event.data?.message || JSON.stringify(event.data); - - // Auto-repair tool_result errors (duplicate or orphaned after compaction) - if ( - errorMessage.includes('multiple `tool_result` blocks') || - errorMessage.includes('each tool_use must have a single result') || - errorMessage.includes('unexpected `tool_use_id`') || - errorMessage.includes('Each `tool_result` block must have a corresponding `tool_use`') - ) { - log.info(`[${sessionId}] Detected tool_result corruption error, attempting auto-repair...`); - repairDuplicateToolResults(sessionId).then((repaired) => { - if (repaired) { - mainWindow?.webContents.send('copilot:error', { - sessionId, - message: 'Session repaired. Please resend your last message.', - isRepaired: true, - }); - } else { - mainWindow?.webContents.send('copilot:error', { sessionId, message: errorMessage }); - } - }); - return; - } - - mainWindow.webContents.send('copilot:error', { sessionId, message: errorMessage }); - } else if (event.type === 'session.usage_info') { - mainWindow.webContents.send('copilot:usageInfo', { - sessionId, - tokenLimit: event.data.tokenLimit, - currentTokens: event.data.currentTokens, - messagesLength: event.data.messagesLength, - }); - } else if (event.type === 'session.compaction_start') { - console.log(`[${sessionId}] Compaction started`); - mainWindow.webContents.send('copilot:compactionStart', { sessionId }); - } else if (event.type === 'session.compaction_complete') { - console.log(`[${sessionId}] Compaction complete:`, event.data); - mainWindow.webContents.send('copilot:compactionComplete', { - sessionId, - success: event.data.success, - preCompactionTokens: event.data.preCompactionTokens, - postCompactionTokens: event.data.postCompactionTokens, - tokensRemoved: event.data.tokensRemoved, - summaryContent: event.data.summaryContent, - error: event.data.error, - }); - } - }); + const unsubscribeEvents = registerSessionEventForwarding(sessionId, session); sessions.set(sessionId, { session, @@ -4037,6 +3877,7 @@ ipcMain.handle('copilot:resumePreviousSession', async (_event, sessionId: string allowedPaths: new Set(), isProcessing: false, yoloMode: false, + unsubscribeEvents, }); activeSessionId = sessionId; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7e23da81..41055bbc 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3816,7 +3816,10 @@ Only when ALL the above are verified complete, output exactly: ${RALPH_COMPLETIO try { const result = await window.electronAPI.copilot.setModel(activeTab.id, model); - // Update the tab in-place: swap session ID and model, preserve everything else + if (!result.sessionId) { + throw new Error('Model change did not return a valid session'); + } + // Update the tab model (session ID is preserved by resumeSession) setTabs((prev) => prev.map((t) => t.id === activeTab.id @@ -3829,7 +3832,9 @@ Only when ALL the above are verified complete, output exactly: ${RALPH_COMPLETIO : t ) ); - setActiveTabId(result.sessionId); + if (result.sessionId !== activeTab.id) { + setActiveTabId(result.sessionId); + } setStatus('connected'); } catch (error) { console.error('Failed to change model:', error);