Add TUI kanban board, review workflow, and new task statuses
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
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
type editableTask struct {
|
||||
Status string `yaml:"status"`
|
||||
Message string `yaml:"message"`
|
||||
ErrorMessage string `yaml:"error_message"`
|
||||
}
|
||||
|
||||
type newTask struct {
|
||||
Command string `yaml:"command"`
|
||||
Repository string `yaml:"repository"`
|
||||
Ref string `yaml:"ref"`
|
||||
Body string `yaml:"body"`
|
||||
Author string `yaml:"author"`
|
||||
Skill string `yaml:"skill"`
|
||||
PoolRef string `yaml:"pool_ref"`
|
||||
}
|
||||
|
||||
func marshalTaskForEdit(task *models.Task) []byte {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "# forgebot task %s\n", task.ID)
|
||||
b.WriteString("# Editable: status, message, error_message\n\n")
|
||||
|
||||
et := editableTask{
|
||||
Status: string(task.Status),
|
||||
Message: task.Result,
|
||||
ErrorMessage: task.ErrorMessage,
|
||||
}
|
||||
data, _ := yaml.Marshal(et)
|
||||
b.Write(data)
|
||||
|
||||
b.WriteString("\n# -- Context (read-only) --\n")
|
||||
fmt.Fprintf(&b, "# command: %s\n", task.Command)
|
||||
fmt.Fprintf(&b, "# repository: %s\n", task.Repository)
|
||||
fmt.Fprintf(&b, "# ref: %s\n", task.Ref)
|
||||
fmt.Fprintf(&b, "# author: %s\n", task.Author)
|
||||
if task.IssueNumber > 0 {
|
||||
fmt.Fprintf(&b, "# issue: %d\n", task.IssueNumber)
|
||||
}
|
||||
if task.PRNumber > 0 {
|
||||
fmt.Fprintf(&b, "# pr: %d\n", task.PRNumber)
|
||||
}
|
||||
fmt.Fprintf(&b, "# created: %s\n", task.CreatedAt.Format("2006-01-02T15:04:05Z"))
|
||||
if task.Body != "" {
|
||||
b.WriteString("#\n# body:\n")
|
||||
for _, line := range strings.Split(task.Body, "\n") {
|
||||
fmt.Fprintf(&b, "# %s\n", line)
|
||||
}
|
||||
}
|
||||
if task.Result != "" {
|
||||
b.WriteString("#\n# result:\n")
|
||||
for _, line := range strings.Split(task.Result, "\n") {
|
||||
fmt.Fprintf(&b, "# %s\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
func unmarshalEditedTask(data []byte) (*editableTask, error) {
|
||||
var et editableTask
|
||||
if err := yaml.Unmarshal(data, &et); err != nil {
|
||||
return nil, fmt.Errorf("parse edited task: %w", err)
|
||||
}
|
||||
return &et, nil
|
||||
}
|
||||
|
||||
func diffEditableTask(original *models.Task, edited *editableTask) *models.UpdateTaskRequest {
|
||||
req := &models.UpdateTaskRequest{}
|
||||
changed := false
|
||||
|
||||
if edited.Status != string(original.Status) {
|
||||
req.Status = models.TaskStatus(edited.Status)
|
||||
changed = true
|
||||
}
|
||||
if edited.Message != original.Result {
|
||||
req.Message = edited.Message
|
||||
changed = true
|
||||
}
|
||||
if edited.ErrorMessage != original.ErrorMessage {
|
||||
req.ErrorMessage = edited.ErrorMessage
|
||||
changed = true
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func marshalNewTask() []byte {
|
||||
var b strings.Builder
|
||||
b.WriteString("# New forgebot task\n\n")
|
||||
nt := newTask{
|
||||
Command: "implement",
|
||||
Repository: "",
|
||||
Ref: "main",
|
||||
Body: "",
|
||||
Author: "",
|
||||
Skill: "",
|
||||
PoolRef: "",
|
||||
}
|
||||
data, _ := yaml.Marshal(nt)
|
||||
b.Write(data)
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
func unmarshalNewTask(data []byte) (*models.CreateTaskRequest, error) {
|
||||
var nt newTask
|
||||
if err := yaml.Unmarshal(data, &nt); err != nil {
|
||||
return nil, fmt.Errorf("parse new task: %w", err)
|
||||
}
|
||||
if nt.Command == "" || nt.Repository == "" {
|
||||
return nil, fmt.Errorf("command and repository are required")
|
||||
}
|
||||
return &models.CreateTaskRequest{
|
||||
Command: nt.Command,
|
||||
Repository: nt.Repository,
|
||||
Ref: nt.Ref,
|
||||
Body: nt.Body,
|
||||
Author: nt.Author,
|
||||
Skill: nt.Skill,
|
||||
PoolRef: nt.PoolRef,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user