fix(tmux): wake Claude in detached sessions by triggering SIGWINCH (#976)
Add functions to wake Claude Code's event loop in detached tmux sessions: - IsSessionAttached: Check if session has attached clients - WakePane: Always trigger SIGWINCH via resize dance - WakePaneIfDetached: Smart wrapper that skips attached sessions When Claude runs in a detached tmux session, its TUI library may not process stdin until a terminal event occurs. Attaching triggers SIGWINCH which wakes the event loop. WakePane simulates that by resizing the pane down 1 row then back up. NudgeSession and NudgePane now call WakePaneIfDetached after sending Enter, covering all 22 nudge call sites in the codebase. Fixes: gt-6s75ln Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -721,9 +721,43 @@ func getSessionNudgeLock(session string) *sync.Mutex {
|
||||
return actual.(*sync.Mutex)
|
||||
}
|
||||
|
||||
// IsSessionAttached returns true if the session has any clients attached.
|
||||
func (t *Tmux) IsSessionAttached(target string) bool {
|
||||
attached, err := t.run("display-message", "-t", target, "-p", "#{session_attached}")
|
||||
return err == nil && attached == "1"
|
||||
}
|
||||
|
||||
// WakePane triggers a SIGWINCH in a pane by resizing it slightly then restoring.
|
||||
// This wakes up Claude Code's event loop by simulating a terminal resize.
|
||||
//
|
||||
// When Claude runs in a detached tmux session, its TUI library may not process
|
||||
// stdin until a terminal event occurs. Attaching triggers SIGWINCH which wakes
|
||||
// the event loop. This function simulates that by doing a resize dance.
|
||||
//
|
||||
// Note: This always performs the resize. Use WakePaneIfDetached to skip
|
||||
// attached sessions where the wake is unnecessary.
|
||||
func (t *Tmux) WakePane(target string) {
|
||||
// Resize pane down by 1 row, then up by 1 row
|
||||
// This triggers SIGWINCH without changing the final pane size
|
||||
_, _ = t.run("resize-pane", "-t", target, "-y", "-1")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
_, _ = t.run("resize-pane", "-t", target, "-y", "+1")
|
||||
}
|
||||
|
||||
// WakePaneIfDetached triggers a SIGWINCH only if the session is detached.
|
||||
// This avoids unnecessary latency on attached sessions where Claude is
|
||||
// already processing terminal events.
|
||||
func (t *Tmux) WakePaneIfDetached(target string) {
|
||||
if t.IsSessionAttached(target) {
|
||||
return
|
||||
}
|
||||
t.WakePane(target)
|
||||
}
|
||||
|
||||
// NudgeSession sends a message to a Claude Code session reliably.
|
||||
// This is the canonical way to send messages to Claude sessions.
|
||||
// Uses: literal mode + 500ms debounce + ESC (for vim mode) + separate Enter.
|
||||
// After sending, triggers SIGWINCH to wake Claude in detached sessions.
|
||||
// Verification is the Witness's job (AI), not this function.
|
||||
//
|
||||
// IMPORTANT: Nudges to the same session are serialized to prevent interleaving.
|
||||
@@ -759,6 +793,8 @@ func (t *Tmux) NudgeSession(session, message string) error {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
// 5. Wake the pane to trigger SIGWINCH for detached sessions
|
||||
t.WakePaneIfDetached(session)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to send Enter after 3 attempts: %w", lastErr)
|
||||
@@ -766,6 +802,7 @@ func (t *Tmux) NudgeSession(session, message string) error {
|
||||
|
||||
// NudgePane sends a message to a specific pane reliably.
|
||||
// Same pattern as NudgeSession but targets a pane ID (e.g., "%9") instead of session name.
|
||||
// After sending, triggers SIGWINCH to wake Claude in detached sessions.
|
||||
// Nudges to the same pane are serialized to prevent interleaving.
|
||||
func (t *Tmux) NudgePane(pane, message string) error {
|
||||
// Serialize nudges to this pane to prevent interleaving
|
||||
@@ -796,6 +833,8 @@ func (t *Tmux) NudgePane(pane, message string) error {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
// 5. Wake the pane to trigger SIGWINCH for detached sessions
|
||||
t.WakePaneIfDetached(pane)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to send Enter after 3 attempts: %w", lastErr)
|
||||
|
||||
Reference in New Issue
Block a user