Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,16 +845,16 @@ func listen() error {
target := tmuxTargetByID(windowID, tmuxName)
// Send arrow down keys to select option, then Enter
for i := 0; i < optionIndex; i++ {
exec.Command(tmuxPath, "send-keys", "-t", target, "Down").Run()
tmuxSendKeys(target, "Down")
time.Sleep(50 * time.Millisecond)
}
exec.Command(tmuxPath, "send-keys", "-t", target, "Enter").Run()
tmuxSendKeys(target, "Enter")
listenLog("[callback] Selected option %d for %s (question %d/%d)", optionIndex, sessionName, questionIndex+1, totalQuestions)

// After the last question, send Enter to confirm "Submit answers"
if totalQuestions > 0 && questionIndex == totalQuestions-1 {
time.Sleep(300 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", target, "Enter").Run()
tmuxSendKeys(target, "Enter")
listenLog("[callback] Auto-submitted answers for %s", sessionName)
}
}
Expand Down Expand Up @@ -1450,7 +1450,7 @@ func handleAuth(config *Config, chatID, threadID int64) {
}

time.Sleep(500 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, claudePath+" --dangerously-skip-permissions", "C-m").Run()
tmuxSendKeys(authTmuxSession, claudePath+" --dangerously-skip-permissions", "C-m")

var oauthURL string
for i := 0; i < 30; i++ {
Expand Down Expand Up @@ -1503,24 +1503,24 @@ func handleAuthCode(config *Config, chatID, threadID int64, code string) {

sendMessage(config, chatID, threadID, "🔄 Sending code to Claude...")

exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, "-l", code).Run()
tmuxPasteText(authTmuxSession, code)
time.Sleep(200 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, "C-m").Run()
tmuxSendKeys(authTmuxSession, "C-m")

for i := 0; i < 10; i++ {
time.Sleep(2 * time.Second)
out, _ := exec.Command(tmuxPath, "capture-pane", "-t", authTmuxSession, "-p").Output()
pane := string(out)

if strings.Contains(pane, "Yes, I accept") {
exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, "Down").Run()
tmuxSendKeys(authTmuxSession, "Down")
time.Sleep(200 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, "C-m").Run()
tmuxSendKeys(authTmuxSession, "C-m")
continue
}

if strings.Contains(pane, "Press Enter") || strings.Contains(pane, "Enter to confirm") {
exec.Command(tmuxPath, "send-keys", "-t", authTmuxSession, "C-m").Run()
tmuxSendKeys(authTmuxSession, "C-m")
continue
}

Expand Down
56 changes: 39 additions & 17 deletions tmux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -164,7 +165,7 @@ func createTmuxWindow(windowName string, workDir string, continueSession bool) (

// Send the command to the window via send-keys using window ID
time.Sleep(200 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", windowID, cccCmd, "C-m").Run()
tmuxSendKeys(windowID, cccCmd, "C-m")

return windowID, nil
}
Expand All @@ -185,6 +186,10 @@ func runClaudeRaw(continueSession bool) error {
}

var args []string
// Auto-approve mode: skip permissions when no OTP is configured
if config, err := loadConfig(); err == nil && config.OTPSecret == "" {
args = append(args, "--dangerously-skip-permissions")
}
if continueSession {
args = append(args, "-c")
}
Expand Down Expand Up @@ -247,32 +252,49 @@ func sendToTmuxFromTelegramWithDelay(target string, windowName string, text stri
return sendToTmuxWithDelay(target, text, delay)
}

func sendToTmux(target string, text string) error {
// Calculate delay based on text length
// Base: 50ms + 0.5ms per character, capped at 5 seconds
baseDelay := 50 * time.Millisecond
charDelay := time.Duration(len(text)) * 500 * time.Microsecond // 0.5ms per char
delay := baseDelay + charDelay
if delay > 5*time.Second {
delay = 5 * time.Second
// tmuxSendKeys sends control keys to a tmux target with timeout protection.
// Use for all send-keys calls to prevent indefinite hangs (tmux/tmux#1185).
func tmuxSendKeys(target string, args ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmdArgs := append([]string{"send-keys", "-t", target}, args...)
return exec.CommandContext(ctx, tmuxPath, cmdArgs...).Run()
}

// tmuxPasteText sends text to a tmux target using set-buffer + paste-buffer -p.
// Bracketed paste mode avoids the send-keys -l hang when the PTY buffer is full.
func tmuxPasteText(target string, text string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := exec.CommandContext(ctx, tmuxPath, "set-buffer", "-b", "ccc-input", text).Run(); err != nil {
return fmt.Errorf("set-buffer: %w", err)
}
return sendToTmuxWithDelay(target, text, delay)
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2()
if err := exec.CommandContext(ctx2, tmuxPath, "paste-buffer", "-b", "ccc-input", "-p", "-t", target).Run(); err != nil {
return fmt.Errorf("paste-buffer: %w", err)
}
return nil
}

func sendToTmux(target string, text string) error {
return sendToTmuxWithDelay(target, text, 0)
}

func sendToTmuxWithDelay(target string, text string, delay time.Duration) error {
// Send text literally
cmd := exec.Command(tmuxPath, "send-keys", "-t", target, "-l", text)
if err := cmd.Run(); err != nil {
if err := tmuxPasteText(target, text); err != nil {
return err
}

// Brief pause for TUI to process pasted text
// Dismiss autocomplete popup that bracketed paste may trigger
time.Sleep(100 * time.Millisecond)
tmuxSendKeys(target, "Escape")

// Send Enter twice (Claude Code needs double Enter)
exec.Command(tmuxPath, "send-keys", "-t", target, "C-m").Run()
// Submit with double Enter (Claude Code needs double Enter)
time.Sleep(50 * time.Millisecond)
tmuxSendKeys(target, "Enter")
time.Sleep(50 * time.Millisecond)
exec.Command(tmuxPath, "send-keys", "-t", target, "C-m").Run()
tmuxSendKeys(target, "Enter")

return nil
}
Expand Down