diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index 40179fe79..e2052dc28 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -1091,6 +1091,112 @@ describe('TmuxSessionManager', () => { paneId: '%isolated-session-ses_first', sessionId: 'ses_first', }) + expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBeUndefined() + expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined() + }) + + test('#given session isolation with another subagent still tracked #when the anchor subagent is deleted first #then it reassigns the anchor and cleans up when the last subagent exits', async () => { + // given + mockIsInsideTmux.mockReturnValue(true) + mockQueryWindowState.mockImplementation(async (paneId) => { + if (paneId === '%isolated-session-ses_first') { + return createWindowState({ + mainPane: { + paneId, + width: 110, + height: 44, + left: 0, + top: 0, + title: 'isolated', + isActive: true, + }, + agentPanes: [ + { + paneId: '%mock', + width: 40, + height: 44, + left: 110, + top: 0, + title: 'omo-subagent-Second Task', + isActive: false, + }, + ], + }) + } + + if (paneId === '%mock') { + return createWindowState({ + mainPane: { + paneId: '%isolated-session-ses_first', + width: 110, + height: 44, + left: 0, + top: 0, + title: 'isolated', + isActive: true, + }, + agentPanes: [ + { + paneId, + width: 40, + height: 44, + left: 110, + top: 0, + title: 'omo-subagent-Second Task', + isActive: false, + }, + ], + }) + } + + return createWindowState() + }) + + 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') + ) + await manager.onSessionCreated( + createSessionCreatedEvent('ses_second', 'ses_parent', 'Second Task') + ) + + mockExecuteAction.mockClear() + + // when + await manager.onSessionDeleted({ sessionID: 'ses_first' }) + + // then + expect(mockExecuteAction).toHaveBeenCalledTimes(0) + expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBe('%isolated-session-ses_first') + expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBe('%mock') + + // when + await manager.onSessionDeleted({ sessionID: 'ses_second' }) + + // then + expect(mockExecuteAction).toHaveBeenCalledTimes(2) + expect(mockExecuteAction.mock.calls[0]?.[0]).toEqual({ + type: 'close', + paneId: '%mock', + sessionId: 'ses_second', + }) + expect(mockExecuteAction.mock.calls[1]?.[0]).toEqual({ + type: 'close', + paneId: '%isolated-session-ses_first', + sessionId: 'ses_second', + }) + expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBeUndefined() expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined() }) diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 2a985223d..403efb7c5 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -70,6 +70,7 @@ export class TmuxSessionManager { private nullStateCount = 0 private deps: TmuxUtilDeps private pollingManager: TmuxPollingManager + private isolatedContainerPaneId: string | undefined private isolatedWindowPaneId: string | undefined constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: TmuxUtilDeps = defaultTmuxDeps) { this.client = ctx.client @@ -125,6 +126,7 @@ export class TmuxSessionManager { if (this.isolatedWindowPaneId) { const state = await queryWindowState(this.isolatedWindowPaneId).catch(() => null) if (state) return null + this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined } @@ -136,6 +138,7 @@ export class TmuxSessionManager { : await spawnTmuxWindow(sessionId, title, this.tmuxConfig, this.serverUrl) if (result.success && result.paneId) { + this.isolatedContainerPaneId = result.paneId this.isolatedWindowPaneId = result.paneId log("[tmux-session-manager] isolated container created", { isolation, @@ -172,6 +175,19 @@ export class TmuxSessionManager { } } + private reassignIsolatedContainerAnchor(): void { + const nextAnchor = this.sessions.values().next().value + if (!nextAnchor) { + return + } + + this.isolatedWindowPaneId = nextAnchor.paneId + log("[tmux-session-manager] reassigned isolated container anchor pane", { + sessionId: nextAnchor.sessionId, + paneId: nextAnchor.paneId, + }) + } + private async cleanupIsolatedContainerAfterSessionDeletion( tracked: TrackedSession, isolatedPaneAlreadyClosed: boolean, @@ -182,18 +198,25 @@ export class TmuxSessionManager { } if (this.sessions.size > 0) { + this.reassignIsolatedContainerAnchor() return } + const isolatedContainerPaneId = this.isolatedContainerPaneId + this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined - if (isolatedPaneAlreadyClosed) { + if (!isolatedContainerPaneId) { + return + } + + if (isolatedPaneAlreadyClosed && tracked.paneId === isolatedContainerPaneId) { return } try { const result = await executeAction( - { type: "close", paneId: tracked.paneId, sessionId: tracked.sessionId }, + { type: "close", paneId: isolatedContainerPaneId, sessionId: tracked.sessionId }, { config: this.tmuxConfig, serverUrl: this.serverUrl, @@ -205,13 +228,13 @@ export class TmuxSessionManager { if (!result.success) { log("[tmux-session-manager] failed to close isolated container pane after anchor session deletion", { sessionId: tracked.sessionId, - paneId: tracked.paneId, + paneId: isolatedContainerPaneId, }) } } catch (error) { log("[tmux-session-manager] failed to cleanup isolated container pane after anchor session deletion", { sessionId: tracked.sessionId, - paneId: tracked.paneId, + paneId: isolatedContainerPaneId, error: String(error), }) } @@ -837,6 +860,7 @@ export class TmuxSessionManager { } await this.retryPendingCloses() + this.isolatedContainerPaneId = undefined this.isolatedWindowPaneId = undefined log("[tmux-session-manager] cleanup complete")