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
2 changes: 2 additions & 0 deletions cmd/roborev/tui/control_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ func (m model) handleCtrlSetFilter(
m.fetchSeq++
m.queueColGen++
m.loadingJobs = true
m.recomputeClassifyEffective()
return m, controlResponse{OK: true}, m.fetchJobs()
}

Expand Down Expand Up @@ -299,6 +300,7 @@ func (m model) handleCtrlClearFilter(
m.fetchSeq++
m.queueColGen++
m.loadingJobs = true
m.recomputeClassifyEffective()
return m, controlResponse{OK: true}, m.fetchJobs()
}

Expand Down
14 changes: 14 additions & 0 deletions cmd/roborev/tui/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ func listJobsParams(values neturl.Values) daemonclient.ListJobsParams {
}
setStringParam("job_type", &params.JobType)
setStringParam("exclude_job_type", &params.ExcludeJobType)
if value := values.Get("hide_classify_jobs"); value != "" {
typed := daemonclient.ListJobsParamsHideClassifyJobs(value)
params.HideClassifyJobs = &typed
}
setStringParam("repo_prefix", &params.RepoPrefix)
setIntParam("limit", &params.Limit)
setIntParam("offset", &params.Offset)
Expand Down Expand Up @@ -198,6 +202,13 @@ func (m model) fetchJobs() tea.Cmd {
// Exclude fix jobs — they belong in the Tasks view, not the queue
params.Set("exclude_job_type", "fix")

// Hide auto-design-router byproducts (classify rows + skipped design
// rows) unless the user opted in via show_classify_jobs. Resolved at
// fetch time so single-repo filters honor that repo's override.
if !m.shouldShowClassifyJobs() {
params.Set("hide_classify_jobs", "true")
}

// Set limit: use pagination unless we need client-side filtering (multi-repo)
if needsAllJobs {
params.Set("limit", "0")
Expand Down Expand Up @@ -240,6 +251,9 @@ func (m model) fetchMoreJobs() tea.Cmd {
params.Set("closed", "false")
}
params.Set("exclude_job_type", "fix")
if !m.shouldShowClassifyJobs() {
params.Set("hide_classify_jobs", "true")
}
result, err := m.loadJobsPage(params)
if err != nil {
return paginationErrMsg{
Expand Down
2 changes: 2 additions & 0 deletions cmd/roborev/tui/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ func (m *model) reconcileAutoRepoFilter() bool {
m.fetchSeq++
m.queueColGen++
m.loadingJobs = true
m.recomputeClassifyEffective()
return true
}

Expand Down Expand Up @@ -319,6 +320,7 @@ func (m *model) popFilter() string {
case filterTypeRepo:
m.activeRepoFilter = nil
m.autoRepoFilter = false
m.recomputeClassifyEffective()
case filterTypeBranch:
m.activeBranchFilter = ""
}
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 @@ -129,6 +129,8 @@ func (m model) handleGlobalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m.handleBranchFilterOpenKey()
case "h":
return m.handleHideClosedKey()
case "s":
return m.handleToggleClassifyKey()
case "c":
return m.handleCommentOpenKey()
case "y":
Expand Down
4 changes: 4 additions & 0 deletions cmd/roborev/tui/handlers_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ func (m model) handleFilterKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.fetchSeq++
m.queueColGen++
m.loadingJobs = true
// activeRepoFilter may have changed; refresh the cached
// classify visibility so the next render/fetch sees the
// repo-scoped value without hitting disk on the hot path.
m.recomputeClassifyEffective()
return m, m.fetchJobs()
case "backspace":
if len(m.filterSearch) > 0 {
Expand Down
16 changes: 15 additions & 1 deletion cmd/roborev/tui/handlers_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,22 @@ func (m model) handleLogOutputMsg(
}
if msg.err != nil {
if errors.Is(msg.err, errNoLog) {
// Auto-design-router rows (running classify, terminal
// skipped, failed/canceled classify) never produce a
// streamed agent log because the classifier is a
// one-shot SchemaAgent.Decide call. Stay in the log
// view so renderLogView's classifyReasoningLines
// header can show the verdict, skip reason, and any
// classifier error — bouncing back to the queue
// would hide that information behind a flash.
job := m.logViewLookupJob()
if job != nil && len(classifyReasoningLines(job, m.width)) > 0 {
m.logLines = []logLine{}
m.logStreaming = false
return m, nil
}
flash := "No log available for this job"
if job := m.logViewLookupJob(); job != nil &&
if job != nil &&
job.Status == storage.JobStatusFailed &&
job.Error != "" {
flash = fmt.Sprintf(
Expand Down
17 changes: 17 additions & 0 deletions cmd/roborev/tui/handlers_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,20 @@ func (m model) handleHideClosedKey() (tea.Model, tea.Cmd) {
m.loadingJobs = true
return m, m.fetchJobs()
}

// handleToggleClassifyKey flips visibility of auto-design-router
// classifier rows and skipped design rows in the queue. The toggle
// is session-only; it overrides show_classify_jobs config until the
// TUI is restarted.
func (m model) handleToggleClassifyKey() (tea.Model, tea.Cmd) {
if m.currentView != viewQueue {
return m, nil
}
next := !m.shouldShowClassifyJobs()
m.classifyOverride = &next
m.recomputeClassifyEffective()
m.queueColGen++
m.fetchSeq++
m.loadingJobs = true
return m, m.fetchJobs()
}
2 changes: 1 addition & 1 deletion cmd/roborev/tui/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ func TestRenderHelpTableLinesWithinWidth(t *testing.T) {
helpSets := map[string][][]helpItem{
"queue": {
{{"x", "cancel"}, {"r", "rerun"}, {"l", "log"}, {"p", "prompt"}, {"c", "comment"}, {"y", "copy"}, {"m", "commit"}, {"F", "fix"}},
{{"↑/↓", "nav"}, {"enter", "review"}, {"a", "closed"}, {"f", "filter"}, {"h", "hide"}, {"T", "tasks"}, {"?", "help"}, {"q", "quit"}},
{{"↑/↓", "nav"}, {"enter", "review"}, {"a", "closed"}, {"f", "filter"}, {"h", "hide"}, {"s", "show classify"}, {"T", "tasks"}, {"?", "help"}, {"q", "quit"}},
},
"review": {
{{"p", "prompt"}, {"c", "comment"}, {"m", "commit"}, {"a", "closed"}, {"y", "copy"}, {"F", "fix"}},
Expand Down
6 changes: 3 additions & 3 deletions cmd/roborev/tui/nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,17 @@ func (m *model) logViewLookupJob() *storage.ReviewJob {
}

// logVisibleLines returns the number of content lines visible in the
// log view, accounting for title, optional command line, separator,
// status, and help bar.
// log view, accounting for title, optional command line, optional
// classify-reasoning lines, separator, status, and help bar.
func (m *model) logVisibleLines() int {
// title + separator + status + help(N)
helpRows := m.logHelpRows()
reserved := 3 + len(reflowHelpRows(helpRows, m.width))
// Check if command line header is shown
if job := m.logViewLookupJob(); job != nil {
if commandLineForJob(job) != "" {
reserved++
}
reserved += len(classifyReasoningLines(job, m.width))
}
return max(m.height-reserved, 1)
}
Expand Down
128 changes: 128 additions & 0 deletions cmd/roborev/tui/render_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/mattn/go-runewidth"
"github.com/roborev-dev/roborev/internal/storage"
)

func (m model) renderLogView() string {
Expand Down Expand Up @@ -42,6 +43,19 @@ func (m model) renderLogView() string {
headerLines++
}

// For auto-design-router rows (classify-typed, or terminal skipped
// design reviews) the streamed agent log is empty — the classifier
// runs as a one-shot SchemaAgent.Decide call, not a streaming chat.
// Surface the classifier's verdict and reasoning here so 'l' on a
// classify/skipped row actually shows something useful. Truncated
// to m.width so each returned string occupies exactly one terminal
// row, keeping the log content area aligned with logVisibleLines.
for _, line := range classifyReasoningLines(job, m.width) {
b.WriteString(line)
b.WriteString("\x1b[K\n")
headerLines++
}

// Calculate visible area (must match logVisibleLines())
logHelp := m.logHelpRows()
logHelpLines := len(reflowHelpRows(logHelp, m.width))
Expand Down Expand Up @@ -105,6 +119,119 @@ func (m model) renderLogView() string {
return b.String()
}

// classifyReasoningLines returns pre-styled header lines describing the
// auto-design-router classifier's verdict for a classify-typed row or a
// terminal skipped design row. Returns nil for jobs that aren't part of
// the auto-design pipeline so the log view leaves their layout alone.
//
// Both classify and skipped triggers require source='auto_design'; a
// future pipeline that adopts the classify job_type or the skipped
// status for unrelated reasons should not be mislabeled as
// auto-design output.
//
// Each returned string is truncated to fit width so it occupies exactly
// one terminal row. width <= 0 disables truncation (useful for tests or
// when the caller doesn't know the terminal width yet); the styled
// output reflects the raw text in that case.
func classifyReasoningLines(job *storage.ReviewJob, width int) []string {
if job == nil || job.Source != "auto_design" {
return nil
}
isClassify := job.JobType == storage.JobTypeClassify
isAutoDesignSkipped := job.Status == storage.JobStatusSkipped
if !isClassify && !isAutoDesignSkipped {
return nil
}

var verdict string
switch {
case isAutoDesignSkipped:
// completeClassifyAsSkip converts a classifier execution
// failure into a skipped row with job.Error populated. A
// clean "no design review needed" verdict comes from
// applyClassifyVerdict and leaves job.Error empty. Don't
// label a degraded skip as a clean verdict — operators
// reading the log header need to see that the classifier
// errored.
if job.Error != "" {
verdict = "Auto-design classifier failed (skipped)"
} else {
verdict = "Auto-design verdict: no design review needed"
}
case isClassify:
// classify rows can sit in queued/running, or end in
// failed/canceled when something went wrong before the
// classifier could MarkClassifyAsSkippedDesign or
// PromoteClassifyToDesignReview the row. Distinguish these so
// 'l' on a failed classifier doesn't claim it's still running.
switch job.Status {
case storage.JobStatusQueued, storage.JobStatusRunning:
verdict = "Auto-design classifier in progress"
case storage.JobStatusFailed:
verdict = "Auto-design classifier failed"
case storage.JobStatusCanceled:
verdict = "Auto-design classifier canceled"
default:
verdict = "Auto-design classifier: " + string(job.Status)
}
default:
verdict = "Auto-design classifier"
}

render := func(text string) string {
// Fold embedded newlines/tabs to single spaces so each
// rendered line occupies one terminal row. job.Error stores
// raw classifier stderr/error text and routinely contains
// '\n'; without this, multiline blobs would wrap across
// terminal rows while logVisibleLines reserves only one row
// per returned line, pushing status/help off the screen.
text = foldWhitespace(text)
if width > 0 && runewidth.StringWidth(text) > width {
text = runewidth.Truncate(text, width, "…")
}
return statusStyle.Render(text)
}

var lines []string
lines = append(lines, render(verdict))
if job.SkipReason != "" {
lines = append(lines, render("Reason: "+job.SkipReason))
}
if job.Error != "" {
lines = append(lines, render("Detail: "+job.Error))
}
return lines
}

// foldWhitespace replaces embedded newlines, carriage returns, and
// tabs with a single space and collapses runs of resulting whitespace.
// Used to keep one-row-per-line invariants when rendering raw error or
// reason text into the log view header.
func foldWhitespace(s string) string {
if !strings.ContainsAny(s, "\n\r\t") {
return s
}
var b strings.Builder
b.Grow(len(s))
prevSpace := false
for _, r := range s {
switch r {
case '\n', '\r', '\t':
r = ' '
}
if r == ' ' {
if prevSpace {
continue
}
prevSpace = true
} else {
prevSpace = false
}
b.WriteRune(r)
}
return b.String()
}

func formatHelpLine(key, desc string) string {
return fmt.Sprintf(" %-14s %s", key, desc)
}
Expand Down Expand Up @@ -147,6 +274,7 @@ func helpLines(tasksEnabled, noQuit bool) []string {
keys: []struct{ key, desc string }{
{"f", "Filter by repository/branch"},
{"h", "Toggle hide closed/failed"},
{"s", "Toggle classify rows (auto-design router)"},
{"esc", "Clear filters (one at a time)"},
},
},
Expand Down
Loading
Loading