diff --git a/src/hooks/interactive-bash-session/hook.ts b/src/hooks/interactive-bash-session/hook.ts index 86aa2f101..9e3398d28 100644 --- a/src/hooks/interactive-bash-session/hook.ts +++ b/src/hooks/interactive-bash-session/hook.ts @@ -2,7 +2,7 @@ import type { PluginInput } from "@opencode-ai/plugin"; import { saveInteractiveBashSessionState, clearInteractiveBashSessionState } from "./storage"; import { buildSessionReminderMessage } from "./constants"; import type { InteractiveBashSessionState } from "./types"; -import { tokenizeCommand, findSubcommand, extractSessionNameFromTokens } from "./parser"; +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"; @@ -60,8 +60,7 @@ export function createInteractiveBashSessionHook(ctx: PluginInput) { } const tmuxCommand = args.tmux_command; - const tokens = tokenizeCommand(tmuxCommand); - const subCommand = findSubcommand(tokens); + const { subCommand, sessionName } = parseTmuxCommand(tmuxCommand); const state = getOrCreateStateLocal(sessionID); let stateChanged = false; @@ -74,13 +73,11 @@ export function createInteractiveBashSessionHook(ctx: PluginInput) { const isKillSession = subCommand === "kill-session"; const isKillServer = subCommand === "kill-server"; - const sessionName = extractSessionNameFromTokens(tokens, subCommand); - if (isNewSession && isOmoSession(sessionName)) { - state.tmuxSessions.add(sessionName!); + state.tmuxSessions.add(sessionName); stateChanged = true; } else if (isKillSession && isOmoSession(sessionName)) { - state.tmuxSessions.delete(sessionName!); + state.tmuxSessions.delete(sessionName); stateChanged = true; } else if (isKillServer) { state.tmuxSessions.clear(); diff --git a/src/hooks/interactive-bash-session/parser.ts b/src/hooks/interactive-bash-session/parser.ts deleted file mode 100644 index 0002d9312..000000000 --- a/src/hooks/interactive-bash-session/parser.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Quote-aware command tokenizer with escape handling - * Handles single/double quotes and backslash escapes - */ -export function tokenizeCommand(cmd: string): string[] { - const tokens: string[] = [] - let current = "" - let inQuote = false - let quoteChar = "" - let escaped = false - - for (let i = 0; i < cmd.length; i++) { - const char = cmd[i] - - if (escaped) { - current += char - escaped = false - continue - } - - if (char === "\\") { - escaped = true - continue - } - - if ((char === "'" || char === '"') && !inQuote) { - inQuote = true - quoteChar = char - } else if (char === quoteChar && inQuote) { - inQuote = false - quoteChar = "" - } else if (char === " " && !inQuote) { - if (current) { - tokens.push(current) - current = "" - } - } else { - current += char - } - } - - if (current) tokens.push(current) - return tokens -} - -/** - * Normalize session name by stripping :window and .pane suffixes - * e.g., "omo-x:1" -> "omo-x", "omo-x:1.2" -> "omo-x" - */ -export function normalizeSessionName(name: string): string { - return name.split(":")[0].split(".")[0] -} - -export function findFlagValue(tokens: string[], flag: string): string | null { - for (let i = 0; i < tokens.length - 1; i++) { - if (tokens[i] === flag) return tokens[i + 1] - } - return null -} - -/** - * Extract session name from tokens, considering the subCommand - * For new-session: prioritize -s over -t - * For other commands: use -t - */ -export function extractSessionNameFromTokens(tokens: string[], subCommand: string): string | null { - if (subCommand === "new-session") { - const sFlag = findFlagValue(tokens, "-s") - if (sFlag) return normalizeSessionName(sFlag) - const tFlag = findFlagValue(tokens, "-t") - if (tFlag) return normalizeSessionName(tFlag) - } else { - const tFlag = findFlagValue(tokens, "-t") - if (tFlag) return normalizeSessionName(tFlag) - } - return null -} - -/** - * Find the tmux subcommand from tokens, skipping global options. - * tmux allows global options before the subcommand: - * e.g., `tmux -L socket-name new-session -s omo-x` - * Global options with args: -L, -S, -f, -c, -T - * Standalone flags: -C, -v, -V, etc. - * Special: -- (end of options marker) - */ -export function findSubcommand(tokens: string[]): string { - // Options that require an argument: -L, -S, -f, -c, -T - const globalOptionsWithArgs = new Set(["-L", "-S", "-f", "-c", "-T"]) - - let i = 0 - while (i < tokens.length) { - const token = tokens[i] - - // Handle end of options marker - if (token === "--") { - // Next token is the subcommand - return tokens[i + 1] ?? "" - } - - if (globalOptionsWithArgs.has(token)) { - // Skip the option and its argument - i += 2 - continue - } - - if (token.startsWith("-")) { - // Skip standalone flags like -C, -v, -V - i++ - continue - } - - // Found the subcommand - return token - } - - return "" -} diff --git a/src/hooks/interactive-bash-session/state-manager.ts b/src/hooks/interactive-bash-session/state-manager.ts index c3a286421..70f737d11 100644 --- a/src/hooks/interactive-bash-session/state-manager.ts +++ b/src/hooks/interactive-bash-session/state-manager.ts @@ -2,21 +2,25 @@ import type { InteractiveBashSessionState } from "./types"; import { loadInteractiveBashSessionState } from "./storage"; import { OMO_SESSION_PREFIX } from "./constants"; import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"; +import { log } from "../../shared/logger"; export function getOrCreateState(sessionID: string, sessionStates: Map): InteractiveBashSessionState { - if (!sessionStates.has(sessionID)) { - const persisted = loadInteractiveBashSessionState(sessionID); - const state: InteractiveBashSessionState = persisted ?? { - sessionID, - tmuxSessions: new Set(), - updatedAt: Date.now(), - }; - sessionStates.set(sessionID, state); + const existing = sessionStates.get(sessionID); + if (existing) { + return existing; } - return sessionStates.get(sessionID)!; + + const persisted = loadInteractiveBashSessionState(sessionID); + const state: InteractiveBashSessionState = persisted ?? { + sessionID, + tmuxSessions: new Set(), + updatedAt: Date.now(), + }; + sessionStates.set(sessionID, state); + return state; } -export function isOmoSession(sessionName: string | null): boolean { +export function isOmoSession(sessionName: string | null): sessionName is string { return sessionName !== null && sessionName.startsWith(OMO_SESSION_PREFIX); } @@ -30,6 +34,11 @@ export async function killAllTrackedSessions( stderr: "ignore", }); await proc.exited; - } catch {} + } catch (error) { + log("[interactive-bash-session] failed to kill tracked tmux session", { + error: error instanceof Error ? error.message : String(error), + sessionName, + }); + } } }