Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 101 additions & 4 deletions electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3006,7 +3006,9 @@ function deliverDesktopExitedSession(id, exited) {
exitCode: exited.exitCode,
signal: exited.signal,
exitClaimToken: exited.exitClaimToken,
exitDeliveryToken: exited.exitDeliveryToken
exitDeliveryToken: exited.exitDeliveryToken,
hostGeneration: exited.hostGeneration,
outputRevision: exited.outputRevision
});
return true;
}
Expand Down Expand Up @@ -3116,7 +3118,8 @@ function sanitizeMobileWorkspace(value) {
lastActivityAt: Math.max(0, Number(session?.lastActivityAt) || Number(session?.lastResponseAt) || 0),
notified: Boolean(session?.notified),
inputRequired: Boolean(session?.inputRequired),
busy: Boolean(session?.busy)
busy: Boolean(session?.busy),
exited: Boolean(session?.exited)
})).filter((session) => session.id) : [];
const activeId = String(value?.activeId || '').slice(0, 100);
return { groups, sessions: workspaceSessions, activeId: workspaceSessions.some((session) => session.id === activeId) ? activeId : '' };
Expand All @@ -3133,8 +3136,10 @@ function mobileSessionSnapshot() {
groupId: metadata.get(id)?.groupId || '',
notified: Boolean(metadata.get(id)?.notified),
inputRequired: Boolean(metadata.get(id)?.inputRequired),
busy: mobileExitedSessions.has(id) ? false : Boolean(metadata.get(id)?.busy),
exited: mobileExitedSessions.has(id)
busy: mobileExitedSessions.has(id) || metadata.get(id)?.exited
? false
: Boolean(metadata.get(id)?.busy),
exited: mobileExitedSessions.has(id) || Boolean(metadata.get(id)?.exited)
}));
}

Expand Down Expand Up @@ -4084,6 +4089,92 @@ async function closeSession(id) {
}
}

async function cleanupStoppedSession(id, hostGeneration, snapshot = {}, { retainMobile = true } = {}) {
const sessionId = String(id || '');
const generation = String(hostGeneration || '');
if (!sessionId) return false;

if (retainMobile && !mobileExitedSessions.has(sessionId)) {
mobileExitedSessions.set(sessionId, {
data: String(snapshot.terminalState || '').slice(-MOBILE_RESTORED_STATE_MAX_CHARACTERS),
exitCode: null,
revision: 1,
cols: Math.min(1_000, Math.max(2, Math.floor(Number(snapshot.cols) || 80))),
rows: Math.min(500, Math.max(1, Math.floor(Number(snapshot.rows) || 24))),
source: 'raw'
});
while (mobileExitedSessions.size > MOBILE_EXITED_SESSION_LIMIT) {
mobileExitedSessions.delete(mobileExitedSessions.keys().next().value);
}
broadcastMobileSnapshot();
}

if (!generation) {
if (!retainMobile) mobileExitedSessions.delete(sessionId);
broadcastMobileSnapshot();
return true;
}

const liveSession = sessions.get(sessionId);
if (liveSession) {
if (!liveSession.windowsHosted
|| String(liveSession.processHandle?.generation || '') !== generation) {
throw new Error('The terminal generation changed before stopped state could be cleaned up.');
}
await closeSession(sessionId);
}

const exitedSession = desktopExitedSessions.get(sessionId);
if (!liveSession && exitedSession) {
const exitedGeneration = String(
exitedSession.hostGeneration || exitedSession.processHandle?.generation || ''
);
if (exitedGeneration !== generation) {
throw new Error('The terminal generation changed before stopped state could be cleaned up.');
}
await exitedSession.processHandle.kill();
if (desktopExitedSessions.get(sessionId) === exitedSession) {
desktopExitedSessions.delete(sessionId);
}
} else if (!liveSession) {
if (process.platform === 'win32') {
const client = await getWindowsPtyHostClient();
await client.cleanupExitedSession(sessionId, generation);
}
}
if (!retainMobile) mobileExitedSessions.delete(sessionId);
broadcastMobileSnapshot();
return true;
}

