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
5 changes: 5 additions & 0 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1703,6 +1703,11 @@ func SendKeyToPane(taskID int64, keys ...string) error {

// killAllWindowsByNameAllSessions kills ALL windows with a given name across all daemon sessions.
// Also kills any -shell variant windows.
// KillAllWindowsByNameAllSessions kills all tmux windows with the given name across all sessions.
func KillAllWindowsByNameAllSessions(windowName string) {
killAllWindowsByNameAllSessions(windowName)
}

func killAllWindowsByNameAllSessions(windowName string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Expand Down
45 changes: 45 additions & 0 deletions internal/ui/detail.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,17 @@ func (m *DetailModel) Refresh() tea.Cmd {
}

if m.ready && prevTask != nil && m.task != nil {
// Detect executor change — kill old session and restart with new executor
if prevTask.Executor != m.task.Executor && m.cachedWindowTarget != "" {
m.restartForExecutorSwitch(prevTask.Executor)
}

if prevTask.Status != m.task.Status ||
prevTask.DangerousMode != m.task.DangerousMode ||
prevTask.Pinned != m.task.Pinned ||
prevTask.Project != m.task.Project ||
prevTask.Type != m.task.Type ||
prevTask.Executor != m.task.Executor ||
prevTask.Title != m.task.Title {
m.viewport.SetContent(m.renderContent())
}
Expand Down Expand Up @@ -725,6 +731,45 @@ func (m *DetailModel) findTaskWindow() string {
return ""
}

// restartForExecutorSwitch kills the current tmux session and starts a fresh one
// with the new executor. Called when Refresh() detects the executor field changed.
func (m *DetailModel) restartForExecutorSwitch(prevExecutor string) {
log := GetLogger()
log.Info("restartForExecutorSwitch: %s -> %s for task %d", prevExecutor, m.task.Executor, m.task.ID)

// Kill the old tmux window
windowName := executor.TmuxWindowName(m.task.ID)
executor.KillAllWindowsByNameAllSessions(windowName)

// Clear cached tmux state
m.cachedWindowTarget = ""
m.claudePaneID = ""
m.workdirPaneID = ""

// Clear the stale session ID (it belongs to the old executor)
m.database.UpdateTaskClaudeSessionID(m.task.ID, "")
m.task.ClaudeSessionID = ""

m.database.AppendTaskLog(m.task.ID, "system", fmt.Sprintf("Executor switched from %s to %s — restarting session", prevExecutor, m.task.Executor))

// Start fresh session with the new executor asynchronously
m.paneLoading = true
m.paneLoadingStart = time.Now()
// Use a goroutine so Refresh() can return its tea.Cmd
go func() {
if err := m.startResumableSession(""); err != nil {
log.Error("restartForExecutorSwitch: failed to start new session: %v", err)
return
}
// Find and join the new window
m.cachedWindowTarget = m.findTaskWindow()
if m.cachedWindowTarget != "" {
m.joinTmuxPane()
}
m.paneLoading = false
}()
}

// startResumableSession starts a new tmux window with the task's executor.
// This reconnects to a session that was previously running but whose tmux window was killed.
func (m *DetailModel) startResumableSession(sessionID string) error {
Expand Down
Loading