fix(agent-hooks): skip stdin read when it is a tty

The agent-hook emitter reads its JSON payload from stdin until EOF,
which assumes the spawner pipes a payload and closes the stream (as
Claude Code's hooks do). OpenCode's plugin runner spawns the emitter
with stdin inherited from the pane's terminal instead: a tty never
reaches EOF, so the read blocked forever, hanging the plugin's async
init and leaving OpenCode's TUI blank on startup — while also
swallowing the user's keystrokes.

Skip the stdin read when stdin is a terminal; a bare event without
session_id/message is already a supported payload.

Fixes #88
This commit is contained in:
l0ng-ai
2026-07-15 21:48:45 +08:00
parent 846032183d
commit ffcb990d33
+8 -3
View File
@@ -15,7 +15,7 @@
//! shell tty7 spawns), so hooks installed globally stay silent when an agent
//! runs in another terminal.
use std::io::Read as _;
use std::io::{IsTerminal as _, Read as _};
use std::path::{Path, PathBuf};
use crate::core::cli_agent::AGENT_EVENT_SENTINEL;
@@ -40,9 +40,14 @@ pub fn run_agent_hook(agent: &str, event: &str) {
}
// Hook payload: the agent writes JSON ({"session_id": …, "message": …, …})
// and closes stdin. Absent/malformed input still emits the bare event —
// the state machine works without ids or messages.
// the state machine works without ids or messages. A tty stdin means the
// spawner inherited the pane's terminal instead of piping a payload (e.g.
// OpenCode's plugin runner, issue #88); reading it would block forever on
// an EOF that never comes and swallow the user's keystrokes, so skip it.
let mut input = String::new();
let _ = std::io::stdin().take(MAX_STDIN).read_to_string(&mut input);
if !std::io::stdin().is_terminal() {
let _ = std::io::stdin().take(MAX_STDIN).read_to_string(&mut input);
}
let Some(event) = effective_event(agent, event, &input) else {
return;
};