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
33 changes: 25 additions & 8 deletions cmd/roborev/daemon_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ func registerRepo(repoPath string) error {
return nil
}

// ensureDaemon checks if daemon is running, starts it if not
// If daemon is running but has different version, restart it
// ensureDaemon checks if daemon is running, starts it if not.
// If daemon is running but has different version, restart it.
// Set ROBOREV_SKIP_VERSION_CHECK=1 to accept any daemon version without
// restarting (useful for development with go run).
func ensureDaemon() error {
skipVersionCheck := os.Getenv("ROBOREV_SKIP_VERSION_CHECK") == "1"
client := &http.Client{Timeout: 500 * time.Millisecond}

// First check runtime files for any running daemon
Expand All @@ -140,10 +143,17 @@ func ensureDaemon() error {
}
decodeErr := json.NewDecoder(resp.Body).Decode(&status)

// Fail closed: restart if decode fails, version empty, or mismatch
if decodeErr != nil || status.Version == "" || status.Version != version.Version {
// Always fail on decode errors (response is not a valid daemon)
if decodeErr != nil {
if verbose {
fmt.Printf("Daemon version mismatch or unreadable (daemon: %s, cli: %s), restarting...\n", status.Version, version.Version)
fmt.Printf("Daemon response unreadable, restarting...\n")
}
return restartDaemon()
}
// Skip version mismatch check when env var is set
if !skipVersionCheck && (status.Version == "" || status.Version != version.Version) {
if verbose {
fmt.Printf("Daemon version mismatch (daemon: %s, cli: %s), restarting...\n", status.Version, version.Version)
}
return restartDaemon()
}
Expand All @@ -162,10 +172,17 @@ func ensureDaemon() error {
}
decodeErr := json.NewDecoder(resp.Body).Decode(&status)

// Fail closed: restart if decode fails, version empty, or mismatch
if decodeErr != nil || status.Version == "" || status.Version != version.Version {
// Always fail on decode errors (response is not a valid daemon)
if decodeErr != nil {
if verbose {
fmt.Printf("Daemon response unreadable, restarting...\n")
}
return restartDaemon()
}
// Skip version mismatch check when env var is set
if !skipVersionCheck && (status.Version == "" || status.Version != version.Version) {
if verbose {
fmt.Printf("Daemon version mismatch or unreadable (daemon: %s, cli: %s), restarting...\n", status.Version, version.Version)
fmt.Printf("Daemon version mismatch (daemon: %s, cli: %s), restarting...\n", status.Version, version.Version)
}
return restartDaemon()
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/roborev/tui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func (m model) handleGlobalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleFixKey()
case "T":
return m.handleToggleTasksKey()
case "D":
return m.handleDistractionFreeKey()
case "tab":
return m.handleTabKey()
}
Expand Down
14 changes: 13 additions & 1 deletion cmd/roborev/tui/handlers_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ func (m *model) handleQueueMouseClick(_ int, y int) {
start = max(end-visibleRows, 0)
}
}
row := y - 5 // rows start after title, status, update, header, separator
headerRows := 5 // title, status, update, header, separator
if m.queueCompact() {
headerRows = 1 // title only
}
row := y - headerRows
if row < 0 || row >= visibleRows {
return
}
Expand Down Expand Up @@ -75,6 +79,14 @@ func (m *model) handleTasksMouseClick(y int) {
m.fixSelectedIdx = idx
}

func (m model) handleDistractionFreeKey() (tea.Model, tea.Cmd) {
if m.currentView != viewQueue {
return m, nil
}
m.distractionFree = !m.distractionFree
return m, nil
}

func (m model) handleEnterKey() (tea.Model, tea.Cmd) {
job, ok := m.selectedJob()
if m.currentView != viewQueue || !ok {
Expand Down
94 changes: 91 additions & 3 deletions cmd/roborev/tui/queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func TestTUIQueueMouseClickSelectsVisibleRow(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
m.width = 120
m.height = 14
m.height = 20
m.jobs = []storage.ReviewJob{
makeJob(1),
makeJob(2),
Expand All @@ -195,7 +195,7 @@ func TestTUIQueueMouseHeaderClickDoesNotSort(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
m.width = 120
m.height = 14
m.height = 20
m.jobs = []storage.ReviewJob{
makeJob(3),
makeJob(1),
Expand Down Expand Up @@ -259,7 +259,7 @@ func TestTUIQueueMouseClickScrolledWindow(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
m.width = 120
m.height = 12 // small terminal
m.height = 16 // small but not compact (compact hides headers, changing click offsets)

// Create more jobs than fit on screen.
for i := range 20 {
Expand Down Expand Up @@ -307,6 +307,94 @@ func TestTUIQueueMouseClickScrolledWindow(t *testing.T) {
}
}

func TestTUIQueueCompactMode(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
m.width = 80
m.height = 10 // compact mode (< 15)

for i := range 5 {
m.jobs = append(m.jobs, makeJob(int64(i+1)))
}
m.selectedIdx = 0
m.selectedJobID = 1

output := m.View()

// Should have the title line
if !strings.Contains(output, "roborev queue") {
t.Error("compact mode should show title")
}
// Should NOT have the table header
if strings.Contains(output, "JobID") {
t.Error("compact mode should hide table header")
}
// Should NOT have help footer keys
if strings.Contains(output, "navigate") {
t.Error("compact mode should hide help footer")
}
// Should NOT have daemon status line
if strings.Contains(output, "Daemon:") {
t.Error("compact mode should hide status line")
}

// Mouse click at y=1 (first data row in compact mode) should select first job
m.selectedIdx = 2
m.selectedJobID = 3
m2, _ := updateModel(t, m, mouseLeftClick(4, 1))
if m2.selectedJobID != 1 {
t.Errorf("compact mouse click at y=1: expected job 1, got %d", m2.selectedJobID)
}
}

func TestTUIQueueDistractionFreeToggle(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewQueue
m.width = 120
m.height = 30 // tall enough for normal mode

for i := range 5 {
m.jobs = append(m.jobs, makeJob(int64(i+1)))
}
m.selectedIdx = 0
m.selectedJobID = 1

// Normal mode: should show chrome
output := m.View()
if !strings.Contains(output, "JobID") {
t.Error("normal mode should show table header")
}
if !strings.Contains(output, "navigate") {
t.Error("normal mode should show help footer")
}

// Toggle distraction-free with 'D'
m2, _ := updateModel(t, m, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}})
if !m2.distractionFree {
t.Fatal("D should toggle distraction-free on")
}
output = m2.View()
if strings.Contains(output, "JobID") {
t.Error("distraction-free should hide table header")
}
if strings.Contains(output, "navigate") {
t.Error("distraction-free should hide help footer")
}
if strings.Contains(output, "Daemon:") {
t.Error("distraction-free should hide status line")
}

// Toggle back off
m3, _ := updateModel(t, m2, tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}})
if m3.distractionFree {
t.Fatal("D should toggle distraction-free off")
}
output = m3.View()
if !strings.Contains(output, "JobID") {
t.Error("should show table header after toggling off")
}
}

func TestTUITasksMouseClickSelectsRow(t *testing.T) {
m := newTuiModel("http://localhost")
m.currentView = tuiViewTasks
Expand Down
1 change: 1 addition & 0 deletions cmd/roborev/tui/render_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func helpLines() []string {
{"r", "Re-run completed/failed job"},
{"F", "Trigger fix for selected review"},
{"T", "Open Tasks view"},
{"D", "Toggle distraction-free mode"},
},
},
{
Expand Down
Loading
Loading