diff --git a/src/features/tmux-subagent/manager.test.ts b/src/features/tmux-subagent/manager.test.ts index f252b3efe..6853349a3 100644 --- a/src/features/tmux-subagent/manager.test.ts +++ b/src/features/tmux-subagent/manager.test.ts @@ -11,6 +11,11 @@ type ExecuteActionsResult = { results: Array<{ action: PaneAction; result: ActionResult }> } +type SpawnTmuxContainerResult = { + success: boolean + paneId?: string +} + const mockQueryWindowState = mock<(paneId: string) => Promise>( async () => ({ windowWidth: 212, @@ -32,6 +37,25 @@ const mockExecuteAction = mock<( action: PaneAction, ctx: ExecuteContext ) => Promise>(async () => ({ success: true })) +const mockSpawnTmuxWindow = mock<( + sessionId: string, + description: string, + config: TmuxConfig, + serverUrl: string +) => Promise>(async () => ({ + success: true, + paneId: '%isolated-window', +})) +const mockSpawnTmuxSession = mock<( + sessionId: string, + description: string, + config: TmuxConfig, + serverUrl: string, + sourcePaneId?: string +) => Promise>(async () => ({ + success: true, + paneId: '%isolated-session', +})) const mockIsInsideTmux = mock<() => boolean>(() => true) const mockGetCurrentPaneId = mock<() => string | undefined>(() => '%0') @@ -70,6 +94,8 @@ mock.module('../../shared/tmux', () => { SESSION_MISSING_GRACE_MS, SESSION_READY_POLL_INTERVAL_MS: 100, SESSION_READY_TIMEOUT_MS: 500, + spawnTmuxWindow: mockSpawnTmuxWindow, + spawnTmuxSession: mockSpawnTmuxSession, } }) @@ -133,6 +159,8 @@ describe('TmuxSessionManager', () => { mockPaneExists.mockClear() mockExecuteActions.mockClear() mockExecuteAction.mockClear() + mockSpawnTmuxWindow.mockClear() + mockSpawnTmuxSession.mockClear() mockIsInsideTmux.mockClear() mockGetCurrentPaneId.mockClear() trackedSessions.clear() @@ -150,6 +178,20 @@ describe('TmuxSessionManager', () => { results: [], } }) + mockSpawnTmuxWindow.mockImplementation(async (sessionId) => { + trackedSessions.add(sessionId) + return { + success: true, + paneId: `%isolated-window-${sessionId}`, + } + }) + mockSpawnTmuxSession.mockImplementation(async (sessionId) => { + trackedSessions.add(sessionId) + return { + success: true, + paneId: `%isolated-session-${sessionId}`, + } + }) }) describe('constructor', () => { @@ -349,6 +391,71 @@ describe('TmuxSessionManager', () => { expect(actionsArg[0].type).toBe('spawn') }) + test('#given session isolation with healthy existing container #when second subagent is created #then it spawns inline from isolated pane', 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, + }, + }) + } + + 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') + ) + + mockExecuteActions.mockClear() + + // when + await manager.onSessionCreated( + createSessionCreatedEvent('ses_second', 'ses_parent', 'Second Task') + ) + + // then + expect(mockSpawnTmuxSession).toHaveBeenCalledTimes(1) + expect(mockExecuteActions).toHaveBeenCalledTimes(1) + + const executeActionsCall = mockExecuteActions.mock.calls[0] + expect(executeActionsCall).toBeDefined() + const actions = executeActionsCall?.[0] + const context = executeActionsCall?.[1] + + expect(actions).toBeDefined() + expect(actions).toHaveLength(1) + expect(actions?.[0]?.type).toBe('spawn') + + if (actions?.[0]?.type === 'spawn') { + expect(actions[0].sessionId).toBe('ses_second') + expect(actions[0].targetPaneId).toBe('%isolated-session-ses_first') + } + + expect(context?.sourcePaneId).toBe('%isolated-session-ses_first') + }) + test('does NOT spawn pane when session has no parentID', async () => { // given mockIsInsideTmux.mockReturnValue(true) diff --git a/src/features/tmux-subagent/manager.ts b/src/features/tmux-subagent/manager.ts index 077741767..a61b1433d 100644 --- a/src/features/tmux-subagent/manager.ts +++ b/src/features/tmux-subagent/manager.ts @@ -537,7 +537,7 @@ export class TmuxSessionManager { return } - if (this.isIsolated()) { + if (this.isIsolated() && !this.isolatedWindowPaneId) { log("[tmux-session-manager] isolated container failed, skipping inline fallback to preserve isolation", { sessionId }) return }