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
135 lines
3.2 KiB
Go
135 lines
3.2 KiB
Go
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
|
|
}
|