Skip to content
Merged
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
31 changes: 27 additions & 4 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,29 @@ func (e *Executor) IsSuspended(taskID int64) bool {
return suspended
}

// agentSendTargetForPane returns the tmux send-keys target for a task's agent
// pane. It prefers the persisted pane id (the stable "%pane_id" captured at
// window creation and used by the UI for capture) so input is never
// misdelivered when the detail view has joined the agent pane into the UI
// session — which collapses the shell pane onto window index 0. Falls back to
// windowTarget+".0" when no pane id has been persisted yet.
func agentSendTargetForPane(claudePaneID, windowTarget string) string {
if claudePaneID != "" {
return claudePaneID
}
return windowTarget + ".0"
}

// agentSendTarget resolves the send-keys target for a task's agent pane,
// reading the persisted pane id from the database. See agentSendTargetForPane.
func (e *Executor) agentSendTarget(taskID int64, windowTarget string) string {
claudePaneID := ""
if t, err := e.db.GetTask(taskID); err == nil && t != nil {
claudePaneID = t.ClaudePaneID
}
return agentSendTargetForPane(claudePaneID, windowTarget)
}

// findPanesForWindow parses tmux list-panes output and returns PIDs for panes
// in windows matching the given name exactly. The input format is one line per pane:
//
Expand Down Expand Up @@ -2668,7 +2691,7 @@ func (e *Executor) resumeClaudeDangerous(task *db.Task, workDir string) bool {

// Automatically send "continue working" to resume the task
// This tells Claude to continue where it left off after the mode switch
exec.Command("tmux", "send-keys", "-t", windowTarget+".0", "continue working", "Enter").Run()
exec.Command("tmux", "send-keys", "-t", e.agentSendTarget(task.ID, windowTarget), "continue working", "Enter").Run()
e.logLine(taskID, "system", "Sent 'continue working' to resume task")

// Don't poll for completion here - the process will continue running in tmux
Expand Down Expand Up @@ -2833,7 +2856,7 @@ func (e *Executor) resumeClaudeSafe(task *db.Task, workDir string) bool {

// Automatically send "continue working" to resume the task
// This tells Claude to continue where it left off after the mode switch
exec.Command("tmux", "send-keys", "-t", windowTarget+".0", "continue working", "Enter").Run()
exec.Command("tmux", "send-keys", "-t", e.agentSendTarget(task.ID, windowTarget), "continue working", "Enter").Run()
e.logLine(taskID, "system", "Sent 'continue working' to resume task")

// Don't poll for completion here - the process will continue running in tmux
Expand Down Expand Up @@ -2939,7 +2962,7 @@ func (e *Executor) resumeCodexWithMode(task *db.Task, workDir string, dangerousM

// Automatically send "continue working" to resume the task
// This tells Codex to continue where it left off after the mode switch
exec.Command("tmux", "send-keys", "-t", windowTarget+".0", "continue working", "Enter").Run()
exec.Command("tmux", "send-keys", "-t", e.agentSendTarget(task.ID, windowTarget), "continue working", "Enter").Run()
e.logLine(taskID, "system", "Sent 'continue working' to resume task")

return true
Expand Down Expand Up @@ -3047,7 +3070,7 @@ func (e *Executor) resumeGeminiWithMode(task *db.Task, workDir string, dangerous

// Automatically send "continue working" to resume the task
// This tells Gemini to continue where it left off after the mode switch
exec.Command("tmux", "send-keys", "-t", windowTarget+".0", "continue working", "Enter").Run()
exec.Command("tmux", "send-keys", "-t", e.agentSendTarget(task.ID, windowTarget), "continue working", "Enter").Run()
e.logLine(taskID, "system", "Sent 'continue working' to resume task")

return true
Expand Down
41 changes: 41 additions & 0 deletions internal/executor/tmux_send_target_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package executor

import (
"testing"
)

// When the detail view joins the agent pane into the UI session, the shell pane
// collapses onto window index 0. Sending to windowTarget+".0" then misdelivers
// input to the shell and the agent starves. agentSendTargetForPane must prefer
// the persisted, stable pane id (the same one the UI uses for capture).
func TestAgentSendTargetForPane(t *testing.T) {
tests := []struct {
name string
claudePaneID string
windowTarget string
want string
}{
{
name: "persisted pane id is used, never window .0",
claudePaneID: "%3412",
windowTarget: "task-daemon-123:task-5",
want: "%3412",
},
{
name: "no persisted pane id -> fall back to window .0",
claudePaneID: "",
windowTarget: "task-daemon-123:task-5",
want: "task-daemon-123:task-5.0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := agentSendTargetForPane(tt.claudePaneID, tt.windowTarget)
if got != tt.want {
t.Errorf("agentSendTargetForPane(%q, %q) = %q, want %q",
tt.claudePaneID, tt.windowTarget, got, tt.want)
}
})
}
}
Loading