Skip to content
Open
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
51 changes: 51 additions & 0 deletions cmd/roborev/tui/handlers_modal.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package tui

import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"unicode"
Expand Down Expand Up @@ -465,6 +468,39 @@ func (m model) handleTasksKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {

// handlePatchKey handles key input in the patch viewer.
func (m model) handlePatchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// When the save-filename input is active, route keys there.
if m.savePatchInputActive {
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "esc":
m.savePatchInputActive = false
m.savePatchInput = ""
return m, nil
case "enter":
path := strings.TrimSpace(m.savePatchInput)
if path == "" {
return m, nil
}
m.savePatchInputActive = false
m.savePatchInput = ""
return m, m.savePatchToFile(path)
case "backspace":
if len(m.savePatchInput) > 0 {
runes := []rune(m.savePatchInput)
m.savePatchInput = string(runes[:len(runes)-1])
}
return m, nil
default:
for _, r := range msg.Runes {
if unicode.IsPrint(r) {
m.savePatchInput += string(r)
}
}
return m, nil
}
}

switch msg.String() {
case "ctrl+c":
return m, tea.Quit
Expand All @@ -474,6 +510,10 @@ func (m model) handlePatchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.patchScroll = 0
m.patchJobID = 0
return m, nil
case "s":
m.savePatchInputActive = true
m.savePatchInput = filepath.Join(os.TempDir(), fmt.Sprintf("roborev-%d.patch", m.patchJobID))
return m, nil
case "up", "k":
if m.patchScroll > 0 {
m.patchScroll--
Expand Down Expand Up @@ -501,3 +541,14 @@ func (m model) handlePatchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
return m, nil
}

// savePatchToFile writes the current patch text to path.
func (m model) savePatchToFile(path string) tea.Cmd {
patch := m.patchText
return func() tea.Msg {
if err := os.WriteFile(path, []byte(patch), 0o644); err != nil {
return savePatchResultMsg{err: err}
}
return savePatchResultMsg{path: path}
}
}
10 changes: 10 additions & 0 deletions cmd/roborev/tui/handlers_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,16 @@ func (m model) handleClipboardResultMsg(
return m, nil
}

// handleSavePatchResultMsg processes save-patch-to-file results.
func (m model) handleSavePatchResultMsg(msg savePatchResultMsg) (tea.Model, tea.Cmd) {
if msg.err != nil {
m.err = fmt.Errorf("save patch failed: %w", msg.err)
} else {
m.setFlash("Saved to "+msg.path, 3*time.Second, viewPatch)
}
return m, nil
}

// handleReconnectMsg processes daemon reconnection attempts.
func (m model) handleReconnectMsg(msg reconnectMsg) (tea.Model, tea.Cmd) {
m.reconnecting = false
Expand Down
20 changes: 17 additions & 3 deletions cmd/roborev/tui/render_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,23 @@ func (m model) renderPatchView() string {
}
}

b.WriteString(renderHelpTable([][]helpItem{
{{"j/k/↑/↓", "scroll"}, {"esc", "back to tasks"}},
}, m.width))
if m.savePatchInputActive {
label := "Save to: "
inputWidth := max(m.width-len(label)-2, 10)
display := m.savePatchInput
if len(display) > inputWidth {
display = display[len(display)-inputWidth:]
}
display = display + strings.Repeat(" ", max(inputWidth-len(display), 0))
b.WriteString(helpStyle.Render(label) + display + "\x1b[K\n")
b.WriteString(renderHelpTable([][]helpItem{
{{"enter", "save"}, {"esc", "cancel"}},
}, m.width))
} else {
b.WriteString(renderHelpTable([][]helpItem{
{{"j/k/↑/↓", "scroll"}, {"s", "save"}, {"esc", "back to tasks"}},
}, m.width))
}
b.WriteString("\x1b[K\x1b[J")
return b.String()
}
Expand Down
20 changes: 12 additions & 8 deletions cmd/roborev/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,16 @@ type model struct {
reviewFromView viewKind // View to return to when exiting review (queue or tasks)

// Fix task state
fixJobs []storage.ReviewJob // Fix jobs for tasks view
fixSelectedIdx int // Selected index in tasks view
fixPromptText string // Editable fix prompt text
fixPromptJobID int64 // Parent job ID for fix prompt modal
fixShowHelp bool // Show help overlay in tasks view
patchText string // Current patch text for patch viewer
patchScroll int // Scroll offset in patch viewer
patchJobID int64 // Job ID of the patch being viewed
fixJobs []storage.ReviewJob // Fix jobs for tasks view
fixSelectedIdx int // Selected index in tasks view
fixPromptText string // Editable fix prompt text
fixPromptJobID int64 // Parent job ID for fix prompt modal
fixShowHelp bool // Show help overlay in tasks view
patchText string // Current patch text for patch viewer
patchScroll int // Scroll offset in patch viewer
patchJobID int64 // Job ID of the patch being viewed
savePatchInputActive bool // Whether the save-filename input is visible
savePatchInput string // Current text in the save-filename input

// Inline fix panel (review view)
reviewFixPanelOpen bool // true when fix panel is visible in review view
Expand Down Expand Up @@ -692,6 +694,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
result, cmd = m.handlePatchResultMsg(msg)
case applyPatchResultMsg:
result, cmd = m.handleApplyPatchResultMsg(msg)
case savePatchResultMsg:
result, cmd = m.handleSavePatchResultMsg(msg)
case configSaveErrMsg:
m.colOptionsDirty = true
m.setFlash(
Expand Down
5 changes: 5 additions & 0 deletions cmd/roborev/tui/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ type patchMsg struct {
err error
}

type savePatchResultMsg struct {
path string
err error
}

// ClipboardWriter is an interface for clipboard operations (allows mocking in tests)
type ClipboardWriter interface {
WriteText(text string) error
Expand Down
Loading