8f48dd838b
Replace task statuses (pending/running/succeeded/failed/cancelled) with
a kanban workflow: todo → in_progress → in_review → done/wontdo.
When a non-review agent task completes, the API auto-creates a child
review task and moves the parent to in_review. Only humans can move
tasks from in_review to done/wontdo via the TUI.
New components:
- cmd/tui: bubbletea kanban board with $EDITOR integration
- POST /api/v1/tasks/{id}/complete: agent completion callback
- Operator --api-url flag for completion callbacks
- ProviderQueue sets tasks to in_progress on pickup
- AgentTask reconciler calls /complete on job finish
118 lines
2.5 KiB
Go
118 lines
2.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"git.unkin.net/unkin/forgebot/pkg/models"
|
|
)
|
|
|
|
type detailView struct {
|
|
task *models.Task
|
|
viewport viewport.Model
|
|
ready bool
|
|
}
|
|
|
|
func newDetailView() detailView {
|
|
return detailView{}
|
|
}
|
|
|
|
func (d *detailView) setTask(task *models.Task, width, height int) {
|
|
d.task = task
|
|
d.viewport = viewport.New(width, height-2)
|
|
d.viewport.SetContent(d.renderContent(width))
|
|
d.ready = true
|
|
}
|
|
|
|
func (d *detailView) renderContent(width int) string {
|
|
t := d.task
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
|
|
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(statusColors[t.Status])
|
|
b.WriteString(titleStyle.Render(fmt.Sprintf("Task: %s", t.ID)))
|
|
b.WriteString("\n\n")
|
|
|
|
row := func(label, value string) {
|
|
b.WriteString(detailLabelStyle.Render(label))
|
|
b.WriteString(detailValueStyle.Render(value))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
row("Status:", string(t.Status))
|
|
row("Command:", t.Command)
|
|
row("Repository:", t.Repository)
|
|
row("Ref:", t.Ref)
|
|
row("Author:", t.Author)
|
|
if t.PoolRef != "" {
|
|
row("Pool:", t.PoolRef)
|
|
}
|
|
if t.IssueNumber > 0 {
|
|
row("Issue:", fmt.Sprintf("#%d", t.IssueNumber))
|
|
}
|
|
if t.PRNumber > 0 {
|
|
row("PR:", fmt.Sprintf("#%d", t.PRNumber))
|
|
}
|
|
if t.Skill != "" {
|
|
row("Skill:", t.Skill)
|
|
}
|
|
if t.JobName != "" {
|
|
row("Job:", t.JobName)
|
|
}
|
|
if t.ParentTaskID != "" {
|
|
row("Parent:", t.ParentTaskID)
|
|
}
|
|
|
|
b.WriteString("\n")
|
|
row("Created:", t.CreatedAt.Format("2006-01-02 15:04:05"))
|
|
if t.StartedAt != nil {
|
|
row("Started:", t.StartedAt.Format("2006-01-02 15:04:05"))
|
|
}
|
|
if t.CompletedAt != nil {
|
|
row("Completed:", t.CompletedAt.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
if t.Body != "" {
|
|
b.WriteString("\n")
|
|
b.WriteString(lipgloss.NewStyle().Bold(true).Render("Body:"))
|
|
b.WriteString("\n")
|
|
b.WriteString(t.Body)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if t.Result != "" {
|
|
b.WriteString("\n")
|
|
b.WriteString(lipgloss.NewStyle().Bold(true).Render("Result:"))
|
|
b.WriteString("\n")
|
|
b.WriteString(t.Result)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if t.ErrorMessage != "" {
|
|
b.WriteString("\n")
|
|
b.WriteString(errStyle.Bold(true).Render("Error:"))
|
|
b.WriteString("\n")
|
|
b.WriteString(errStyle.Render(t.ErrorMessage))
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (d *detailView) view() string {
|
|
if !d.ready {
|
|
return ""
|
|
}
|
|
|
|
header := lipgloss.NewStyle().Bold(true).Render("Task Detail") +
|
|
" " + helpStyle.Render("esc=back e=edit j/k=scroll")
|
|
|
|
return header + "\n" + d.viewport.View()
|
|
}
|