Files
oh-my-openagent/src/hooks/interactive-bash-session/interactive-bash-session-tracker.ts

120 lines
3.7 KiB
TypeScript

import {
loadInteractiveBashSessionState,
saveInteractiveBashSessionState,
clearInteractiveBashSessionState,
} from "./storage";
import { OMO_SESSION_PREFIX, buildSessionReminderMessage } from "./constants";
import type { InteractiveBashSessionState } from "./types";
import { subagentSessions } from "../../features/claude-code-session-state";
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide";
type AbortSession = (args: { path: { id: string } }) => Promise<unknown>
function isOmoSession(sessionName: string | null): sessionName is string {
return sessionName !== null && sessionName.startsWith(OMO_SESSION_PREFIX)
}
async function killAllTrackedSessions(
abortSession: AbortSession,
state: InteractiveBashSessionState,
): Promise<void> {
for (const sessionName of state.tmuxSessions) {
try {
const proc = spawnWithWindowsHide(["tmux", "kill-session", "-t", sessionName], {
stdout: "ignore",
stderr: "ignore",
})
await proc.exited
} catch {
// best-effort cleanup
}
}
for (const sessionId of subagentSessions) {
abortSession({ path: { id: sessionId } }).catch(() => {})
}
}
export function createInteractiveBashSessionTracker(options: {
abortSession: AbortSession
}): {
getOrCreateState: (sessionID: string) => InteractiveBashSessionState
handleSessionDeleted: (sessionID: string) => Promise<void>
handleTmuxCommand: (input: {
sessionID: string
subCommand: string
sessionName: string | null
toolOutput: string
}) => { reminderToAppend: string | null }
} {
const { abortSession } = options
const sessionStates = new Map<string, InteractiveBashSessionState>()
function getOrCreateState(sessionID: string): InteractiveBashSessionState {
const existing = sessionStates.get(sessionID)
if (existing) return existing
const persisted = loadInteractiveBashSessionState(sessionID)
const state: InteractiveBashSessionState = persisted ?? {
sessionID,
tmuxSessions: new Set<string>(),
updatedAt: Date.now(),
}
sessionStates.set(sessionID, state)
return state
}
async function handleSessionDeleted(sessionID: string): Promise<void> {
const state = getOrCreateState(sessionID)
await killAllTrackedSessions(abortSession, state)
sessionStates.delete(sessionID)
clearInteractiveBashSessionState(sessionID)
}
function handleTmuxCommand(input: {
sessionID: string
subCommand: string
sessionName: string | null
toolOutput: string
}): { reminderToAppend: string | null } {
const { sessionID, subCommand, sessionName, toolOutput } = input
const state = getOrCreateState(sessionID)
let stateChanged = false
if (toolOutput.startsWith("Error:")) {
return { reminderToAppend: null }
}
const isNewSession = subCommand === "new-session"
const isKillSession = subCommand === "kill-session"
const isKillServer = subCommand === "kill-server"
if (isNewSession && isOmoSession(sessionName)) {
state.tmuxSessions.add(sessionName)
stateChanged = true
} else if (isKillSession && isOmoSession(sessionName)) {
state.tmuxSessions.delete(sessionName)
stateChanged = true
} else if (isKillServer) {
state.tmuxSessions.clear()
stateChanged = true
}
if (stateChanged) {
state.updatedAt = Date.now()
saveInteractiveBashSessionState(state)
}
const isSessionOperation = isNewSession || isKillSession || isKillServer
if (!isSessionOperation) {
return { reminderToAppend: null }
}
const reminder = buildSessionReminderMessage(Array.from(state.tmuxSessions))
return { reminderToAppend: reminder || null }
}
return { getOrCreateState, handleSessionDeleted, handleTmuxCommand }
}