@@ -1341,6 +1341,59 @@ func sanitizeNudgeMessage(msg string) string {
13411341 return b .String ()
13421342}
13431343
1344+ // isInRewindMode checks if a tmux target is displaying Claude Code's Rewind
1345+ // conversation history browser. When Rewind is active, the session ignores
1346+ // typed text and only responds to Enter (accept rewind) or Escape (cancel).
1347+ // This can happen when a stray or deliberate Escape keystroke combines with
1348+ // a previous Escape to form the double-Escape sequence that activates Rewind.
1349+ //
1350+ // Detection is based on pane content analysis. Returns false on any error
1351+ // (defensive — don't block nudge delivery on detection failure).
1352+ func (t * Tmux ) isInRewindMode (target string ) bool {
1353+ content , err := t .CapturePane (target , 15 )
1354+ if err != nil {
1355+ return false
1356+ }
1357+ return containsRewindIndicators (content )
1358+ }
1359+
1360+ // containsRewindIndicators checks pane content for Claude Code Rewind menu
1361+ // patterns. The Rewind UI takes over the terminal and shows distinctive
1362+ // action prompts (Enter to act, Esc to cancel/exit). We require multiple
1363+ // co-occurring indicators to avoid false positives from conversation text.
1364+ func containsRewindIndicators (content string ) bool {
1365+ lower := strings .ToLower (content )
1366+
1367+ // Primary: "rewind" appears alongside both Enter and Esc action prompts.
1368+ if strings .Contains (lower , "rewind" ) {
1369+ if strings .Contains (lower , "enter" ) && strings .Contains (lower , "esc" ) {
1370+ return true
1371+ }
1372+ }
1373+
1374+ // Secondary: specific action prompt pairs characteristic of the Rewind UI.
1375+ rewindActionPairs := [][2 ]string {
1376+ {"enter to continue" , "esc to exit" },
1377+ {"enter to accept" , "esc to cancel" },
1378+ {"enter to select" , "esc to go back" },
1379+ {"enter to select" , "esc to cancel" },
1380+ }
1381+ for _ , pair := range rewindActionPairs {
1382+ if strings .Contains (lower , pair [0 ]) && strings .Contains (lower , pair [1 ]) {
1383+ return true
1384+ }
1385+ }
1386+
1387+ return false
1388+ }
1389+
1390+ // dismissRewindMode sends Escape to cancel Claude Code's Rewind menu,
1391+ // then waits briefly for the UI to return to normal.
1392+ func (t * Tmux ) dismissRewindMode (target string ) {
1393+ _ , _ = t .run ("send-keys" , "-t" , target , "Escape" )
1394+ time .Sleep (300 * time .Millisecond )
1395+ }
1396+
13441397// sendMessageToTarget sends a sanitized message to a tmux target. For small
13451398// messages (< sendKeysChunkSize), uses send-keys -l. For larger messages,
13461399// sends in chunks with delays to avoid overwhelming the TTY input buffer.
@@ -1493,6 +1546,14 @@ func (t *Tmux) NudgeSessionWithOpts(session, message string, opts NudgeOpts) err
14931546 target = agentPane
14941547 }
14951548
1549+ // 0. Pre-delivery: dismiss Rewind menu if the session is stuck in it.
1550+ // A previous nudge or user action may have triggered Claude Code's
1551+ // double-Escape Rewind UI, which captures all input. Dismiss it first
1552+ // so the nudge can be delivered normally. (GH#gt-8el)
1553+ if t .isInRewindMode (target ) {
1554+ t .dismissRewindMode (target )
1555+ }
1556+
14961557 // 1. Exit copy/scroll mode if active — copy mode intercepts input,
14971558 // preventing delivery to the underlying process.
14981559 if inMode , _ := t .run ("display-message" , "-p" , "-t" , target , "#{pane_in_mode}" ); strings .TrimSpace (inMode ) == "1" {
@@ -1522,6 +1583,19 @@ func (t *Tmux) NudgeSessionWithOpts(session, message string, opts NudgeOpts) err
15221583 // Without this, ESC+Enter within 500ms becomes M-Enter (meta-return) which
15231584 // does NOT submit the line.
15241585 time .Sleep (600 * time .Millisecond )
1586+
1587+ // 6.5. Post-Escape: check if our Escape triggered Rewind mode.
1588+ // This happens when a previous Escape was still in the input buffer,
1589+ // combining with ours to form the double-Escape that activates Rewind.
1590+ // If triggered, dismiss Rewind and re-send the message (Rewind
1591+ // consumed the original input). Skip the second Escape to avoid
1592+ // re-triggering. (GH#gt-8el)
1593+ if t .isInRewindMode (target ) {
1594+ t .dismissRewindMode (target )
1595+ // Re-send message text — Rewind consumed the original input.
1596+ _ = t .sendMessageToTarget (target , sanitized , constants .NudgeReadyTimeout )
1597+ time .Sleep (500 * time .Millisecond )
1598+ }
15251599 }
15261600
15271601 // 7. Send Enter with retry (critical for message submission)
@@ -1553,6 +1627,11 @@ func (t *Tmux) NudgePane(pane, message string) error {
15531627 }
15541628 defer releaseNudgeLock (pane )
15551629
1630+ // 0. Pre-delivery: dismiss Rewind menu if active. (GH#gt-8el)
1631+ if t .isInRewindMode (pane ) {
1632+ t .dismissRewindMode (pane )
1633+ }
1634+
15561635 // 1. Exit copy/scroll mode if active — copy mode intercepts input,
15571636 // preventing delivery to the underlying process.
15581637 if inMode , _ := t .run ("display-message" , "-p" , "-t" , pane , "#{pane_in_mode}" ); strings .TrimSpace (inMode ) == "1" {
@@ -1579,6 +1658,13 @@ func (t *Tmux) NudgePane(pane, message string) error {
15791658 // 6. Wait 600ms — must exceed bash readline's keyseq-timeout (500ms default)
15801659 time .Sleep (600 * time .Millisecond )
15811660
1661+ // 6.5. Post-Escape: check if our Escape triggered Rewind mode. (GH#gt-8el)
1662+ if t .isInRewindMode (pane ) {
1663+ t .dismissRewindMode (pane )
1664+ _ = t .sendMessageToTarget (pane , sanitized , constants .NudgeReadyTimeout )
1665+ time .Sleep (500 * time .Millisecond )
1666+ }
1667+
15821668 // 7. Send Enter with retry (critical for message submission)
15831669 var lastErr error
15841670 for attempt := 0 ; attempt < 3 ; attempt ++ {
0 commit comments