From b1ed93c9cc44f3746bd0042531c78667d19c9119 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sat, 24 Oct 2026 09:43:11 -0700 Subject: [PATCH] fix(tui): confirm Stop with Enter so y can never stop a session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Flow, K armed a stop that the next y confirmed. y is also the key that allows a permission prompt, so a user who pressed K by mistake, or who was answering prompts, could stop a session while meaning "allow". K now asks "Stop ""? Enter stop · any other key cancels". Only Enter confirms; y and n cancel the stop and stay prompt answers only. Claude-Session: https://claude.ai/code/session_01T2vtcW6ihn1ywy9vxUq9a4 --- internal/tui/app.go | 2 +- internal/tui/board.go | 22 ++++++++++------------ internal/tui/board_test.go | 15 ++++++++++----- internal/tui/header.go | 2 +- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/internal/tui/app.go b/internal/tui/app.go index 6190bf7a..198a1c9b 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -101,7 +101,7 @@ type app struct { boardOrder []string boardGroups []string boardCursor int - boardStop int64 // session awaiting y/n stop confirmation + boardStop int64 // session awaiting Enter to confirm a stop boardMachine bool boardFilter int boardIdle bool diff --git a/internal/tui/board.go b/internal/tui/board.go index 91be3bb3..97ccc528 100644 --- a/internal/tui/board.go +++ b/internal/tui/board.go @@ -152,9 +152,16 @@ func (m *app) boardSelected() *view.BoardRow { return &r } func (m *app) boardKey(k string) (tea.Cmd, bool) { - if m.boardStop != 0 && k != "y" && k != "n" { + // A pending stop takes the next key: Enter stops, anything else cancels. + // y and n stay prompt answers only, so allowing a prompt can never stop a session. + if id := m.boardStop; id != 0 { m.boardStop = 0 - m.status = "Cancelled" + if k != "enter" { + m.status = "Stop cancelled" + return nil, true + } + ctx, run := m.opts.Context, m.opts.SessionAction + return func() tea.Msg { return actionMsg{status: "Session stopped", err: run(ctx, "kill", id)} }, true } m.syncBoard() switch k { @@ -218,19 +225,10 @@ func (m *app) boardKey(k string) (tea.Cmd, bool) { m.status = fmt.Sprintf("driga agents kill %d · confirm in terminal", r.Node.ID) default: m.boardStop = r.Node.ID - m.status = fmt.Sprintf("Stop %q? y stop · n cancel", r.Title) + m.status = fmt.Sprintf("Stop %q? Enter stop · any other key cancels", r.Title) } return nil, true case "y", "n": - if id := m.boardStop; id != 0 { - m.boardStop = 0 - if k == "n" { - m.status = "Cancelled" - return nil, true - } - ctx, run := m.opts.Context, m.opts.SessionAction - return func() tea.Msg { return actionMsg{status: "Session stopped", err: run(ctx, "kill", id)} }, true - } if r := m.boardSelected(); m.flowAttention && r != nil && r.PromptID != "" { if m.boardPromptFocus != r.PromptID || m.opts.Now().Sub(m.boardPromptAt) < 600*time.Millisecond { m.status = "Review prompt before answering" diff --git a/internal/tui/board_test.go b/internal/tui/board_test.go index e8546b7e..38395d2d 100644 --- a/internal/tui/board_test.go +++ b/internal/tui/board_test.go @@ -226,13 +226,18 @@ func TestBoardStopNeedsConfirmation(t *testing.T) { t.Fatal("another key must cancel a pending stop") } m.Update(tea.KeyPressMsg{Code: 'k'}) - m.Update(tea.KeyPressMsg{Code: 'K'}) - m.Update(tea.KeyPressMsg{Code: 'n'}) - if len(stopped) != 0 { - t.Fatal("n stopped the session") + // y and n answer prompts; they must never confirm a stop. + for _, key := range []rune{'n', 'y'} { + m.Update(tea.KeyPressMsg{Code: 'K'}) + if _, cmd := m.Update(tea.KeyPressMsg{Code: key}); cmd != nil { + cmd() + } + if len(stopped) != 0 || m.boardStop != 0 { + t.Fatalf("%c confirmed a stop", key) + } } m.Update(tea.KeyPressMsg{Code: 'K'}) - _, cmd := m.Update(tea.KeyPressMsg{Code: 'y'}) + _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) execute(t, m, cmd) if len(stopped) != 1 || stopped[0] != id { t.Fatal("confirmed stop not dispatched", stopped, id) diff --git a/internal/tui/header.go b/internal/tui/header.go index 988c9fd9..c80a7b4d 100644 --- a/internal/tui/header.go +++ b/internal/tui/header.go @@ -370,7 +370,7 @@ func (m *app) emptyText() string { } func (m *app) helpText() string { - body := wrapText("0 Flow · 1 Projects · 2 Machines · c Chat panel · Esc closes panel\nFlow: m Map · t Timeline · s Sessions · h activity / return\nMap and Timeline use a project tree in the terminal. Tab focuses Needs you.\nj/k or arrows select · Enter opens · / find · Esc returns\ny/n answer explicitly focused prompt after review · J Terminal · K Stop\nMachines: h health · p reclaim processes · d reclaim disk\nSessions: S sort · right/left expand families\nChat: Enter sends · Shift+Enter new line · Ctrl+n new · Ctrl+h history\nChat: y/n allow/deny · Alt+y/Alt+n while typing · m trust mode\nAlt+0/1/2 switches tabs while typing · Ctrl+x clears Chat context\n? help · q / ctrl+c quit", m.width) + body := wrapText("0 Flow · 1 Projects · 2 Machines · c Chat panel · Esc closes panel\nFlow: m Map · t Timeline · s Sessions · h activity / return\nMap and Timeline use a project tree in the terminal. Tab focuses Needs you.\nj/k or arrows select · Enter opens · / find · Esc returns\ny/n answer explicitly focused prompt after review · J Terminal · K Stop, then Enter\nMachines: h health · p reclaim processes · d reclaim disk\nSessions: S sort · right/left expand families\nChat: Enter sends · Shift+Enter new line · Ctrl+n new · Ctrl+h history\nChat: y/n allow/deny · Alt+y/Alt+n while typing · m trust mode\nAlt+0/1/2 switches tabs while typing · Ctrl+x clears Chat context\n? help · q / ctrl+c quit", m.width) if m.tmuxTarget() != "" { body += "\na attach tmux" }