Files
orca/src/main/ipc/session.ts
T
Jinwoo HongandE2E Test cf94325fb5 fix(layout): main shows live terminals no pane shows and derives what the next startup reconnects (dark, layout-owner build only) (#31118)
Main runs one pass at startup and each window load, with or without a window
(desktop, orca serve, orcad): every live local terminal no pane names is bound
to the pane its record names while main still holds it free, else shown in a new
tab bound to it. The window no longer asks main to adopt terminals; it only reads
that main holds them. The window's layout.apply loses its adopt field.

Main also derives activeWorktreeIdsOnShutdown, remoteSessionIdsByTabId and
activeConnectionIdsAtShutdown from its own layout, terminal records and SSH
connections, replacing the window's copies in every save and the quit checkpoint.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-20 09:00:10 -04:00

105 lines
4.5 KiB
TypeScript

import { ipcMain } from 'electron'
import type { Store } from '../persistence'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { parseTerminalSurfaceCloseTarget } from '../../shared/terminal-surface-close-target'
import { isFrozenOrcadSourceSessionPartition } from '../ssh/orcad-retained-source'
import { markAgentLaunchesClosedByUser } from '../agent-launch/agent-launch-pane-attachment'
import { windowViewPatchForMain, windowViewSaveForMain } from '../persistence/window-save-view'
import { reportWindowSaveDrift } from '../persistence/window-save-divergence'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import type {
WorkspaceSessionPatch,
WorkspaceSessionState
} from '../../shared/workspace-session-state-types'
export function registerSessionHandlers(store: Store, runtime: OrcaRuntimeService): void {
// Why: renderer saves would change a fenced host's frozen source partition.
const isFenced = (hostId?: string | null): boolean =>
isFrozenOrcadSourceSessionPartition(store, hostId)
// Why: hostId is an optional second arg so an older renderer that invokes
// these channels without it keeps reading/writing the 'local' partition
// exactly as before. Channel names stay stable.
ipcMain.handle('session:get', (_event, hostId?: string | null) => {
return store.getWorkspaceSession(hostId)
})
// Why a census channel: boot used to infer which partitions exist from the repo catalog, which
// cannot name an SSH target whose only workspace is a folder — the runtime wrote that partition
// and no reader ever enumerated it (#12723).
ipcMain.handle('session:list-host-ids', () => {
return store.getWorkspaceSessionHostIds()
})
// Switch point: main keeps its own layout over the window's and takes the window's view and
// facts; the drift check reports where the save's layout still differs from main's.
const sessionForMain = (args: WorkspaceSessionState, hostId: string | null | undefined) => {
const stored = store.getWorkspaceSession(hostId)
reportWindowSaveDrift(stored, args, hostId, 'session:set')
return windowViewSaveForMain(stored, args, hostId, (session, partition) =>
runtime.shutdownReconnectFactsFor(session, partition)
)
}
const patchForMain = (args: WorkspaceSessionPatch, hostId: string | null | undefined) => {
const stored = store.getWorkspaceSession(hostId)
reportWindowSaveDrift(stored, { ...stored, ...args }, hostId, 'session:patch')
return windowViewPatchForMain(stored, args, hostId, (session, partition) =>
runtime.shutdownReconnectFactsFor(session, partition)
)
}
ipcMain.handle('session:set', (_event, args: WorkspaceSessionState, hostId?: string | null) => {
if (!isFenced(hostId)) {
store.setWorkspaceSession(
ORCA_LAYOUT_OWNER_IS_MAIN ? sessionForMain(args, hostId) : args,
hostId
)
}
})
ipcMain.handle('session:patch', (_event, args: WorkspaceSessionPatch, hostId?: string | null) => {
if (!isFenced(hostId)) {
store.patchWorkspaceSession(
ORCA_LAYOUT_OWNER_IS_MAIN ? patchForMain(args, hostId) : args,
hostId
)
}
})
// Why: a renderer save cannot shrink membership main owns, so each close commits it explicitly.
ipcMain.handle(
'session:close-terminal-surface',
(_event, args: { worktreeId?: unknown; target?: unknown; reason?: unknown } | undefined) => {
const target = parseTerminalSurfaceCloseTarget(args?.target)
if (typeof args?.worktreeId !== 'string' || !target) {
throw new Error('invalid_terminal_surface')
}
// Why only these two: main alone closes a tab for its process exit.
const reason = args.reason === 'cleanup' ? 'cleanup' : 'user'
if (reason === 'user') {
// A launch still starting or delivering in what the user closed stops, and says so.
markAgentLaunchesClosedByUser(args.worktreeId, target)
}
return runtime.closeTerminalSurfaceFromRenderer({
worktreeId: args.worktreeId,
target,
reason
})
}
)
ipcMain.handle('session:flush', () => {
// Why: durable lifecycle RPCs must propagate disk failures instead of
// returning success through Store.flush(), which intentionally only logs.
return store.flushPendingOrThrowAsync()
})
ipcMain.on(
'session:read-terminal-scrollback-sync',
(event, args: { ref?: unknown } | undefined) => {
event.returnValue =
typeof args?.ref === 'string' ? store.readTerminalScrollbackSnapshot(args.ref) : null
}
)
}