async function restoreStoppedSession(options = {}) {
const id = String(options.id || '');
if (!id) throw new Error('A session id is required.');
const tmux = tmuxRuntime();
const tmuxSession = tmux ? tmuxSessionName(id) : '';
if (tmux && tmuxSessionExists(tmux, tmuxSession)) {
desktopExitedSessions.delete(id);
return createSession(options);
}

const confirmedCheckpoint = options.exitCheckpointConfirmed === true;
if (process.platform === 'win32' && !confirmedCheckpoint) {
return createSession(options);
}

desktopExitedSessions.delete(id);
try {
await cleanupStoppedSession(id, options.checkpointGeneration, {
terminalState: options.mobileTerminalState,
cols: options.terminalStateCols,
rows: options.terminalStateRows
});
} catch {
// The persisted stopped pane remains usable and closing it retries cleanup.
}
return { id, stopped: true, exited: true };
}

function detachAllSessions() {
if (detachAllSessionsPromise) return detachAllSessionsPromise;
terminalSessionDrainActive = true;
Expand Down Expand Up @@ -4145,6 +4236,7 @@ function registerIpc() {
return true;
});
ipcMain.handle('terminal:create', (_event, options) => createSession(options));
ipcMain.handle('terminal:restore-stopped', (_event, options) => restoreStoppedSession(options));
ipcMain.handle('terminal:renderer-ready', (_event, id) => markTerminalRendererReady(String(id || '')));
ipcMain.on('terminal:write', (_event, { id, data }) => {
sessions.get(id)?.processHandle.write(data);
Expand Down Expand Up @@ -4229,6 +4321,11 @@ function registerIpc() {
);
return operation;
});
ipcMain.handle('terminal:cleanup-stopped', (_event, {
id, hostGeneration, terminalState, cols, rows, retainMobile = true
} = {}) => (
cleanupStoppedSession(id, hostGeneration, { terminalState, cols, rows }, { retainMobile })
));
ipcMain.handle('terminal:get-state', (_event, id) => {
const session = sessions.get(id);
if (!session) return null;
Expand Down
4 changes: 4 additions & 0 deletions electron/preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ contextBridge.exposeInMainWorld('sideTerm', {
saveTerminalCheckpoint: (checkpoint) => ipcRenderer.invoke('workspace:save-terminal-checkpoint', checkpoint),
pruneTerminalCheckpoints: (activeIds) => ipcRenderer.invoke('workspace:prune-terminal-checkpoints', activeIds),
createSession: (options) => ipcRenderer.invoke('terminal:create', options),
restoreStoppedSession: (options) => ipcRenderer.invoke('terminal:restore-stopped', options),
markRendererReady: (id) => ipcRenderer.invoke('terminal:renderer-ready', id),
write: (id, data) => ipcRenderer.send('terminal:write', { id, data }),
resize: (id, cols, rows) => ipcRenderer.send('terminal:resize', { id, cols, rows }),
scroll: (id, amount) => ipcRenderer.send('terminal:scroll', { id, amount }),
armGithubPush: (id, details) => ipcRenderer.send('github:push-armed', { id, details }),
close: (id) => ipcRenderer.invoke('terminal:close', id),
cleanupStopped: (id, hostGeneration, snapshot = {}, retainMobile = true) => ipcRenderer.invoke('terminal:cleanup-stopped', {
...snapshot, id, hostGeneration, retainMobile
}),
getSessionState: (id) => ipcRenderer.invoke('terminal:get-state', id),
onData: (callback) => subscribe('terminal:data', callback),
acknowledgeData: (id, byteLength, replayClaimToken = '', replayDeliveryToken = '', rendererDataDeliveryToken = '', exitClaimToken = '', exitDeliveryToken = '') => ipcRenderer.send('terminal:data-ack', {
Expand Down
7 changes: 6 additions & 1 deletion electron/sessions/reattach.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ function reattachSession(id, session, { cols = 100, rows = 30 } = {}) {
const nextRows = positiveInteger(rows, 30);
session.cols = Math.max(2, nextCols);
session.rows = nextRows;
session.processHandle.resize(session.cols, session.rows);
try {
session.processHandle.resize(session.cols, session.rows);
} catch {
// A direct node-pty process can exit between lookup and resize. Its queued
// exit event remains authoritative and is delivered after renderer-ready.
}
return sessionDetails(id, session, { reattached: true });
}

Expand Down
9 changes: 9 additions & 0 deletions electron/sessions/windows-pty-client.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,15 @@ class WindowsPtyHostClient {
return handle;
}

cleanupExitedSession(id, generation) {
const sessionId = String(id || '');
const hostGeneration = String(generation || '');
if (!sessionId || !hostGeneration) {
return Promise.reject(new Error('A session id and host generation are required.'));
}
return this.request('kill', { id: sessionId, generation: hostGeneration });
}

shutdownIfIdle() {
return this.request('shutdown-if-idle');
}
Expand Down
Loading