refactor(hooks): improve interactive bash session tracking and command parsing

- Replace regex-based session extraction with quote-aware tokenizer
- Add proper tmux global options handling (-L, -S, -f, -c, -T)
- Add normalizeSessionName to strip :window and .pane suffixes
- Add findSubcommand for proper subcommand detection
- Add early error output return to avoid false state tracking
- Fix tool-output-truncator to exclude grep/Grep from generic truncation
- Fix todo-continuation-enforcer to clear reminded state on assistant response
- Add proper parallel stdout/stderr reading in interactive_bash tool
- Improve error handling with proper exit code checking

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-15 19:02:31 +09:00
parent c2e96f1ffe
commit 03a450131d
4 changed files with 207 additions and 26 deletions

View File

@@ -1,6 +1,51 @@
import { tool } from "@opencode-ai/plugin/tool"
import { DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants"
/**
* Quote-aware command tokenizer with escape handling
* Handles single/double quotes and backslash escapes without external dependencies
*/
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
}
export const interactive_bash = tool({
description: INTERACTIVE_BASH_DESCRIPTION,
args: {
@@ -8,7 +53,7 @@ export const interactive_bash = tool({
},
execute: async (args) => {
try {
const parts = args.tmux_command.split(/\s+/).filter((p) => p.length > 0)
const parts = tokenizeCommand(args.tmux_command)
if (parts.length === 0) {
return "Error: Empty tmux command"
@@ -27,12 +72,20 @@ export const interactive_bash = tool({
proc.exited.then(() => clearTimeout(id))
})
const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise])
const stderr = await new Response(proc.stderr).text()
const exitCode = await proc.exited
// Read stdout and stderr in parallel to avoid race conditions
const [stdout, stderr, exitCode] = await Promise.race([
Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]),
timeoutPromise,
])
if (exitCode !== 0 && stderr.trim()) {
return `Error: ${stderr.trim()}`
// Check exitCode properly - return error even if stderr is empty
if (exitCode !== 0) {
const errorMsg = stderr.trim() || `Command failed with exit code ${exitCode}`
return `Error: ${errorMsg}`
}
return stdout || "(no output)"