diff --git a/cmd/roborev/tui/handlers.go b/cmd/roborev/tui/handlers.go index 7c831748..f085395d 100644 --- a/cmd/roborev/tui/handlers.go +++ b/cmd/roborev/tui/handlers.go @@ -75,7 +75,7 @@ func (m model) handleMouseMsg(msg tea.MouseMsg) (tea.Model, tea.Cmd) { // handleGlobalKey handles keys shared across queue, review, prompt, commit msg, and help views. func (m model) handleGlobalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c", "ctrl+d", "q": return m.handleQuitKey() case "home", "g": return m.handleHomeKey() diff --git a/cmd/roborev/tui/handlers_modal.go b/cmd/roborev/tui/handlers_modal.go index 681dbeb7..be83e4fe 100644 --- a/cmd/roborev/tui/handlers_modal.go +++ b/cmd/roborev/tui/handlers_modal.go @@ -253,7 +253,7 @@ func (m model) handleLogKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": return m, tea.Quit - case "esc", "q": + case "ctrl+d", "esc", "q": m.currentView = m.logFromView m.logStreaming = false return m, nil @@ -329,7 +329,7 @@ func (m model) handleWorktreeConfirmKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": return m, tea.Quit - case "esc", "n": + case "ctrl+d", "esc", "n": m.currentView = viewTasks m.worktreeConfirmJobID = 0 m.worktreeConfirmBranch = "" @@ -347,7 +347,7 @@ func (m model) handleWorktreeConfirmKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // handleTasksKey handles key input in the tasks view. func (m model) handleTasksKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { - case "ctrl+c", "q": + case "ctrl+c", "ctrl+d", "q": return m, tea.Quit case "esc", "T": m.currentView = viewQueue @@ -504,7 +504,7 @@ func (m model) handlePatchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": return m, tea.Quit - case "esc", "q": + case "ctrl+d", "esc", "q": m.currentView = viewTasks m.patchText = "" m.patchScroll = 0 diff --git a/cmd/roborev/tui/handlers_queue.go b/cmd/roborev/tui/handlers_queue.go index 1ee08022..4652750a 100644 --- a/cmd/roborev/tui/handlers_queue.go +++ b/cmd/roborev/tui/handlers_queue.go @@ -197,7 +197,7 @@ func (m model) handleColumnOptionsInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } switch msg.String() { - case "esc": + case "ctrl+d", "esc": m.currentView = m.colOptionsReturnView if m.colOptionsDirty { m.colOptionsDirty = false diff --git a/cmd/roborev/tui/review_views_test.go b/cmd/roborev/tui/review_views_test.go index a71b00ce..9d167fc8 100644 --- a/cmd/roborev/tui/review_views_test.go +++ b/cmd/roborev/tui/review_views_test.go @@ -120,6 +120,50 @@ func TestTUICommitMsgViewNavigationWithQ(t *testing.T) { } } +func TestTUICtrlDQuitsFromQueueView(t *testing.T) { + m := initTestModel(withCurrentView(viewQueue)) + + _, cmd := pressSpecial(m, tea.KeyCtrlD) + + if cmd == nil { + t.Fatal("Expected quit command") + } + assertMsgType[tea.QuitMsg](t, cmd()) +} + +func TestTUICtrlDNavigatesBackFromReviewView(t *testing.T) { + job := makeJob(1) + m := initTestModel( + withCurrentView(viewReview), + withReview(&storage.Review{JobID: 1, Job: &job}), + withReviewFromView(viewQueue), + ) + + got, _ := pressSpecial(m, tea.KeyCtrlD) + + assertView(t, got, viewQueue) +} + +func TestTUICtrlDNoOpInCommentModal(t *testing.T) { + m := initTestModel(withCurrentView(viewKindComment)) + m.commentFromView = viewQueue + m.commentText = "draft comment" + m.commentJobID = 42 + + got, cmd := pressSpecial(m, tea.KeyCtrlD) + + assertView(t, got, viewKindComment) + if got.commentText != "draft comment" { + t.Errorf("Expected comment text preserved, got %q", got.commentText) + } + if got.commentJobID != 42 { + t.Errorf("Expected comment job ID preserved, got %d", got.commentJobID) + } + if cmd != nil { + t.Fatal("Expected no command from Ctrl-D in comment modal") + } +} + func TestFetchCommitMsgJobTypeDetection(t *testing.T) { // Test that fetchCommitMsg correctly identifies job types and returns appropriate errors // This is critical: Prompt field is populated for ALL jobs (stores review prompt),