Merge branch 'fix/p0-7-tmux-cleanup' into dev

This commit is contained in:
YeonGyu-Kim
2026-04-01 18:23:18 -07:00
2 changed files with 123 additions and 0 deletions
@@ -1025,6 +1025,75 @@ describe('TmuxSessionManager', () => {
})
})
test('#given session isolation with a spawned container #when the first isolated subagent is deleted #then it cleans up the isolated container and clears the anchor pane id', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
let stateCallCount = 0
mockQueryWindowState.mockImplementation(async (paneId) => {
stateCallCount++
if (paneId === '%isolated-session-ses_first') {
return createWindowState({
mainPane: {
paneId,
width: 110,
height: 44,
left: 0,
top: 0,
title: 'isolated',
isActive: true,
},
})
}
if (stateCallCount === 1) {
return createWindowState()
}
return createWindowState({
mainPane: {
paneId: '%isolated-session-ses_first',
width: 110,
height: 44,
left: 0,
top: 0,
title: 'isolated',
isActive: true,
},
})
})
const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext()
const config: TmuxConfig = {
enabled: true,
isolation: 'session',
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40,
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated(
createSessionCreatedEvent('ses_first', 'ses_parent', 'First Task')
)
mockExecuteAction.mockClear()
// when
await manager.onSessionDeleted({ sessionID: 'ses_first' })
// then
expect(mockExecuteAction).toHaveBeenCalledTimes(1)
expect(mockExecuteAction.mock.calls[0]?.[0]).toEqual({
type: 'close',
paneId: '%isolated-session-ses_first',
sessionId: 'ses_first',
})
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined()
})
test('does nothing when untracked session is deleted', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
+54
View File
@@ -172,6 +172,51 @@ export class TmuxSessionManager {
}
}
private async cleanupIsolatedContainerAfterSessionDeletion(
tracked: TrackedSession,
isolatedPaneAlreadyClosed: boolean,
state: WindowState,
): Promise<void> {
if (tracked.paneId !== this.isolatedWindowPaneId) {
return
}
if (this.sessions.size > 0) {
return
}
this.isolatedWindowPaneId = undefined
if (isolatedPaneAlreadyClosed) {
return
}
try {
const result = await executeAction(
{ type: "close", paneId: tracked.paneId, sessionId: tracked.sessionId },
{
config: this.tmuxConfig,
serverUrl: this.serverUrl,
windowState: state,
sourcePaneId: this.sourcePaneId ?? tracked.paneId,
},
)
if (!result.success) {
log("[tmux-session-manager] failed to close isolated container pane after anchor session deletion", {
sessionId: tracked.sessionId,
paneId: tracked.paneId,
})
}
} catch (error) {
log("[tmux-session-manager] failed to cleanup isolated container pane after anchor session deletion", {
sessionId: tracked.sessionId,
paneId: tracked.paneId,
error: String(error),
})
}
}
private markSessionClosePending(sessionId: string): void {
const tracked = this.sessions.get(sessionId)
if (!tracked) return
@@ -698,9 +743,13 @@ export class TmuxSessionManager {
const closeAction = decideCloseAction(state, event.sessionID, this.getSessionMappings())
if (!closeAction) {
this.removeTrackedSession(event.sessionID)
await this.cleanupIsolatedContainerAfterSessionDeletion(tracked, false, state)
return
}
const isolatedPaneAlreadyClosed =
closeAction.type === "close" && closeAction.paneId === tracked.paneId
try {
const result = await executeAction(closeAction, {
config: this.tmuxConfig,
@@ -723,6 +772,11 @@ export class TmuxSessionManager {
}
this.removeTrackedSession(event.sessionID)
await this.cleanupIsolatedContainerAfterSessionDeletion(
tracked,
isolatedPaneAlreadyClosed,
state,
)
}