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,106 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"git.unkin.net/unkin/forgebot/pkg/models"
|
||||
)
|
||||
|
||||
type editorFinishedMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func editTaskCmd(task *models.Task, client *Client) tea.Cmd {
|
||||
data := marshalTaskForEdit(task)
|
||||
tmpFile, err := os.CreateTemp("", "forgebot-task-*.yaml")
|
||||
if err != nil {
|
||||
return func() tea.Msg { return editorFinishedMsg{err: err} }
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpPath)
|
||||
return func() tea.Msg { return editorFinishedMsg{err: err} }
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
editor := resolveEditor()
|
||||
c := exec.Command(editor, tmpPath)
|
||||
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
defer os.Remove(tmpPath)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
edited, err := os.ReadFile(tmpPath)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
et, err := unmarshalEditedTask(edited)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
diff := diffEditableTask(task, et)
|
||||
if diff == nil {
|
||||
return editorFinishedMsg{}
|
||||
}
|
||||
|
||||
err = client.UpdateTask(context.Background(), task.ID, *diff)
|
||||
return editorFinishedMsg{err: err}
|
||||
})
|
||||
}
|
||||
|
||||
func newTaskEditorCmd(client *Client) tea.Cmd {
|
||||
data := marshalNewTask()
|
||||
tmpFile, err := os.CreateTemp("", "forgebot-new-*.yaml")
|
||||
if err != nil {
|
||||
return func() tea.Msg { return editorFinishedMsg{err: err} }
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpPath)
|
||||
return func() tea.Msg { return editorFinishedMsg{err: err} }
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
editor := resolveEditor()
|
||||
c := exec.Command(editor, tmpPath)
|
||||
|
||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||
defer os.Remove(tmpPath)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
edited, err := os.ReadFile(tmpPath)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
req, err := unmarshalNewTask(edited)
|
||||
if err != nil {
|
||||
return editorFinishedMsg{err: err}
|
||||
}
|
||||
|
||||
_, err = client.CreateTask(context.Background(), *req)
|
||||
return editorFinishedMsg{err: err}
|
||||
})
|
||||
}
|
||||
|
||||
func resolveEditor() string {
|
||||
if e := os.Getenv("EDITOR"); e != "" {
|
||||
return e
|
||||
}
|
||||
if e := os.Getenv("VISUAL"); e != "" {
|
||||
return e
|
||||
}
|
||||
return "vi"
|
||||
}
|
||||
Reference in New Issue
Block a user