fix full-suite isolation regressions
This commit is contained in:
@@ -22,6 +22,22 @@ type SessionReadyWaitParams = {
|
||||
sessionId: string
|
||||
}
|
||||
|
||||
type TmuxSessionManagerContext = ConstructorParameters<typeof import('./manager').TmuxSessionManager>[0]
|
||||
|
||||
type TmuxSessionManagerInternals = {
|
||||
serverUrl: string
|
||||
deferredQueue: string[]
|
||||
tryAttachDeferredSession: () => Promise<void>
|
||||
}
|
||||
|
||||
function cast<TValue>(value: unknown): TValue {
|
||||
return value as TValue
|
||||
}
|
||||
|
||||
function getManagerInternals(manager: TmuxSessionManagerType): TmuxSessionManagerInternals {
|
||||
return cast<TmuxSessionManagerInternals>(manager)
|
||||
}
|
||||
|
||||
const mockQueryWindowState = mock<(paneId: string) => Promise<WindowState | null>>(
|
||||
async () => ({
|
||||
windowWidth: 212,
|
||||
@@ -78,6 +94,8 @@ const mockTmuxDeps: TmuxUtilDeps = {
|
||||
isInsideTmux: mockIsInsideTmux,
|
||||
getCurrentPaneId: mockGetCurrentPaneId,
|
||||
queryWindowState: mockQueryWindowState,
|
||||
waitForSessionReady: mockWaitForSessionReady,
|
||||
log: (...args) => sharedModule.log(...args),
|
||||
}
|
||||
|
||||
function registerModuleMocks(): void {
|
||||
@@ -119,8 +137,8 @@ const readySessions = new Set<string>()
|
||||
function createMockContext(overrides?: {
|
||||
sessionStatusResult?: { data?: Record<string, { type: string }> }
|
||||
sessionMessagesResult?: { data?: unknown[] }
|
||||
}) {
|
||||
return {
|
||||
}): TmuxSessionManagerContext {
|
||||
return cast<TmuxSessionManagerContext>({
|
||||
serverUrl: new URL('http://localhost:4096'),
|
||||
client: {
|
||||
session: {
|
||||
@@ -145,7 +163,7 @@ function createMockContext(overrides?: {
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as any
|
||||
})
|
||||
}
|
||||
|
||||
function createSessionCreatedEvent(
|
||||
@@ -374,7 +392,7 @@ describe('TmuxSessionManager', () => {
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
expect(getManagerInternals(manager).serverUrl).toBe('http://localhost:4096')
|
||||
})
|
||||
|
||||
test('falls back to configured OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
@@ -406,7 +424,7 @@ describe('TmuxSessionManager', () => {
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:5678')
|
||||
expect(getManagerInternals(manager).serverUrl).toBe('http://localhost:5678')
|
||||
})
|
||||
|
||||
test('ignores invalid OPENCODE_PORT when serverUrl has port 0', async () => {
|
||||
@@ -438,7 +456,7 @@ describe('TmuxSessionManager', () => {
|
||||
}
|
||||
|
||||
// then
|
||||
expect((manager as any).serverUrl).toBe('http://localhost:4096')
|
||||
expect(getManagerInternals(manager).serverUrl).toBe('http://localhost:4096')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -808,7 +826,7 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
// then - with small window, manager defers instead of replacing
|
||||
expect(mockExecuteActions).toHaveBeenCalledTimes(0)
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_new'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_new'])
|
||||
})
|
||||
|
||||
test('keeps deferred queue idempotent for duplicate session.created events', async () => {
|
||||
@@ -850,7 +868,7 @@ describe('TmuxSessionManager', () => {
|
||||
)
|
||||
|
||||
// then
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_dup'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_dup'])
|
||||
})
|
||||
|
||||
test('auto-attaches deferred sessions in FIFO order', async () => {
|
||||
@@ -900,17 +918,17 @@ describe('TmuxSessionManager', () => {
|
||||
await manager.onSessionCreated(createSessionCreatedEvent('ses_1', 'ses_parent', 'Task 1'))
|
||||
await manager.onSessionCreated(createSessionCreatedEvent('ses_2', 'ses_parent', 'Task 2'))
|
||||
await manager.onSessionCreated(createSessionCreatedEvent('ses_3', 'ses_parent', 'Task 3'))
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_1', 'ses_2', 'ses_3'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_1', 'ses_2', 'ses_3'])
|
||||
|
||||
// when
|
||||
mockQueryWindowState.mockImplementation(async () => createWindowState())
|
||||
await (manager as any).tryAttachDeferredSession()
|
||||
await (manager as any).tryAttachDeferredSession()
|
||||
await (manager as any).tryAttachDeferredSession()
|
||||
await getManagerInternals(manager).tryAttachDeferredSession()
|
||||
await getManagerInternals(manager).tryAttachDeferredSession()
|
||||
await getManagerInternals(manager).tryAttachDeferredSession()
|
||||
|
||||
// then
|
||||
expect(attachOrder).toEqual(['ses_1', 'ses_2', 'ses_3'])
|
||||
expect((manager as any).deferredQueue).toEqual([])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual([])
|
||||
})
|
||||
|
||||
test('does not attach deferred session more than once across repeated retries', async () => {
|
||||
@@ -963,12 +981,12 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
// when
|
||||
mockQueryWindowState.mockImplementation(async () => createWindowState())
|
||||
await (manager as any).tryAttachDeferredSession()
|
||||
await (manager as any).tryAttachDeferredSession()
|
||||
await getManagerInternals(manager).tryAttachDeferredSession()
|
||||
await getManagerInternals(manager).tryAttachDeferredSession()
|
||||
|
||||
// then
|
||||
expect(attachCount).toBe(1)
|
||||
expect((manager as any).deferredQueue).toEqual([])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual([])
|
||||
})
|
||||
|
||||
test('skips deferred attach when the session is already pending through another spawn path', async () => {
|
||||
@@ -998,7 +1016,7 @@ describe('TmuxSessionManager', () => {
|
||||
await manager.onSessionCreated(
|
||||
createSessionCreatedEvent('ses_pending_race', 'ses_parent', 'Pending Race Task')
|
||||
)
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_pending_race'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_pending_race'])
|
||||
|
||||
mockQueryWindowState.mockImplementation(async () => createWindowState())
|
||||
Reflect.get(manager, 'pendingSessions').add('ses_pending_race')
|
||||
@@ -1008,7 +1026,7 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
// then
|
||||
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_pending_race'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_pending_race'])
|
||||
})
|
||||
|
||||
test('drops deferred sessions that were already closed by polling', async () => {
|
||||
@@ -1038,7 +1056,7 @@ describe('TmuxSessionManager', () => {
|
||||
await manager.onSessionCreated(
|
||||
createSessionCreatedEvent('ses_bounce', 'ses_parent', 'Bounce Task')
|
||||
)
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_bounce'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_bounce'])
|
||||
|
||||
mockQueryWindowState.mockImplementation(async () => createWindowState())
|
||||
Reflect.set(manager, 'closedByPolling', new Set(['ses_bounce']))
|
||||
@@ -1048,7 +1066,7 @@ describe('TmuxSessionManager', () => {
|
||||
|
||||
// then
|
||||
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
|
||||
expect((manager as any).deferredQueue).toEqual([])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual([])
|
||||
})
|
||||
|
||||
test('removes deferred session when session is deleted before attach', async () => {
|
||||
@@ -1084,13 +1102,13 @@ describe('TmuxSessionManager', () => {
|
||||
await manager.onSessionCreated(
|
||||
createSessionCreatedEvent('ses_pending', 'ses_parent', 'Pending Task')
|
||||
)
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_pending'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_pending'])
|
||||
|
||||
// when
|
||||
await manager.onSessionDeleted({ sessionID: 'ses_pending' })
|
||||
|
||||
// then
|
||||
expect((manager as any).deferredQueue).toEqual([])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual([])
|
||||
expect(mockExecuteAction).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
@@ -1179,7 +1197,7 @@ describe('TmuxSessionManager', () => {
|
||||
)
|
||||
|
||||
// then
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_null_state'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_null_state'])
|
||||
|
||||
logSpy.mockRestore()
|
||||
})
|
||||
@@ -1269,7 +1287,7 @@ describe('TmuxSessionManager', () => {
|
||||
)
|
||||
|
||||
// then
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_fail_no_close'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_fail_no_close'])
|
||||
|
||||
logSpy.mockRestore()
|
||||
})
|
||||
@@ -1315,7 +1333,7 @@ describe('TmuxSessionManager', () => {
|
||||
)
|
||||
|
||||
// then
|
||||
expect((manager as any).deferredQueue).toEqual(['ses_fail_with_close'])
|
||||
expect(getManagerInternals(manager).deferredQueue).toEqual(['ses_fail_with_close'])
|
||||
|
||||
logSpy.mockRestore()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { TrackedSession, CapacityConfig, WindowState } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import * as sharedModule from "../../shared"
|
||||
import {
|
||||
isInsideTmux as defaultIsInsideTmux,
|
||||
getCurrentPaneId as defaultGetCurrentPaneId,
|
||||
@@ -53,12 +53,16 @@ export interface TmuxUtilDeps {
|
||||
isInsideTmux: () => boolean
|
||||
getCurrentPaneId: () => string | undefined
|
||||
queryWindowState: (paneId: string) => Promise<WindowState | null>
|
||||
waitForSessionReady: (params: { client: OpencodeClient; sessionId: string }) => Promise<boolean>
|
||||
log: typeof sharedModule.log
|
||||
}
|
||||
|
||||
const defaultTmuxDeps: TmuxUtilDeps = {
|
||||
isInsideTmux: defaultIsInsideTmux,
|
||||
getCurrentPaneId: defaultGetCurrentPaneId,
|
||||
queryWindowState: defaultQueryWindowState,
|
||||
waitForSessionReady,
|
||||
log: sharedModule.log,
|
||||
}
|
||||
|
||||
const DEFERRED_SESSION_TTL_MS = 5 * 60 * 1000
|
||||
@@ -92,11 +96,11 @@ export class TmuxSessionManager {
|
||||
private isolatedContainerNullStateCount = 0
|
||||
private staleSweepCompleted = false
|
||||
private staleSweepInProgress = false
|
||||
constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: TmuxUtilDeps = defaultTmuxDeps) {
|
||||
constructor(ctx: PluginInput, tmuxConfig: TmuxConfig, deps: Partial<TmuxUtilDeps> = {}) {
|
||||
this.client = ctx.client
|
||||
this.tmuxConfig = tmuxConfig
|
||||
this.projectDirectory = ctx.directory || process.cwd()
|
||||
this.deps = deps
|
||||
this.deps = { ...defaultTmuxDeps, ...deps }
|
||||
const configuredPort = process.env.OPENCODE_PORT
|
||||
const parsedPort = configuredPort ? Number(configuredPort) : 4096
|
||||
const defaultPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
||||
@@ -113,20 +117,20 @@ export class TmuxSessionManager {
|
||||
this.serverUrl = fallbackUrl
|
||||
}
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] failed to parse server URL, using fallback", {
|
||||
this.deps.log("[tmux-session-manager] failed to parse server URL, using fallback", {
|
||||
serverUrl: rawServerUrl,
|
||||
error: String(error),
|
||||
})
|
||||
this.serverUrl = fallbackUrl
|
||||
}
|
||||
this.sourcePaneId = deps.getCurrentPaneId()
|
||||
this.sourcePaneId = this.deps.getCurrentPaneId()
|
||||
this.pollingManager = new TmuxPollingManager(
|
||||
this.client,
|
||||
this.sessions,
|
||||
this.closeSessionFromPolling.bind(this),
|
||||
this.retryPendingCloses.bind(this)
|
||||
)
|
||||
log("[tmux-session-manager] initialized", {
|
||||
this.deps.log("[tmux-session-manager] initialized", {
|
||||
configEnabled: this.tmuxConfig.enabled,
|
||||
tmuxConfig: this.tmuxConfig,
|
||||
projectDirectory: this.projectDirectory,
|
||||
@@ -156,7 +160,7 @@ export class TmuxSessionManager {
|
||||
if (!this.isIsolated()) return null
|
||||
if (this.isolatedWindowPaneId) {
|
||||
const state = await this.deps.queryWindowState(this.isolatedWindowPaneId).catch((error) => {
|
||||
log("[tmux-session-manager] failed to query isolated window state", {
|
||||
this.deps.log("[tmux-session-manager] failed to query isolated window state", {
|
||||
paneId: this.isolatedWindowPaneId,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -167,7 +171,7 @@ export class TmuxSessionManager {
|
||||
return null
|
||||
}
|
||||
this.isolatedContainerNullStateCount += 1
|
||||
log("[tmux-session-manager] isolated container state query returned null", {
|
||||
this.deps.log("[tmux-session-manager] isolated container state query returned null", {
|
||||
paneId: this.isolatedWindowPaneId,
|
||||
nullStateCount: this.isolatedContainerNullStateCount,
|
||||
maxNullStateCount: MAX_ISOLATED_CONTAINER_NULL_STATE_COUNT,
|
||||
@@ -181,7 +185,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
const isolation = this.tmuxConfig.isolation
|
||||
log("[tmux-session-manager] creating isolated tmux container", { isolation, sessionId, title })
|
||||
this.deps.log("[tmux-session-manager] creating isolated tmux container", { isolation, sessionId, title })
|
||||
|
||||
const result = isolation === "session"
|
||||
? await spawnTmuxSession(sessionId, title, this.tmuxConfig, this.serverUrl, this.projectDirectory, this.sourcePaneId)
|
||||
@@ -191,13 +195,13 @@ export class TmuxSessionManager {
|
||||
this.isolatedContainerPaneId = result.paneId
|
||||
this.isolatedWindowPaneId = result.paneId
|
||||
this.isolatedContainerNullStateCount = 0
|
||||
log("[tmux-session-manager] isolated container created", {
|
||||
this.deps.log("[tmux-session-manager] isolated container created", {
|
||||
isolation,
|
||||
paneId: result.paneId,
|
||||
})
|
||||
return result.paneId
|
||||
}
|
||||
log("[tmux-session-manager] failed to create isolated container", { isolation, sessionId })
|
||||
this.deps.log("[tmux-session-manager] failed to create isolated container", { isolation, sessionId })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -242,7 +246,7 @@ export class TmuxSessionManager {
|
||||
|
||||
this.isolatedContainerNullStateCount = 0
|
||||
this.isolatedWindowPaneId = nextAnchor.paneId
|
||||
log("[tmux-session-manager] reassigned isolated container anchor pane", {
|
||||
this.deps.log("[tmux-session-manager] reassigned isolated container anchor pane", {
|
||||
sessionId: nextAnchor.sessionId,
|
||||
paneId: nextAnchor.paneId,
|
||||
})
|
||||
@@ -288,13 +292,13 @@ export class TmuxSessionManager {
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
log("[tmux-session-manager] failed to close isolated container pane after anchor session deletion", {
|
||||
this.deps.log("[tmux-session-manager] failed to close isolated container pane after anchor session deletion", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: isolatedContainerPaneId,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] failed to cleanup isolated container pane after anchor session deletion", {
|
||||
this.deps.log("[tmux-session-manager] failed to cleanup isolated container pane after anchor session deletion", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: isolatedContainerPaneId,
|
||||
error: String(error),
|
||||
@@ -307,7 +311,7 @@ export class TmuxSessionManager {
|
||||
if (!tracked) return
|
||||
|
||||
this.sessions.set(sessionId, markTrackedSessionClosePending(tracked))
|
||||
log("[tmux-session-manager] marked session close pending", {
|
||||
this.deps.log("[tmux-session-manager] marked session close pending", {
|
||||
sessionId,
|
||||
paneId: tracked.paneId,
|
||||
closeRetryCount: tracked.closeRetryCount,
|
||||
@@ -321,7 +325,7 @@ export class TmuxSessionManager {
|
||||
try {
|
||||
return await this.deps.queryWindowState(paneId)
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] failed to query window state for close", {
|
||||
this.deps.log("[tmux-session-manager] failed to query window state for close", {
|
||||
error: String(error),
|
||||
})
|
||||
return null
|
||||
@@ -339,7 +343,7 @@ export class TmuxSessionManager {
|
||||
): Promise<boolean> {
|
||||
const state = await this.queryWindowStateSafely()
|
||||
if (!state) {
|
||||
log("[tmux-session-manager] unable to verify pane after max close retries; keeping session tracked", {
|
||||
this.deps.log("[tmux-session-manager] unable to verify pane after max close retries; keeping session tracked", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: tracked.paneId,
|
||||
source,
|
||||
@@ -348,7 +352,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
if (this.windowStateContainsPane(state, tracked.paneId)) {
|
||||
log("[tmux-session-manager] pane still exists after max close retries; manual intervention required", {
|
||||
this.deps.log("[tmux-session-manager] pane still exists after max close retries; manual intervention required", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: tracked.paneId,
|
||||
source,
|
||||
@@ -356,7 +360,7 @@ export class TmuxSessionManager {
|
||||
return false
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] pane already gone after max close retries; finalizing tracked close", {
|
||||
this.deps.log("[tmux-session-manager] pane already gone after max close retries; finalizing tracked close", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: tracked.paneId,
|
||||
source,
|
||||
@@ -389,7 +393,7 @@ export class TmuxSessionManager {
|
||||
|
||||
return result.success
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] close session pane failed", {
|
||||
this.deps.log("[tmux-session-manager] close session pane failed", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: tracked.paneId,
|
||||
error: String(error),
|
||||
@@ -444,7 +448,7 @@ export class TmuxSessionManager {
|
||||
|
||||
const closed = await this.closeTrackedSession(tracked)
|
||||
if (closed) {
|
||||
log("[tmux-session-manager] retried close succeeded", {
|
||||
this.deps.log("[tmux-session-manager] retried close succeeded", {
|
||||
sessionId: tracked.sessionId,
|
||||
paneId: tracked.paneId,
|
||||
closeRetryCount: tracked.closeRetryCount,
|
||||
@@ -468,7 +472,7 @@ export class TmuxSessionManager {
|
||||
closePending: true,
|
||||
closeRetryCount: nextRetryCount,
|
||||
})
|
||||
log("[tmux-session-manager] retried close failed", {
|
||||
this.deps.log("[tmux-session-manager] retried close failed", {
|
||||
sessionId: currentTracked.sessionId,
|
||||
paneId: currentTracked.paneId,
|
||||
closeRetryCount: nextRetryCount,
|
||||
@@ -497,7 +501,7 @@ export class TmuxSessionManager {
|
||||
return
|
||||
}
|
||||
if (this.deferredQueue.length >= MAX_DEFERRED_QUEUE_SIZE) {
|
||||
log("[tmux-session-manager] deferred queue full, dropping session", {
|
||||
this.deps.log("[tmux-session-manager] deferred queue full, dropping session", {
|
||||
sessionId,
|
||||
queueLength: this.deferredQueue.length,
|
||||
maxQueueSize: MAX_DEFERRED_QUEUE_SIZE,
|
||||
@@ -511,7 +515,7 @@ export class TmuxSessionManager {
|
||||
retryIsolatedContainer,
|
||||
})
|
||||
this.deferredQueue.push(sessionId)
|
||||
log("[tmux-session-manager] deferred session queued", {
|
||||
this.deps.log("[tmux-session-manager] deferred session queued", {
|
||||
sessionId,
|
||||
queueLength: this.deferredQueue.length,
|
||||
})
|
||||
@@ -521,7 +525,7 @@ export class TmuxSessionManager {
|
||||
private removeDeferredSession(sessionId: string): void {
|
||||
if (!this.deferredSessions.delete(sessionId)) return
|
||||
this.deferredQueue = this.deferredQueue.filter((id) => id !== sessionId)
|
||||
log("[tmux-session-manager] deferred session removed", {
|
||||
this.deps.log("[tmux-session-manager] deferred session removed", {
|
||||
sessionId,
|
||||
queueLength: this.deferredQueue.length,
|
||||
})
|
||||
@@ -544,7 +548,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
})
|
||||
}, POLL_INTERVAL_BACKGROUND_MS)
|
||||
log("[tmux-session-manager] deferred attach polling started", {
|
||||
this.deps.log("[tmux-session-manager] deferred attach polling started", {
|
||||
intervalMs: POLL_INTERVAL_BACKGROUND_MS,
|
||||
})
|
||||
}
|
||||
@@ -555,7 +559,7 @@ export class TmuxSessionManager {
|
||||
this.deferredAttachInterval = undefined
|
||||
this.deferredAttachTickScheduled = false
|
||||
this.nullStateCount = 0
|
||||
log("[tmux-session-manager] deferred attach polling stopped")
|
||||
this.deps.log("[tmux-session-manager] deferred attach polling stopped")
|
||||
}
|
||||
|
||||
private beginPendingSession(
|
||||
@@ -567,7 +571,7 @@ export class TmuxSessionManager {
|
||||
|| this.pendingSessions.has(sessionId)
|
||||
|| (!options?.allowDeferredSession && this.deferredSessions.has(sessionId))
|
||||
) {
|
||||
log("[tmux-session-manager] session already tracked or pending", { sessionId })
|
||||
this.deps.log("[tmux-session-manager] session already tracked or pending", { sessionId })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -580,7 +584,7 @@ export class TmuxSessionManager {
|
||||
stage: SpawnStage,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const ready = await waitForSessionReady({
|
||||
const ready = await this.deps.waitForSessionReady({
|
||||
client: this.client,
|
||||
sessionId,
|
||||
})
|
||||
@@ -590,14 +594,14 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
const readinessError = new Error("Session readiness timed out")
|
||||
log("[tmux-session-manager] session readiness failed before spawn", {
|
||||
this.deps.log("[tmux-session-manager] session readiness failed before spawn", {
|
||||
sessionId,
|
||||
stage,
|
||||
error: String(readinessError),
|
||||
})
|
||||
return false
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] session readiness failed before spawn", {
|
||||
this.deps.log("[tmux-session-manager] session readiness failed before spawn", {
|
||||
sessionId,
|
||||
stage,
|
||||
error: String(error),
|
||||
@@ -612,7 +616,7 @@ export class TmuxSessionManager {
|
||||
const allStatuses = parseSessionStatusMap(statusResult.data)
|
||||
return allStatuses[sessionId]?.type
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] failed to read session status before spawn", {
|
||||
this.deps.log("[tmux-session-manager] failed to read session status before spawn", {
|
||||
sessionId,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -672,7 +676,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
this.failedReadinessSessions.delete(sessionId)
|
||||
log("[tmux-session-manager] expired failed readiness session", {
|
||||
this.deps.log("[tmux-session-manager] expired failed readiness session", {
|
||||
sessionId,
|
||||
ttlMs: FAILED_READINESS_SESSION_TTL_MS,
|
||||
})
|
||||
@@ -694,7 +698,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
this.failedReadinessSessions.delete(sessionId)
|
||||
log("[tmux-session-manager] expired failed readiness session on access", {
|
||||
this.deps.log("[tmux-session-manager] expired failed readiness session on access", {
|
||||
sessionId,
|
||||
ttlMs: FAILED_READINESS_SESSION_TTL_MS,
|
||||
})
|
||||
@@ -724,7 +728,7 @@ export class TmuxSessionManager {
|
||||
|
||||
const sessionStatus = await this.getSessionStatusType(sessionId)
|
||||
if (!isAttachableSessionStatus(sessionStatus)) {
|
||||
log("[tmux-session-manager] session not attachable for pane spawn", {
|
||||
this.deps.log("[tmux-session-manager] session not attachable for pane spawn", {
|
||||
sessionId,
|
||||
stage,
|
||||
status: sessionStatus,
|
||||
@@ -744,7 +748,7 @@ export class TmuxSessionManager {
|
||||
createTrackedSession({ sessionId, paneId: isolatedPaneId, description: title }),
|
||||
)
|
||||
this.pollingManager.startPolling()
|
||||
log("[tmux-session-manager] first subagent spawned in isolated window", {
|
||||
this.deps.log("[tmux-session-manager] first subagent spawned in isolated window", {
|
||||
sessionId,
|
||||
paneId: isolatedPaneId,
|
||||
})
|
||||
@@ -752,24 +756,24 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
if (this.isIsolated() && !this.isolatedWindowPaneId) {
|
||||
log("[tmux-session-manager] isolated container failed, deferring session for retry", { sessionId })
|
||||
this.deps.log("[tmux-session-manager] isolated container failed, deferring session for retry", { sessionId })
|
||||
this.enqueueDeferredSession(sessionId, title, true)
|
||||
return
|
||||
}
|
||||
const sourcePaneId = this.getEffectiveSourcePaneId()
|
||||
if (!sourcePaneId) {
|
||||
log("[tmux-session-manager] no effective source pane id")
|
||||
this.deps.log("[tmux-session-manager] no effective source pane id")
|
||||
return
|
||||
}
|
||||
|
||||
const state = await this.deps.queryWindowState(sourcePaneId)
|
||||
if (!state) {
|
||||
log("[tmux-session-manager] failed to query window state, deferring session")
|
||||
this.deps.log("[tmux-session-manager] failed to query window state, deferring session")
|
||||
this.enqueueDeferredSession(sessionId, title)
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] window state queried", {
|
||||
this.deps.log("[tmux-session-manager] window state queried", {
|
||||
windowWidth: state.windowWidth,
|
||||
mainPane: state.mainPane?.paneId,
|
||||
agentPaneCount: state.agentPanes.length,
|
||||
@@ -784,7 +788,7 @@ export class TmuxSessionManager {
|
||||
this.getSessionMappings(),
|
||||
)
|
||||
|
||||
log("[tmux-session-manager] spawn decision", {
|
||||
this.deps.log("[tmux-session-manager] spawn decision", {
|
||||
canSpawn: decision.canSpawn,
|
||||
reason: decision.reason,
|
||||
actionCount: decision.actions.length,
|
||||
@@ -802,7 +806,7 @@ export class TmuxSessionManager {
|
||||
})
|
||||
|
||||
if (!decision.canSpawn) {
|
||||
log("[tmux-session-manager] cannot spawn", { reason: decision.reason })
|
||||
this.deps.log("[tmux-session-manager] cannot spawn", { reason: decision.reason })
|
||||
this.enqueueDeferredSession(sessionId, title)
|
||||
return
|
||||
}
|
||||
@@ -821,13 +825,13 @@ export class TmuxSessionManager {
|
||||
for (const { action, result: actionResult } of result.results) {
|
||||
if (action.type === "close" && actionResult.success) {
|
||||
this.sessions.delete(action.sessionId)
|
||||
log("[tmux-session-manager] removed closed session from cache", {
|
||||
this.deps.log("[tmux-session-manager] removed closed session from cache", {
|
||||
sessionId: action.sessionId,
|
||||
})
|
||||
}
|
||||
if (action.type === "replace" && actionResult.success) {
|
||||
this.sessions.delete(action.oldSessionId)
|
||||
log("[tmux-session-manager] removed replaced session from cache", {
|
||||
this.deps.log("[tmux-session-manager] removed replaced session from cache", {
|
||||
oldSessionId: action.oldSessionId,
|
||||
newSessionId: action.newSessionId,
|
||||
})
|
||||
@@ -844,7 +848,7 @@ export class TmuxSessionManager {
|
||||
}),
|
||||
)
|
||||
this.clearFailedReadinessSession(sessionId)
|
||||
log("[tmux-session-manager] pane spawned and tracked", {
|
||||
this.deps.log("[tmux-session-manager] pane spawned and tracked", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
})
|
||||
@@ -852,7 +856,7 @@ export class TmuxSessionManager {
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] spawn failed", {
|
||||
this.deps.log("[tmux-session-manager] spawn failed", {
|
||||
success: result.success,
|
||||
results: result.results.map((resultEntry) => ({
|
||||
type: resultEntry.action.type,
|
||||
@@ -861,7 +865,7 @@ export class TmuxSessionManager {
|
||||
})),
|
||||
})
|
||||
|
||||
log("[tmux-session-manager] re-queueing deferred session after spawn failure", {
|
||||
this.deps.log("[tmux-session-manager] re-queueing deferred session after spawn failure", {
|
||||
sessionId,
|
||||
})
|
||||
this.enqueueDeferredSession(sessionId, title)
|
||||
@@ -906,7 +910,7 @@ export class TmuxSessionManager {
|
||||
try {
|
||||
const sessionStatus = await this.getSessionStatusType(sessionId)
|
||||
if (!isAttachableSessionStatus(sessionStatus)) {
|
||||
log("[tmux-session-manager] session.idle retry skipped because session is not attachable", {
|
||||
this.deps.log("[tmux-session-manager] session.idle retry skipped because session is not attachable", {
|
||||
sessionId,
|
||||
status: sessionStatus,
|
||||
})
|
||||
@@ -954,7 +958,7 @@ export class TmuxSessionManager {
|
||||
if (Date.now() - deferred.queuedAt.getTime() > DEFERRED_SESSION_TTL_MS) {
|
||||
this.deferredQueue.shift()
|
||||
this.deferredSessions.delete(sessionId)
|
||||
log("[tmux-session-manager] deferred session expired", {
|
||||
this.deps.log("[tmux-session-manager] deferred session expired", {
|
||||
sessionId,
|
||||
queuedAt: deferred.queuedAt.toISOString(),
|
||||
ttlMs: DEFERRED_SESSION_TTL_MS,
|
||||
@@ -988,7 +992,7 @@ export class TmuxSessionManager {
|
||||
)
|
||||
this.removeDeferredSession(sessionId)
|
||||
this.pollingManager.startPolling()
|
||||
log("[tmux-session-manager] deferred session attached in isolated window", {
|
||||
this.deps.log("[tmux-session-manager] deferred session attached in isolated window", {
|
||||
sessionId,
|
||||
paneId: isolatedPaneId,
|
||||
})
|
||||
@@ -1002,11 +1006,11 @@ export class TmuxSessionManager {
|
||||
const state = await this.deps.queryWindowState(effectiveSourcePaneId)
|
||||
if (!state) {
|
||||
this.nullStateCount += 1
|
||||
log("[tmux-session-manager] deferred attach window state is null", {
|
||||
this.deps.log("[tmux-session-manager] deferred attach window state is null", {
|
||||
nullStateCount: this.nullStateCount,
|
||||
})
|
||||
if (this.nullStateCount >= 3) {
|
||||
log("[tmux-session-manager] stopping deferred attach loop after consecutive null states", {
|
||||
this.deps.log("[tmux-session-manager] stopping deferred attach loop after consecutive null states", {
|
||||
nullStateCount: this.nullStateCount,
|
||||
})
|
||||
this.stopDeferredAttachLoop()
|
||||
@@ -1024,7 +1028,7 @@ export class TmuxSessionManager {
|
||||
)
|
||||
|
||||
if (!decision.canSpawn || decision.actions.length === 0) {
|
||||
log("[tmux-session-manager] deferred session still waiting for capacity", {
|
||||
this.deps.log("[tmux-session-manager] deferred session still waiting for capacity", {
|
||||
sessionId,
|
||||
reason: decision.reason,
|
||||
})
|
||||
@@ -1049,7 +1053,7 @@ export class TmuxSessionManager {
|
||||
})
|
||||
|
||||
if (!result.success || !result.spawnedPaneId) {
|
||||
log("[tmux-session-manager] deferred session attach failed", {
|
||||
this.deps.log("[tmux-session-manager] deferred session attach failed", {
|
||||
sessionId,
|
||||
results: result.results.map((r) => ({
|
||||
type: r.action.type,
|
||||
@@ -1070,7 +1074,7 @@ export class TmuxSessionManager {
|
||||
)
|
||||
this.removeDeferredSession(sessionId)
|
||||
this.pollingManager.startPolling()
|
||||
log("[tmux-session-manager] deferred session attached", {
|
||||
this.deps.log("[tmux-session-manager] deferred session attached", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
})
|
||||
@@ -1081,7 +1085,7 @@ export class TmuxSessionManager {
|
||||
|
||||
async onSessionCreated(event: SessionCreatedEvent): Promise<void> {
|
||||
const enabled = this.isEnabled()
|
||||
log("[tmux-session-manager] onSessionCreated called", {
|
||||
this.deps.log("[tmux-session-manager] onSessionCreated called", {
|
||||
enabled,
|
||||
tmuxConfigEnabled: this.tmuxConfig.enabled,
|
||||
isInsideTmux: this.deps.isInsideTmux(),
|
||||
@@ -1100,7 +1104,7 @@ export class TmuxSessionManager {
|
||||
const title = info.title ?? "Subagent"
|
||||
|
||||
if (!this.sourcePaneId) {
|
||||
log("[tmux-session-manager] no source pane id")
|
||||
this.deps.log("[tmux-session-manager] no source pane id")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1133,13 +1137,13 @@ export class TmuxSessionManager {
|
||||
private async enqueueSpawn(run: () => Promise<void>): Promise<void> {
|
||||
this.spawnQueue = this.spawnQueue
|
||||
.catch((error) => {
|
||||
log("[tmux-session-manager] recovering spawn queue after previous failure", {
|
||||
this.deps.log("[tmux-session-manager] recovering spawn queue after previous failure", {
|
||||
error: String(error),
|
||||
})
|
||||
})
|
||||
.then(run)
|
||||
.catch((err) => {
|
||||
log("[tmux-session-manager] spawn queue task failed", {
|
||||
this.deps.log("[tmux-session-manager] spawn queue task failed", {
|
||||
error: String(err),
|
||||
})
|
||||
})
|
||||
@@ -1158,7 +1162,7 @@ export class TmuxSessionManager {
|
||||
const tracked = this.sessions.get(event.sessionID)
|
||||
if (!tracked) return
|
||||
|
||||
log("[tmux-session-manager] onSessionDeleted", { sessionId: event.sessionID })
|
||||
this.deps.log("[tmux-session-manager] onSessionDeleted", { sessionId: event.sessionID })
|
||||
|
||||
const state = await this.queryWindowStateSafely()
|
||||
if (!state) {
|
||||
@@ -1193,7 +1197,7 @@ export class TmuxSessionManager {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] failed to close pane for deleted session", {
|
||||
this.deps.log("[tmux-session-manager] failed to close pane for deleted session", {
|
||||
sessionId: event.sessionID,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -1218,7 +1222,7 @@ export class TmuxSessionManager {
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] closing session pane", {
|
||||
this.deps.log("[tmux-session-manager] closing session pane", {
|
||||
sessionId,
|
||||
paneId: tracked.paneId,
|
||||
})
|
||||
@@ -1240,7 +1244,7 @@ export class TmuxSessionManager {
|
||||
return false
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] skipping tmux respawn because polling already closed the session", {
|
||||
this.deps.log("[tmux-session-manager] skipping tmux respawn because polling already closed the session", {
|
||||
sessionId,
|
||||
source,
|
||||
})
|
||||
@@ -1256,7 +1260,7 @@ export class TmuxSessionManager {
|
||||
}
|
||||
|
||||
void this.retryFailedReadinessSession(sessionId).catch((error) => {
|
||||
log("[tmux-session-manager] session.idle retry failed", {
|
||||
this.deps.log("[tmux-session-manager] session.idle retry failed", {
|
||||
sessionId,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -1279,14 +1283,14 @@ export class TmuxSessionManager {
|
||||
this.pollingManager.stopPolling()
|
||||
|
||||
if (this.sessions.size > 0) {
|
||||
log("[tmux-session-manager] closing all panes", { count: this.sessions.size })
|
||||
this.deps.log("[tmux-session-manager] closing all panes", { count: this.sessions.size })
|
||||
|
||||
const sessionIds = Array.from(this.sessions.keys())
|
||||
for (const sessionId of sessionIds) {
|
||||
try {
|
||||
await this.closeSessionById(sessionId)
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] cleanup error for pane", {
|
||||
this.deps.log("[tmux-session-manager] cleanup error for pane", {
|
||||
sessionId,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -1303,12 +1307,12 @@ export class TmuxSessionManager {
|
||||
const isolatedSessionName = getIsolatedSessionName()
|
||||
try {
|
||||
const killed = await killTmuxSessionIfExists(isolatedSessionName)
|
||||
log("[tmux-session-manager] isolated session teardown", {
|
||||
this.deps.log("[tmux-session-manager] isolated session teardown", {
|
||||
session: isolatedSessionName,
|
||||
killed,
|
||||
})
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] isolated session teardown failed", {
|
||||
this.deps.log("[tmux-session-manager] isolated session teardown failed", {
|
||||
session: isolatedSessionName,
|
||||
error: String(error),
|
||||
})
|
||||
@@ -1318,7 +1322,7 @@ export class TmuxSessionManager {
|
||||
this.staleSweepCompleted = false
|
||||
this.staleSweepInProgress = false
|
||||
|
||||
log("[tmux-session-manager] cleanup complete")
|
||||
this.deps.log("[tmux-session-manager] cleanup complete")
|
||||
}
|
||||
|
||||
private async sweepStaleIsolatedSessionsOnce(): Promise<void> {
|
||||
@@ -1333,11 +1337,11 @@ export class TmuxSessionManager {
|
||||
try {
|
||||
const killed = await sweepStaleOmoAgentSessions()
|
||||
if (killed > 0) {
|
||||
log("[tmux-session-manager] stale isolated sessions swept", { killed })
|
||||
this.deps.log("[tmux-session-manager] stale isolated sessions swept", { killed })
|
||||
}
|
||||
this.staleSweepCompleted = true
|
||||
} catch (error) {
|
||||
log("[tmux-session-manager] stale sweep failed", {
|
||||
this.deps.log("[tmux-session-manager] stale sweep failed", {
|
||||
error: String(error),
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
|
||||
import type { TmuxCommandResult } from "../../shared/tmux"
|
||||
|
||||
const paneStateQuerierSpecifier = import.meta.resolve("./pane-state-querier")
|
||||
const loggerSpecifier = import.meta.resolve("../../shared")
|
||||
const runnerSpecifier = import.meta.resolve("../../shared/tmux")
|
||||
const tmuxPathResolverSpecifier = import.meta.resolve("../../tools/interactive-bash/tmux-path-resolver")
|
||||
import { queryWindowStateWithDeps } from "./pane-state-querier"
|
||||
|
||||
const runTmuxCommandMock = mock(async (): Promise<TmuxCommandResult> => ({
|
||||
success: true,
|
||||
@@ -14,25 +10,12 @@ const runTmuxCommandMock = mock(async (): Promise<TmuxCommandResult> => ({
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
}))
|
||||
const getTmuxPathMock = mock(async (): Promise<string | undefined> => "sh")
|
||||
const getTmuxPathMock = mock(async (): Promise<string | null> => "sh")
|
||||
const logMock = mock(() => undefined)
|
||||
|
||||
async function loadQueryWindowState(): Promise<typeof import("./pane-state-querier").queryWindowState> {
|
||||
const module = await import(`${paneStateQuerierSpecifier}?test=${crypto.randomUUID()}`)
|
||||
return module.queryWindowState
|
||||
}
|
||||
|
||||
function registerModuleMocks(): void {
|
||||
mock.module(loggerSpecifier, () => ({ log: logMock }))
|
||||
mock.module(runnerSpecifier, () => ({ runTmuxCommand: runTmuxCommandMock }))
|
||||
mock.module(tmuxPathResolverSpecifier, () => ({ getTmuxPath: getTmuxPathMock }))
|
||||
}
|
||||
|
||||
describe("queryWindowState runner integration", () => {
|
||||
beforeEach(() => {
|
||||
mock.restore()
|
||||
registerModuleMocks()
|
||||
runTmuxCommandMock.mockClear()
|
||||
beforeEach(() => {
|
||||
runTmuxCommandMock.mockClear()
|
||||
getTmuxPathMock.mockClear()
|
||||
logMock.mockClear()
|
||||
|
||||
@@ -43,15 +26,16 @@ describe("queryWindowState runner integration", () => {
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
})
|
||||
getTmuxPathMock.mockResolvedValue("sh")
|
||||
})
|
||||
getTmuxPathMock.mockResolvedValue("sh")
|
||||
})
|
||||
|
||||
it("#given source pane id #when queryWindowState called #then delegates list-panes to shared runner", async () => {
|
||||
// given
|
||||
const queryWindowState = await loadQueryWindowState()
|
||||
|
||||
// when
|
||||
const result = await queryWindowState("%0")
|
||||
const result = await queryWindowStateWithDeps("%0", {
|
||||
getTmuxPath: getTmuxPathMock,
|
||||
runTmuxCommand: runTmuxCommandMock,
|
||||
log: logMock,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result).not.toBeNull()
|
||||
|
||||
@@ -2,13 +2,19 @@ import type { WindowState, TmuxPaneInfo } from "./types"
|
||||
import { parsePaneStateOutput } from "./pane-state-parser"
|
||||
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
|
||||
import { log } from "../../shared"
|
||||
import type { TmuxCommandResult } from "../../shared/tmux"
|
||||
|
||||
export async function queryWindowState(sourcePaneId: string): Promise<WindowState | null> {
|
||||
const tmux = await getTmuxPath()
|
||||
type QueryWindowStateDeps = {
|
||||
getTmuxPath: typeof getTmuxPath
|
||||
runTmuxCommand: (tmuxPath: string, args: string[]) => Promise<TmuxCommandResult>
|
||||
log: typeof log
|
||||
}
|
||||
|
||||
export async function queryWindowStateWithDeps(sourcePaneId: string, deps: QueryWindowStateDeps): Promise<WindowState | null> {
|
||||
const tmux = await deps.getTmuxPath()
|
||||
if (!tmux) return null
|
||||
const { runTmuxCommand } = await import("../../shared/tmux")
|
||||
|
||||
const result = await runTmuxCommand(tmux, [
|
||||
const result = await deps.runTmuxCommand(tmux, [
|
||||
"list-panes",
|
||||
"-t",
|
||||
sourcePaneId,
|
||||
@@ -17,13 +23,13 @@ export async function queryWindowState(sourcePaneId: string): Promise<WindowStat
|
||||
])
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
log("[pane-state-querier] list-panes failed", { exitCode: result.exitCode })
|
||||
deps.log("[pane-state-querier] list-panes failed", { exitCode: result.exitCode })
|
||||
return null
|
||||
}
|
||||
|
||||
const parsedPaneState = parsePaneStateOutput(result.output)
|
||||
if (!parsedPaneState) {
|
||||
log("[pane-state-querier] failed to parse pane state output", {
|
||||
deps.log("[pane-state-querier] failed to parse pane state output", {
|
||||
sourcePaneId,
|
||||
})
|
||||
return null
|
||||
@@ -49,7 +55,7 @@ export async function queryWindowState(sourcePaneId: string): Promise<WindowStat
|
||||
return pane.paneId === sourcePaneId ? pane : selected
|
||||
}, null)
|
||||
if (!mainPane) {
|
||||
log("[pane-state-querier] CRITICAL: failed to determine main pane", {
|
||||
deps.log("[pane-state-querier] CRITICAL: failed to determine main pane", {
|
||||
sourcePaneId,
|
||||
availablePanes: panes.map((p) => p.paneId),
|
||||
})
|
||||
@@ -58,7 +64,7 @@ export async function queryWindowState(sourcePaneId: string): Promise<WindowStat
|
||||
|
||||
const agentPanes = panes.filter((p) => p.paneId !== mainPane.paneId)
|
||||
|
||||
log("[pane-state-querier] window state", {
|
||||
deps.log("[pane-state-querier] window state", {
|
||||
windowWidth,
|
||||
windowHeight,
|
||||
mainPane: mainPane.paneId,
|
||||
@@ -67,3 +73,8 @@ export async function queryWindowState(sourcePaneId: string): Promise<WindowStat
|
||||
|
||||
return { windowWidth, windowHeight, mainPane, agentPanes }
|
||||
}
|
||||
|
||||
export async function queryWindowState(sourcePaneId: string): Promise<WindowState | null> {
|
||||
const { runTmuxCommand } = await import("../../shared/tmux")
|
||||
return queryWindowStateWithDeps(sourcePaneId, { getTmuxPath, runTmuxCommand, log })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user