Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
349f58e
Add commit message viewer (m) and keyboard help modal (?)
wesm Jan 25, 2026
27e28b4
Rename respond command to comment
wesm Jan 25, 2026
f6b465b
Normalize comment terminology and revert skill to roborev:respond
wesm Jan 25, 2026
3fea8f8
Normalize user-facing terminology to use "comment" instead of "response"
wesm Jan 25, 2026
50b7c7f
Rename AddResponse/GetResponses functions to AddComment/GetComments
wesm Jan 25, 2026
6b87596
Rename API endpoints from /api/respond to /api/comment
wesm Jan 25, 2026
4912673
Rename API fields from responder/response to commenter/comment
wesm Jan 25, 2026
b231088
Fix commit message view navigation and sanitize display content
wesm Jan 25, 2026
d991fa8
Fix GetSystemPrompt to return empty for run without template
wesm Jan 25, 2026
399b5b2
Fix gitDescribePattern to match -dirty suffix from git describe
wesm Jan 25, 2026
683c004
Fix dirty job detection, OSC escape handling, and test messages
wesm Jan 25, 2026
054036a
Fix run job detection in fetchCommitMsg
wesm Jan 25, 2026
a818108
Add tests for fetchCommitMsg job type detection
wesm Jan 25, 2026
f76088c
Remove separator lines from commit message view
wesm Jan 25, 2026
6fc66d9
Document left/right arrow keys in help modal
wesm Jan 25, 2026
846d033
Add Page Up/Down to help modal
wesm Jan 25, 2026
face0b8
Order job list by job ID instead of enqueue time
wesm Jan 25, 2026
518142f
Revert incorrect selection update when marking review addressed
wesm Jan 25, 2026
d92d138
Show flash notification when no adjacent review exists
wesm Jan 25, 2026
96bfa07
Move update notification to line 3 above queue table
wesm Jan 25, 2026
fa6d70d
Show flash notification at queue navigation bounds
wesm Jan 25, 2026
79d6136
Improve fetchCommitMsg job detection and add test coverage
wesm Jan 25, 2026
edc7914
Add flashExpiresAt assertions and verify update notification line pos…
wesm Jan 25, 2026
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
10 changes: 5 additions & 5 deletions cmd/roborev/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package main

// Tests for daemon client functions (getResponsesForJob, waitForReview, findJobForCommit)
// Tests for daemon client functions (getCommentsForJob, waitForReview, findJobForCommit)

