diff --git a/.ai/contexts/ipc-bridge.md b/.ai/contexts/ipc-bridge.md index cf56c6b9..4a987bfe 100644 --- a/.ai/contexts/ipc-bridge.md +++ b/.ai/contexts/ipc-bridge.md @@ -21,7 +21,7 @@ This file is the **canonical inventory** of the IPC surface. When you add a new | `get-projects` | `(showArchived)` | `Project[]` | Sidebar payload. Reads from cache. | | `get-active-sessions` | — | `{sessionId, busy}[]` | Currently open PTY sessions plus each one's live `_cliBusy` flag — see "Busy-state reconciliation" below. | | `get-active-terminals` | — | `Terminal[]` | Active PTY identifiers | -| `open-terminal` | `(id, projectPath, isNew, sessionOptions)` | `{ok, error?, mcpActive}` | Spawn or attach a PTY. | +| `open-terminal` | `(id, projectPath, isNew, sessionOptions)` | `{ok, error?, mcpState, mcpError?}` | Spawn or attach a PTY. | | `session-live-elsewhere` | `(id)` | `{pid, cwd, startedAt} \| null` | Whether another process (another Switchboard, a CLI in a terminal) is running session `id`: a live pid in `~/.claude/sessions/` and no PTY for it in this instance. Called by `openSession` before a resume; see `.ai/contexts/cli-session-state.md` ("Live elsewhere"). | | `sessions-live-elsewhere` | `(ids)` | `{[id]: {pid, cwd, startedAt}}` | The same check for a working-set restore batch, answered from one scan of `~/.claude/sessions/`; ids that are not live are absent, and at most 200 ids are looked up. Called once by `runRestore`; see `.ai/contexts/cli-session-state.md` ("Live elsewhere"). | | `stop-session` | `(id)` | `{ok}` | Kill the PTY for `id`. Local only — a remote-attach session's PTY is the local ssh attach client, so this only detaches it; see `remote-stop-session` for the real remote "stop". | diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b62aaf9..50c417cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ What changes for you in each release of Switchboard. How to write an entry: [doc ## Unreleased ### Fixed +- The IDE Emulation label in a session's terminal header now says whether the CLI is connected: it reads "IDE Emulation" only while it is, "IDE Emulation: waiting for CLI" when Switchboard is listening but the CLI has not connected, and "IDE Emulation: failed" when it could not start for that session, with the reason in its tooltip. A session whose IDE Emulation port was already taken no longer shows the label as if it worked. (#320) - On Windows, the file panel no longer opens or saves a credential file (such as one under `.ssh`) through its 8.3 short name or a `\\?\` path. (#390) ### New - A session's Changes panel also lists the changes in the worktrees its subagents are working in, under a header naming the agent and its branch. Those rows open as read-only diffs; a subagent that works in the session's own directory adds nothing. (#303) diff --git a/docs/ide-emulation.md b/docs/ide-emulation.md index b2dda0c0..7a9f8f26 100644 --- a/docs/ide-emulation.md +++ b/docs/ide-emulation.md @@ -13,8 +13,15 @@ Application → IDE Emulation** and save. It applies to sessions launched or resumed afterwards; running sessions keep what they started with, and the settings page says so when the value changes. It is a global setting only. -While a session is connected, its terminal header shows an **IDE Emulation** -label. +The terminal header says what holds for the session: + +- **IDE Emulation** — the CLI is connected. +- **IDE Emulation: waiting for CLI** — Switchboard is listening but the CLI + has not connected (or has disconnected), so file opens do not reach it. +- **IDE Emulation: failed** — the server could not start (port taken, lock + file not written); the session runs without it, and the tooltip gives the + reason. +- Nothing — IDE Emulation is off for the session. With it off, Switchboard does not register, and `claude` finds your own IDE (VS Code, Cursor, …) the way it does outside Switchboard. diff --git a/eslint.config.js b/eslint.config.js index fcd0e2ff..6f58a747 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -275,7 +275,7 @@ const rendererCrossFileGlobals = { openWorkFile: 'readonly', clearNotifications: 'readonly', clearUnread: 'readonly', - setSessionMcpActive: 'readonly', + setSessionMcpState: 'readonly', setSessionSandboxed: 'readonly', destroySession: 'readonly', launchNewSession: 'readonly', diff --git a/main.js b/main.js index c466942c..5a691823 100644 --- a/main.js +++ b/main.js @@ -17,7 +17,7 @@ if (!app.isPackaged && !process.env.SWITCHBOARD_DATA_DIR) { // getFolderIndexMtimeMs moved to session-cache.js const { appendToOutputBuffer, MAX_BUFFER_SIZE } = require('./output-buffer'); -const { startMcpServer, shutdownMcpServer, shutdownAll: shutdownAllMcp, resolvePendingDiff, rekeyMcpServer, cleanStaleLockFiles } = require('./mcp-bridge'); +const { startMcpServer, shutdownMcpServer, shutdownAll: shutdownAllMcp, resolvePendingDiff, rekeyMcpServer, cleanStaleLockFiles, getMcpState } = require('./mcp-bridge'); const { fetchAndTransformUsage } = require('./claude-auth'); // SWITCHBOARD_DATA_DIR isolates a dev/test instance from the installed app: @@ -2339,7 +2339,11 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se mainWindow.webContents.send('terminal-data', sessionId, '\x1b[?25l'); } - return { ok: true, reattached: true, mcpActive: !!session.mcpServer, sandbox: !!session.sandbox }; + return { + ok: true, reattached: true, sandbox: !!session.sandbox, + mcpState: session.mcpError ? 'failed' : getMcpState(session.realSessionId || sessionId), + mcpError: session.mcpError || null, + }; } // see .ai/contexts/session-cache.md ("Remote hosts — tmux attach") @@ -2466,6 +2470,7 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se let ptyProcess; let mcpServer = null; + let mcpError = null; try { if (isPlainTerminal) { const launch = plainTerminalLaunch({ @@ -2584,6 +2589,7 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se mcpServer = await startMcpServer(sessionId, [spawnCwd], mainWindow, log); claudeCmd += ' --ide'; } catch (err) { + mcpError = err.message; log.error(`[mcp] Failed to start MCP server for ${sessionId}: ${err.message}`); } } @@ -2655,7 +2661,7 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se // Recorded so a reattach can report it too — the renderer badges sandboxed // sessions, and a reattached session is still inside the same sandbox. sandbox: !!sessionOptions?.sandbox, - mcpServer, _openedAt: Date.now(), + mcpServer, mcpError, _openedAt: Date.now(), // see docs/automation.md — the trigger watcher's politeness guard composerState: createComposerState(), // see .ai/contexts/trigger-watcher.md, "Session handle" @@ -2676,7 +2682,11 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se log.info(`[fork-spawn] tempId=${sessionId} forkFrom=${sessionOptions.forkFrom} folder=${projectFolder} knownFiles=${knownJsonlFiles.size}`); } - return { ok: true, reattached: false, mcpActive: !!mcpServer, sandbox: !!sessionOptions?.sandbox }; + return { + ok: true, reattached: false, sandbox: !!sessionOptions?.sandbox, + mcpState: mcpError ? 'failed' : getMcpState(sessionId), + mcpError, + }; }); // --- IPC: activity-trace (fire-and-forget, opt-in) --- diff --git a/mcp-bridge.js b/mcp-bridge.js index 874fd26b..05c06906 100644 --- a/mcp-bridge.js +++ b/mcp-bridge.js @@ -313,10 +313,10 @@ async function handleGetDiagnostics(entry, rpcId) { * Start an MCP WebSocket server for a session. * @returns {{ port: number, authToken: string }} */ -async function startMcpServer(sessionId, workspaceFolders, mainWindow, log) { +async function startMcpServer(sessionId, workspaceFolders, mainWindow, log, options = {}) { ensureIdeDir(); - const port = await findFreePort(); + const port = options.port ?? await findFreePort(); const authToken = crypto.randomUUID(); const wss = new WebSocketServer({ @@ -327,6 +327,15 @@ async function startMcpServer(sessionId, workspaceFolders, mainWindow, log) { return false; }, }); + try { + await new Promise((resolve, reject) => { + wss.once('listening', resolve); + wss.once('error', reject); + }); + } catch (err) { + try { wss.close(); } catch {} + throw err; + } const lockFilePath = path.join(IDE_DIR, `${port}.lock`); const lockData = JSON.stringify({ @@ -343,7 +352,16 @@ async function startMcpServer(sessionId, workspaceFolders, mainWindow, log) { if (fs.existsSync(lockFilePath)) { try { fs.chmodSync(lockFilePath, 0o600); } catch {} } - fs.writeFileSync(lockFilePath, lockData, { encoding: 'utf8', mode: 0o600 }); + try { + fs.writeFileSync(lockFilePath, lockData, { encoding: 'utf8', mode: 0o600 }); + if (fs.readFileSync(lockFilePath, 'utf8') !== lockData) { + throw new Error(`lock file ${lockFilePath} does not read back as written`); + } + } catch (err) { + try { wss.close(); } catch {} + try { fs.unlinkSync(lockFilePath); } catch {} + throw err; + } const entry = { sessionId, @@ -372,13 +390,17 @@ async function startMcpServer(sessionId, workspaceFolders, mainWindow, log) { try { entry.ws.close(); } catch {} } entry.ws = ws; + notifyStatus(entry); ws.on('message', (data) => { handleMessage(entry, data.toString(), log); }); ws.on('close', () => { - if (entry.ws === ws) entry.ws = null; + if (entry.ws === ws) { + entry.ws = null; + notifyStatus(entry); + } log.debug(`[mcp] session=${sessionId} CLI disconnected`); }); @@ -397,6 +419,23 @@ async function startMcpServer(sessionId, workspaceFolders, mainWindow, log) { return { port, authToken }; } +/** + * What holds for a session's IDE emulation: 'connected' while the CLI is + * attached, 'listening' when the server is up and no CLI is attached, 'off' + * when there is no server. + */ +function getMcpState(sessionId) { + const entry = servers.get(sessionId); + if (!entry) return 'off'; + return entry.ws ? 'connected' : 'listening'; +} + +function notifyStatus(entry) { + if (entry.mainWindow && !entry.mainWindow.isDestroyed()) { + entry.mainWindow.webContents.send('mcp-status', entry.sessionId, entry.ws ? 'connected' : 'listening'); + } +} + /** * Shut down the MCP server for a session. */ @@ -489,6 +528,7 @@ function cleanStaleLockFiles(log) { module.exports = { startMcpServer, + getMcpState, shutdownMcpServer, shutdownAll, resolvePendingDiff, diff --git a/preload.js b/preload.js index 1efcb553..850110f1 100644 --- a/preload.js +++ b/preload.js @@ -174,6 +174,9 @@ contextBridge.exposeInMainWorld('api', { onMcpCloseAllDiffs: (callback) => { ipcRenderer.on('mcp-close-all-diffs', (_event, sessionId) => callback(sessionId)); }, + onMcpStatus: (callback) => { + ipcRenderer.on('mcp-status', (_event, sessionId, state) => callback(sessionId, state)); + }, onMcpCloseTab: (callback) => { ipcRenderer.on('mcp-close-tab', (_event, sessionId, diffId) => callback(sessionId, diffId)); }, diff --git a/public/app.js b/public/app.js index 7f656e99..b5f42813 100644 --- a/public/app.js +++ b/public/app.js @@ -1161,7 +1161,7 @@ async function launchNewSession(project, sessionOptions) { return; } syncPtySizeAfterOpen(entry); - if (typeof setSessionMcpActive === 'function') setSessionMcpActive(sessionId, !!result.mcpActive); + if (typeof setSessionMcpState === 'function') setSessionMcpState(sessionId, result.mcpState, result.mcpError); setSessionSandboxed(sessionId, result.sandbox); showSession(sessionId); @@ -1235,7 +1235,7 @@ async function openSession(session, customOptions, { automatic = false, live } = } skippedWorkingSetEntries.delete(sessionId); syncPtySizeAfterOpen(entry); - if (typeof setSessionMcpActive === 'function') setSessionMcpActive(sessionId, !!result.mcpActive); + if (typeof setSessionMcpState === 'function') setSessionMcpState(sessionId, result.mcpState, result.mcpError); setSessionSandboxed(sessionId, result.sandbox); showSession(sessionId); diff --git a/public/dialogs.js b/public/dialogs.js index 79094729..04e61086 100644 --- a/public/dialogs.js +++ b/public/dialogs.js @@ -73,7 +73,7 @@ async function launchScheduleCreator(project) { return; } syncPtySizeAfterOpen(entry); - if (typeof setSessionMcpActive === 'function') setSessionMcpActive(result.sessionId, !!openResult.mcpActive); + if (typeof setSessionMcpState === 'function') setSessionMcpState(result.sessionId, openResult.mcpState, openResult.mcpError); if (typeof setSessionSandboxed === 'function') setSessionSandboxed(result.sessionId, openResult.sandbox); showSession(result.sessionId); pollActiveSessions(); diff --git a/public/file-panel.js b/public/file-panel.js index f24049dc..72715323 100644 --- a/public/file-panel.js +++ b/public/file-panel.js @@ -415,6 +415,12 @@ function wireIpcListeners() { closeDiffByDiffId(sessionId, diffId); }); + if (window.api.onMcpStatus) { + window.api.onMcpStatus((sessionId, mcpState) => { + setSessionMcpState(sessionId, mcpState); + }); + } + if (window.api.onGitChangesFileChanged) { window.api.onGitChangesFileChanged((sessionId, filePath) => { handleChangesFileChanged(sessionId, filePath); @@ -430,15 +436,17 @@ function getSessionState(sessionId) { currentTab: null, panelVisible: false, panelWidth: DEFAULT_PANEL_WIDTH, - mcpActive: false, + mcpState: 'off', + mcpDetail: '', }); } return filePanelState.get(sessionId); } -function setSessionMcpActive(sessionId, active) { +function setSessionMcpState(sessionId, mcpState, detail) { const state = getSessionState(sessionId); - state.mcpActive = active; + state.mcpState = mcpState || 'off'; + state.mcpDetail = detail || ''; if (currentPanelSessionId === sessionId) updateMcpIndicator(); } @@ -755,7 +763,15 @@ function updateMcpIndicator() { return; } const state = filePanelState.get(currentPanelSessionId); - mcpIndicatorEl.style.display = (state && state.mcpActive) ? '' : 'none'; + const mcpState = state ? state.mcpState : 'off'; + const look = MCP_INDICATOR_STATES[mcpState]; + if (!look) { + mcpIndicatorEl.style.display = 'none'; + return; + } + mcpIndicatorEl.textContent = look.text; + mcpIndicatorEl.title = look.title + (mcpState === 'failed' && state.mcpDetail ? ` (${state.mcpDetail})` : ''); + mcpIndicatorEl.style.display = ''; } // ── Panel Rendering ───────────────────────────────────────────────── @@ -1773,6 +1789,12 @@ function classifyDiffLine(line) { let mcpIndicatorEl = null; +const MCP_INDICATOR_STATES = { + connected: { text: 'IDE Emulation', title: 'IDE Emulation is active: the CLI is connected. Go to Global Settings to disable.' }, + listening: { text: 'IDE Emulation: waiting for CLI', title: 'IDE Emulation server is listening but the CLI is not connected, so file opens will not reach Switchboard.' }, + failed: { text: 'IDE Emulation: failed', title: 'IDE Emulation could not start for this session; it runs without it.' }, +}; + function addMcpToggle() { mcpIndicatorEl = document.createElement('span'); mcpIndicatorEl.id = 'ide-emulation-indicator'; diff --git a/test/dom-ide-emulation-indicator.test.js b/test/dom-ide-emulation-indicator.test.js new file mode 100644 index 00000000..28214c3a --- /dev/null +++ b/test/dom-ide-emulation-indicator.test.js @@ -0,0 +1,107 @@ +'use strict'; + +// The IDE Emulation badge shows the state that holds for the session — off, +// failed to start, listening with no CLI attached, or connected — not whether +// a server object exists (#320). + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const { JSDOM } = require('jsdom'); + +const PUBLIC_DIR = path.join(__dirname, '..', 'public'); + +const INDEX_HTML = ` + + + +
+ + +`; + +function setup() { + const dom = new JSDOM(INDEX_HTML, { url: 'http://localhost/', runScripts: 'outside-only', pretendToBeVisual: true }); + const { window } = dom; + const calls = { status: null }; + window.api = new Proxy({ + onMcpStatus: (cb) => { calls.status = cb; }, + }, { + get(target, prop) { + if (prop in target) return target[prop]; + if (typeof prop === 'string' && prop.startsWith('on')) return () => {}; + return () => Promise.resolve({ ok: true }); + }, + }); + Object.defineProperty(window, 'activeSessionId', { value: null, writable: true, configurable: true }); + for (const f of ['viewer-toolbar.js', 'viewer-panel.js', 'splitter.js', 'session-state.js', 'session-activity-dom.js', 'session-activity.js', 'header-controls.js', 'file-panel.js']) { + vm.runInContext(fs.readFileSync(path.join(PUBLIC_DIR, f), 'utf8'), dom.getInternalVMContext(), { filename: path.join(PUBLIC_DIR, f) }); + } + window.initFilePanel(); + const badge = () => window.document.getElementById('ide-emulation-indicator'); + return { window, calls, badge, destroy: () => window.close() }; +} + +test('a session with no IDE emulation shows no badge', () => { + const ctx = setup(); + try { + ctx.window.switchPanel('s1'); + ctx.window.setSessionMcpState('s1', 'off'); + assert.equal(ctx.badge().style.display, 'none'); + } finally { ctx.destroy(); } +}); + +test('a listening server whose CLI has not attached says it is waiting, not that IDE emulation works', () => { + const ctx = setup(); + try { + ctx.window.switchPanel('s1'); + ctx.window.setSessionMcpState('s1', 'listening'); + assert.notEqual(ctx.badge().style.display, 'none'); + assert.equal(ctx.badge().textContent, 'IDE Emulation: waiting for CLI'); + assert.match(ctx.badge().title, /not connected/i); + } finally { ctx.destroy(); } +}); + +test('a connected CLI shows the plain IDE Emulation badge', () => { + const ctx = setup(); + try { + ctx.window.switchPanel('s1'); + ctx.window.setSessionMcpState('s1', 'connected'); + assert.notEqual(ctx.badge().style.display, 'none'); + assert.equal(ctx.badge().textContent, 'IDE Emulation'); + } finally { ctx.destroy(); } +}); + +test('a server that could not start shows the failure and its reason', () => { + const ctx = setup(); + try { + ctx.window.switchPanel('s1'); + ctx.window.setSessionMcpState('s1', 'failed', 'EADDRINUSE'); + assert.notEqual(ctx.badge().style.display, 'none'); + assert.equal(ctx.badge().textContent, 'IDE Emulation: failed'); + assert.match(ctx.badge().title, /EADDRINUSE/); + } finally { ctx.destroy(); } +}); + +test('a status pushed from main updates the badge of the session it names, and only that one', () => { + const ctx = setup(); + try { + ctx.window.switchPanel('s1'); + ctx.window.setSessionMcpState('s1', 'listening'); + ctx.window.setSessionMcpState('s2', 'listening'); + + ctx.calls.status('s1', 'connected'); + assert.equal(ctx.badge().textContent, 'IDE Emulation'); + + ctx.calls.status('s2', 'connected'); + ctx.calls.status('s1', 'listening'); + assert.equal(ctx.badge().textContent, 'IDE Emulation: waiting for CLI', 'a disconnect puts the badge back to waiting'); + + ctx.window.switchPanel('s2'); + assert.equal(ctx.badge().textContent, 'IDE Emulation', 'the other session kept its own state'); + } finally { ctx.destroy(); } +}); diff --git a/test/dom-sandbox-toggle.test.js b/test/dom-sandbox-toggle.test.js index 517b59ca..afb55d77 100644 --- a/test/dom-sandbox-toggle.test.js +++ b/test/dom-sandbox-toggle.test.js @@ -318,7 +318,7 @@ test('package.json: the sandbox script ships in builds and is asar-unpacked so b test('sandbox badge: main.js reports the sandbox state on both open-terminal returns', () => { const src = fs.readFileSync(path.join(ROOT, 'main.js'), 'utf8'); - const returns = src.match(/return \{ ok: true, reattached: (?:true|false)[^}]*\}/g) || []; + const returns = src.match(/return \{\s*ok: true, reattached: (?:true|false)[^}]*\}/g) || []; // Local reattach, local spawn, and remote attach (issue #221) — each must // report the sandbox state so the renderer badge never reads `undefined`. assert.equal(returns.length, 3, 'open-terminal has exactly three success returns'); diff --git a/test/mcp-bridge-status.test.js b/test/mcp-bridge-status.test.js new file mode 100644 index 00000000..ba0f1492 --- /dev/null +++ b/test/mcp-bridge-status.test.js @@ -0,0 +1,80 @@ +'use strict'; + +// What the bridge reports about a session is what holds: listening until the +// CLI attaches, connected while it is attached, and a server that cannot listen +// is a refusal, not a started server (#320). + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const net = require('node:net'); +const os = require('node:os'); +const path = require('node:path'); + +const home = fs.mkdtempSync(path.join(os.tmpdir(), 'sb-mcp-status-')); +process.env.HOME = home; +process.env.USERPROFILE = home; + +const WebSocket = require('ws'); +const { startMcpServer, shutdownAll, getMcpState } = require('../mcp-bridge'); + +const log = { info() {}, warn() {}, debug() {}, error() {} }; + +test.after(() => { + shutdownAll(); + fs.rmSync(home, { recursive: true, force: true }); +}); + +function windowSpy(sent) { + return { isDestroyed: () => false, webContents: { send: (...args) => sent.push(args) } }; +} + +function open(port, token) { + const ws = new WebSocket(`ws://127.0.0.1:${port}`, 'mcp', { headers: { 'x-claude-code-ide-authorization': token } }); + return new Promise((resolve, reject) => { ws.once('open', () => resolve(ws)); ws.once('error', reject); }); +} + +const until = async (predicate) => { + for (let i = 0; i < 100 && !predicate(); i++) await new Promise((r) => setTimeout(r, 10)); +}; + +test('a session nobody started a server for is off', () => { + assert.equal(getMcpState('never-started'), 'off'); +}); + +test('a started server is listening, connected once the CLI attaches, and listening again when it leaves', async () => { + const sent = []; + const { port, authToken } = await startMcpServer('s-state', [], windowSpy(sent), log); + assert.equal(getMcpState('s-state'), 'listening'); + assert.deepEqual(sent.filter((a) => a[0] === 'mcp-status'), []); + + const ws = await open(port, authToken); + await until(() => getMcpState('s-state') === 'connected'); + assert.equal(getMcpState('s-state'), 'connected'); + assert.deepEqual(sent.filter((a) => a[0] === 'mcp-status').at(-1), ['mcp-status', 's-state', 'connected']); + + ws.close(); + await until(() => getMcpState('s-state') === 'listening'); + assert.equal(getMcpState('s-state'), 'listening'); + assert.deepEqual(sent.filter((a) => a[0] === 'mcp-status').at(-1), ['mcp-status', 's-state', 'listening']); +}); + +test('a connection with a bad token does not make the session connected', async () => { + const sent = []; + const { port } = await startMcpServer('s-badauth', [], windowSpy(sent), log); + const ws = await open(port, 'not-the-token'); + await new Promise((r) => ws.once('close', r)); + assert.equal(getMcpState('s-badauth'), 'listening'); + assert.deepEqual(sent.filter((a) => a[0] === 'mcp-status'), []); +}); + +test('a port already claimed by another process rejects the start and leaves no lock file behind', async () => { + const peer = net.createServer(); + await new Promise((r) => peer.listen(0, '127.0.0.1', r)); + const { port } = peer.address(); + try { + await assert.rejects(startMcpServer('s-busy', [], windowSpy([]), log, { port }), /EADDRINUSE/); + assert.equal(getMcpState('s-busy'), 'off'); + assert.equal(fs.existsSync(path.join(home, '.claude', 'ide', `${port}.lock`)), false); + } finally { peer.close(); } +}); diff --git a/test/process-exit-status.test.js b/test/process-exit-status.test.js index 256c4ea9..7ddd9cc8 100644 --- a/test/process-exit-status.test.js +++ b/test/process-exit-status.test.js @@ -69,7 +69,7 @@ const PRELUDE = ` async function guardResume() { return true; } async function resolveDefaultSessionOptions() { return {}; } function syncPtySizeAfterOpen() {} - function setSessionMcpActive() {} + function setSessionMcpState() {} function setSessionSandboxed() {} function showSession() { updateTerminalHeader(); } function makeEntry() { return { closed: false, initialSize: null, terminal: { write: (d) => calls.writes.push(d) } }; } diff --git a/test/restore-live-elsewhere.test.js b/test/restore-live-elsewhere.test.js index b3220dc5..90ccf9b0 100644 --- a/test/restore-live-elsewhere.test.js +++ b/test/restore-live-elsewhere.test.js @@ -79,7 +79,7 @@ function setup({ savedSet, liveIds, batchFails = false }) { function destroySession(id) { openSessions.delete(id); } async function resolveDefaultSessionOptions() { return {}; } function syncPtySizeAfterOpen() {} - function setSessionMcpActive() {} + function setSessionMcpState() {} function setSessionSandboxed() {} function forgetSessionExit() {} function schedulePersistWorkingSet() {}