Files
oh-my-opencode/src/hooks/interactive-bash-session/hook.ts
T
YeonGyu-Kim 4785767a0c refactor(interactive-bash): reuse tmux parser
Route interactive bash tracking through the existing tmux parser so session name narrowing is shared and type-safe.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-15 16:31:13 +09:00

123 lines
3.5 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin";
import { saveInteractiveBashSessionState, clearInteractiveBashSessionState } from "./storage";
import { buildSessionReminderMessage } from "./constants";
import type { InteractiveBashSessionState } from "./types";
import { parseTmuxCommand } from "./tmux-command-parser";
import { getOrCreateState, isOmoSession, killAllTrackedSessions } from "./state-manager";
import { subagentSessions } from "../../features/claude-code-session-state";
import { resolveSessionEventID } from "../../shared/event-session-id";
interface ToolExecuteInput {
tool: string;
sessionID: string;
callID: string;
args?: Record<string, unknown>;
}
interface ToolExecuteOutput {
title: string;
output: string;
metadata: unknown;
}
interface EventInput {
event: {
type: string;
properties?: unknown;
};
}
export function createInteractiveBashSessionHook(ctx: PluginInput) {
const sessionStates = new Map<string, InteractiveBashSessionState>();
function getOrCreateStateLocal(sessionID: string): InteractiveBashSessionState {
return getOrCreateState(sessionID, sessionStates);
}
async function killAllTrackedSessionsLocal(
state: InteractiveBashSessionState,
): Promise<void> {
await killAllTrackedSessions(state);
for (const sessionId of subagentSessions) {
ctx.client.session.abort({ path: { id: sessionId } }).catch(() => {})
}
}
const toolExecuteAfter = async (
input: ToolExecuteInput,
output: ToolExecuteOutput,
) => {
const { tool, sessionID, args } = input;
const toolLower = tool.toLowerCase();
if (toolLower !== "interactive_bash") {
return;
}
if (typeof args?.tmux_command !== "string") {
return;
}
const tmuxCommand = args.tmux_command;
const { subCommand, sessionName } = parseTmuxCommand(tmuxCommand);
const state = getOrCreateStateLocal(sessionID);
let stateChanged = false;
const toolOutput = output?.output ?? ""
if (toolOutput.startsWith("Error:")) {
return
}
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) {
const reminder = buildSessionReminderMessage(
Array.from(state.tmuxSessions),
);
if (reminder) {
output.output += reminder;
}
}
};
const eventHandler = async ({ event }: EventInput) => {
const props = event.properties as Record<string, unknown> | undefined;
if (event.type === "session.deleted") {
const sessionID = resolveSessionEventID(props);
if (sessionID) {
const state = getOrCreateStateLocal(sessionID);
await killAllTrackedSessionsLocal(state);
sessionStates.delete(sessionID);
clearInteractiveBashSessionState(sessionID);
}
}
};
return {
"tool.execute.after": toolExecuteAfter,
event: eventHandler,
};
}