import (
"encoding/json"
Expand All @@ -14,10 +14,10 @@ import (
"github.com/roborev-dev/roborev/internal/storage"
)

func TestGetResponsesForJob(t *testing.T) {
func TestGetCommentsForJob(t *testing.T) {
t.Run("returns responses for job", func(t *testing.T) {
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/responses" || r.Method != "GET" {
if r.URL.Path != "/api/comments" || r.Method != "GET" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
return
Expand All @@ -36,7 +36,7 @@ func TestGetResponsesForJob(t *testing.T) {
}))
defer cleanup()

responses, err := getResponsesForJob(42)
responses, err := getCommentsForJob(42)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -51,7 +51,7 @@ func TestGetResponsesForJob(t *testing.T) {
}))
defer cleanup()

_, err := getResponsesForJob(42)
_, err := getCommentsForJob(42)
if err == nil {
t.Fatal("expected error, got nil")
}
Expand Down
10 changes: 5 additions & 5 deletions cmd/roborev/respond_test.go → cmd/roborev/comment_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package main

// Tests for the respond command
// Tests for the comment command

import (
"encoding/json"
Expand All @@ -12,15 +12,15 @@ import (
"github.com/roborev-dev/roborev/internal/version"
)

func TestRespondJobFlag(t *testing.T) {
func TestCommentJobFlag(t *testing.T) {
t.Run("--job forces job ID interpretation", func(t *testing.T) {
var receivedJobID int64
_, cleanup := setupMockDaemon(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/status" {
json.NewEncoder(w).Encode(map[string]interface{}{"version": version.Version})
return
}
if r.URL.Path == "/api/respond" && r.Method == "POST" {
if r.URL.Path == "/api/comment" && r.Method == "POST" {
var req struct {
JobID int64 `json:"job_id"`
}
Expand All @@ -34,7 +34,7 @@ func TestRespondJobFlag(t *testing.T) {
defer cleanup()

// "1234567" could be a SHA, but --job forces job ID interpretation
cmd := respondCmd()
cmd := commentCmd()
cmd.SetArgs([]string{"--job", "1234567", "-m", "test message"})
err := cmd.Execute()
if err != nil {
Expand All @@ -52,7 +52,7 @@ func TestRespondJobFlag(t *testing.T) {
}))
defer cleanup()

cmd := respondCmd()
cmd := commentCmd()
cmd.SetArgs([]string{"--job", "abc123", "-m", "test"})
err := cmd.Execute()
if err == nil {
Expand Down
79 changes: 44 additions & 35 deletions cmd/roborev/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func main() {
rootCmd.AddCommand(reviewCmd())
rootCmd.AddCommand(statusCmd())
rootCmd.AddCommand(showCmd())
rootCmd.AddCommand(respondCmd())
rootCmd.AddCommand(commentCmd())
rootCmd.AddCommand(respondCmd()) // hidden alias for backward compatibility
rootCmd.AddCommand(addressCmd())
rootCmd.AddCommand(installHookCmd())
rootCmd.AddCommand(uninstallHookCmd())
Expand Down Expand Up @@ -1199,27 +1200,27 @@ Examples:
return cmd
}

func respondCmd() *cobra.Command {
func commentCmd() *cobra.Command {
var (
responder string
commenter string
message string
forceJobID bool
)

cmd := &cobra.Command{
Use: "respond <job_id|sha> [message]",
Short: "Add a response to a review",
Long: `Add a response or note to a review.
Use: "comment <job_id|sha> [message]",
Short: "Add a comment to a review",
Long: `Add a comment or note to a review.

The first argument can be either a job ID (numeric) or a commit SHA.
Using job IDs is recommended since they are displayed in the TUI.

Examples:
roborev respond 42 "Fixed the null pointer issue"
roborev respond 42 -m "Added missing error handling"
roborev respond abc123 "Addressed by refactoring"
roborev respond 42 # Opens editor for message
roborev respond --job 1234567 "msg" # Force numeric arg as job ID`,
roborev comment 42 "Fixed the null pointer issue"
roborev comment 42 -m "Added missing error handling"
roborev comment abc123 "Addressed by refactoring"
roborev comment 42 # Opens editor for message
roborev comment --job 1234567 "msg" # Force numeric arg as job ID`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
// Ensure daemon is running
Expand Down Expand Up @@ -1272,7 +1273,7 @@ Examples:
editor = "vim"
}

tmpfile, err := os.CreateTemp("", "roborev-response-*.md")
tmpfile, err := os.CreateTemp("", "roborev-comment-*.md")
if err != nil {
return fmt.Errorf("create temp file: %w", err)
}
Expand All @@ -1289,26 +1290,26 @@ Examples:

content, err := os.ReadFile(tmpfile.Name())
if err != nil {
return fmt.Errorf("read response: %w", err)
return fmt.Errorf("read comment: %w", err)
}
message = strings.TrimSpace(string(content))
}

if message == "" {
return fmt.Errorf("empty response, aborting")
return fmt.Errorf("empty comment, aborting")
}

if responder == "" {
responder = os.Getenv("USER")
if responder == "" {
responder = "anonymous"
if commenter == "" {
commenter = os.Getenv("USER")
if commenter == "" {
commenter = "anonymous"
}
}

// Build request with either job_id or sha
reqData := map[string]interface{}{
"responder": responder,
"response": message,
"commenter": commenter,
"comment": message,
}
if jobID != 0 {
reqData["job_id"] = jobID
Expand All @@ -1319,29 +1320,37 @@ Examples:
reqBody, _ := json.Marshal(reqData)

addr := getDaemonAddr()
resp, err := http.Post(addr+"/api/respond", "application/json", bytes.NewReader(reqBody))
resp, err := http.Post(addr+"/api/comment", "application/json", bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("failed to connect to daemon: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to add response: %s", body)
return fmt.Errorf("failed to add comment: %s", body)
}

fmt.Println("Response added successfully")
fmt.Println("Comment added successfully")
return nil
},
}

cmd.Flags().StringVar(&responder, "responder", "", "responder name (default: $USER)")
cmd.Flags().StringVarP(&message, "message", "m", "", "response message (opens editor if not provided)")
cmd.Flags().StringVar(&commenter, "commenter", "", "commenter name (default: $USER)")
cmd.Flags().StringVarP(&message, "message", "m", "", "comment message (opens editor if not provided)")
cmd.Flags().BoolVar(&forceJobID, "job", false, "force argument to be treated as job ID (not SHA)")

return cmd
}

// respondCmd returns an alias for commentCmd
func respondCmd() *cobra.Command {
cmd := commentCmd()
cmd.Use = "respond <job_id|sha> [message]"
cmd.Short = "Alias for 'comment' - add a comment to a review"
return cmd
}

func addressCmd() *cobra.Command {
var unaddress bool

Expand Down Expand Up @@ -1561,19 +1570,19 @@ func enqueueReview(repoPath, gitRef, agentName string) (int64, error) {
return job.ID, nil
}

// getResponsesForJob fetches responses for a job
func getResponsesForJob(jobID int64) ([]storage.Response, error) {
// getCommentsForJob fetches comments for a job
func getCommentsForJob(jobID int64) ([]storage.Response, error) {
addr := getDaemonAddr()
client := &http.Client{Timeout: 5 * time.Second}

resp, err := client.Get(fmt.Sprintf("%s/api/responses?job_id=%d", addr, jobID))
resp, err := client.Get(fmt.Sprintf("%s/api/comments?job_id=%d", addr, jobID))
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetch responses: %s", resp.Status)
return nil, fmt.Errorf("fetch comments: %s", resp.Status)
}

var result struct {
Expand Down Expand Up @@ -2139,7 +2148,7 @@ func syncStatusCmd() *cobra.Command {
const maxPending = 1000
jobs, jobsErr := db.GetJobsToSync(machineID, maxPending)
reviews, reviewsErr := db.GetReviewsToSync(machineID, maxPending)
responses, responsesErr := db.GetResponsesToSync(machineID, maxPending)
responses, responsesErr := db.GetCommentsToSync(machineID, maxPending)

fmt.Println()
if jobsErr != nil || reviewsErr != nil || responsesErr != nil {
Expand All @@ -2153,7 +2162,7 @@ func syncStatusCmd() *cobra.Command {
}
return fmt.Sprintf("%d", count)
}
fmt.Printf("Pending push: %s jobs, %s reviews, %s responses\n",
fmt.Printf("Pending push: %s jobs, %s reviews, %s comments\n",
formatCount(len(jobs)), formatCount(len(reviews)), formatCount(len(responses)))

// Try to connect to PostgreSQL
Expand Down Expand Up @@ -2257,13 +2266,13 @@ func syncNowCmd() *cobra.Command {
totalJobs := getInt(msg, "total_jobs")
totalRevs := getInt(msg, "total_revs")
totalResps := getInt(msg, "total_resps")
fmt.Printf("\rPushing: batch %d (total: %d jobs, %d reviews, %d responses) ",
fmt.Printf("\rPushing: batch %d (total: %d jobs, %d reviews, %d comments) ",
batch, totalJobs, totalRevs, totalResps)
} else if phase == "pull" {
totalJobs := getInt(msg, "total_jobs")
totalRevs := getInt(msg, "total_revs")
totalResps := getInt(msg, "total_resps")
fmt.Printf("\rPulled: %d jobs, %d reviews, %d responses \n",
fmt.Printf("\rPulled: %d jobs, %d reviews, %d comments \n",
totalJobs, totalRevs, totalResps)
}
case "error":
Expand All @@ -2289,9 +2298,9 @@ func syncNowCmd() *cobra.Command {
}

fmt.Println("Sync completed")
fmt.Printf("Pushed: %d jobs, %d reviews, %d responses\n",
fmt.Printf("Pushed: %d jobs, %d reviews, %d comments\n",
finalPushed.Jobs, finalPushed.Reviews, finalPushed.Responses)
fmt.Printf("Pulled: %d jobs, %d reviews, %d responses\n",
fmt.Printf("Pulled: %d jobs, %d reviews, %d comments\n",
finalPulled.Jobs, finalPulled.Reviews, finalPulled.Responses)

return nil
Expand Down
18 changes: 9 additions & 9 deletions cmd/roborev/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func TestRefineNoChangeRetryLogic(t *testing.T) {
}))
defer cleanup()

responses, _ := getResponsesForJob(1)
responses, _ := getCommentsForJob(1)

// Count no-change attempts
noChangeAttempts := 0
Expand Down Expand Up @@ -350,7 +350,7 @@ func TestRunRefineSurfacesResponseErrors(t *testing.T) {
json.NewEncoder(w).Encode(storage.Review{
ID: 1, JobID: 1, Output: "**Bug found**: fail", Addressed: false,
})
case r.URL.Path == "/api/responses":
case r.URL.Path == "/api/comments":
w.WriteHeader(http.StatusInternalServerError)
default:
w.WriteHeader(http.StatusNotFound)
Expand Down Expand Up @@ -634,7 +634,7 @@ func createMockRefineHandler(state *mockRefineState) http.Handler {
}
json.NewEncoder(w).Encode(reviewCopy)

case r.URL.Path == "/api/responses" && r.Method == "GET":
case r.URL.Path == "/api/comments" && r.Method == "GET":
jobIDStr := r.URL.Query().Get("job_id")
var jobID int64
fmt.Sscanf(jobIDStr, "%d", &jobID)
Expand All @@ -648,7 +648,7 @@ func createMockRefineHandler(state *mockRefineState) http.Handler {
"responses": responses,
})

case r.URL.Path == "/api/respond" && r.Method == "POST":
case r.URL.Path == "/api/comment" && r.Method == "POST":
var req struct {
JobID int64 `json:"job_id"`
Responder string `json:"responder"`
Expand Down Expand Up @@ -807,7 +807,7 @@ func TestRefineLoopNoChangeRetryScenario(t *testing.T) {
_, cleanup := setupMockDaemon(t, createMockRefineHandler(state))
defer cleanup()

responses, err := getResponsesForJob(42)
responses, err := getCommentsForJob(42)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -834,7 +834,7 @@ func TestRefineLoopNoChangeRetryScenario(t *testing.T) {
_, cleanup := setupMockDaemon(t, createMockRefineHandler(state))
defer cleanup()

responses, _ := getResponsesForJob(42)
responses, _ := getCommentsForJob(42)
noChangeAttempts := countNoChangeAttempts(responses)

// Should not give up yet (first attempt)
Expand All @@ -856,7 +856,7 @@ func TestRefineLoopNoChangeRetryScenario(t *testing.T) {
_, cleanup := setupMockDaemon(t, createMockRefineHandler(state))
defer cleanup()

responses, _ := getResponsesForJob(42)
responses, _ := getCommentsForJob(42)
noChangeAttempts := countNoChangeAttempts(responses)

// Should only count the 1 response from roborev-refine, not the others
Expand Down Expand Up @@ -1068,7 +1068,7 @@ func TestRefineLoopStaysOnFailedFixChain(t *testing.T) {
}
json.NewEncoder(w).Encode(review)

case r.URL.Path == "/api/responses" && r.Method == http.MethodGet:
case r.URL.Path == "/api/comments" && r.Method == http.MethodGet:
jobIDStr := r.URL.Query().Get("job_id")
var jobID int64
fmt.Sscanf(jobIDStr, "%d", &jobID)
Expand All @@ -1080,7 +1080,7 @@ func TestRefineLoopStaysOnFailedFixChain(t *testing.T) {
"responses": responses,
})

case r.URL.Path == "/api/respond" && r.Method == http.MethodPost:
case r.URL.Path == "/api/comment" && r.Method == http.MethodPost:
var req struct {
JobID int64 `json:"job_id"`
Responder string `json:"responder"`
Expand Down
Loading
Loading