Add requires_approval and requires_review flags to bead UI across all frontends (#10)
Implements bd-258: display and toggle of requires_approval / requires_review boolean flags in all four AnviLLM frontends. Flags are backed by labels (requires_approval, requires_review) on each bead, read from the labels array in bead JSON and toggled via label/unlabel ctl commands. - Acme: adds Labels/HasLabel to Bead struct; shows flags in frontmatter; updateBead parses and applies label/unlabel on Put - TUI: showBead displays flags (green when set); edit dialog adds two checkboxes that toggle labels on save - Web: detail view shows flag status with Set/Unset buttons; new toggleBeadFlag() function calls beads ctl API and refreshes - Emacs: detail buffer shows flag lines; A/R keybindings invoke new anvillm-bead-toggle-approval and anvillm-bead-toggle-review commands Co-authored-by: Levi Neely <lkn@darkstar.example.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb721c0429
commit
45e495a975
|
|
@ -1440,13 +1440,23 @@ func sendReply(to, msgType, subject, body string) error {
|
|||
}
|
||||
|
||||
type Bead struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Assignee string `json:"assignee"`
|
||||
Priority int `json:"priority"`
|
||||
Blockers []string `json:"blockers,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Assignee string `json:"assignee"`
|
||||
Priority int `json:"priority"`
|
||||
Blockers []string `json:"blockers,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
func (b *Bead) HasLabel(label string) bool {
|
||||
for _, l := range b.Labels {
|
||||
if l == label {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func openTasksWindow() error {
|
||||
|
|
@ -1804,6 +1814,16 @@ func refreshViewBeadWindow(w *acme.Win, beadID string) {
|
|||
buf.WriteString(fmt.Sprintf("priority: %d\n", bead.Priority))
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("blockers: %s\n", strings.Join(bead.Blockers, ", ")))
|
||||
if bead.HasLabel("requires_approval") {
|
||||
buf.WriteString("requires_approval: yes\n")
|
||||
} else {
|
||||
buf.WriteString("requires_approval: no\n")
|
||||
}
|
||||
if bead.HasLabel("requires_review") {
|
||||
buf.WriteString("requires_review: yes\n")
|
||||
} else {
|
||||
buf.WriteString("requires_review: no\n")
|
||||
}
|
||||
buf.WriteString("---\n")
|
||||
if bead.Description != "" {
|
||||
desc, _ := strconv.Unquote(`"` + bead.Description + `"`)
|
||||
|
|
@ -1882,6 +1902,8 @@ func updateBead(beadID, content string, origBlockers []string) error {
|
|||
// Parse frontmatter
|
||||
var title string
|
||||
var newBlockers []string
|
||||
requiresApproval := -1 // -1=unset, 0=false, 1=true
|
||||
requiresReview := -1
|
||||
for _, line := range strings.Split(frontmatter, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "title:") {
|
||||
|
|
@ -1896,6 +1918,20 @@ func updateBead(beadID, content string, origBlockers []string) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "requires_approval:") {
|
||||
val := strings.TrimSpace(strings.TrimPrefix(line, "requires_approval:"))
|
||||
if val == "yes" || val == "true" || val == "1" {
|
||||
requiresApproval = 1
|
||||
} else {
|
||||
requiresApproval = 0
|
||||
}
|
||||
} else if strings.HasPrefix(line, "requires_review:") {
|
||||
val := strings.TrimSpace(strings.TrimPrefix(line, "requires_review:"))
|
||||
if val == "yes" || val == "true" || val == "1" {
|
||||
requiresReview = 1
|
||||
} else {
|
||||
requiresReview = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1930,5 +1966,19 @@ func updateBead(beadID, content string, origBlockers []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Toggle requires_approval label
|
||||
if requiresApproval == 1 {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("label %s requires_approval", beadID)))
|
||||
} else if requiresApproval == 0 {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("unlabel %s requires_approval", beadID)))
|
||||
}
|
||||
|
||||
// Toggle requires_review label
|
||||
if requiresReview == 1 {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("label %s requires_review", beadID)))
|
||||
} else if requiresReview == 0 {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("unlabel %s requires_review", beadID)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1300,7 +1300,30 @@ func showBead(bead map[string]interface{}) {
|
|||
status = s
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("[yellow]ID:[-] %s\n[yellow]Title:[-] %s\n[yellow]Status:[-] %s\n\n%s", id, title, status, description)
|
||||
requiresApproval := false
|
||||
requiresReview := false
|
||||
if labels, ok := bead["labels"].([]interface{}); ok {
|
||||
for _, l := range labels {
|
||||
if s, ok := l.(string); ok {
|
||||
if s == "requires_approval" {
|
||||
requiresApproval = true
|
||||
}
|
||||
if s == "requires_review" {
|
||||
requiresReview = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
approvalStr := "no"
|
||||
if requiresApproval {
|
||||
approvalStr = "[green]YES[-]"
|
||||
}
|
||||
reviewStr := "no"
|
||||
if requiresReview {
|
||||
reviewStr = "[green]YES[-]"
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("[yellow]ID:[-] %s\n[yellow]Title:[-] %s\n[yellow]Status:[-] %s\n[yellow]Requires Approval:[-] %s\n[yellow]Requires Review:[-] %s\n\n%s", id, title, status, approvalStr, reviewStr, description)
|
||||
|
||||
textView := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
|
|
@ -1454,6 +1477,20 @@ func showEditBeadDialog(bead map[string]interface{}) {
|
|||
if p, ok := bead["priority"].(float64); ok {
|
||||
priority = int(p)
|
||||
}
|
||||
requiresApproval := false
|
||||
requiresReview := false
|
||||
if labels, ok := bead["labels"].([]interface{}); ok {
|
||||
for _, l := range labels {
|
||||
if s, ok := l.(string); ok {
|
||||
if s == "requires_approval" {
|
||||
requiresApproval = true
|
||||
}
|
||||
if s == "requires_review" {
|
||||
requiresReview = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleInput := tview.NewInputField().
|
||||
SetLabel("Title: ").
|
||||
|
|
@ -1479,11 +1516,21 @@ func showEditBeadDialog(bead map[string]interface{}) {
|
|||
SetFieldTextColor(tcell.ColorBlack).
|
||||
SetFieldBackgroundColor(tcell.ColorWhite)
|
||||
|
||||
approvalCheck := tview.NewCheckbox().
|
||||
SetLabel("Requires Approval: ").
|
||||
SetChecked(requiresApproval)
|
||||
|
||||
reviewCheck := tview.NewCheckbox().
|
||||
SetLabel("Requires Review: ").
|
||||
SetChecked(requiresReview)
|
||||
|
||||
form := tview.NewForm().
|
||||
AddFormItem(titleInput).
|
||||
AddFormItem(descArea).
|
||||
AddFormItem(statusInput).
|
||||
AddFormItem(priorityInput)
|
||||
AddFormItem(priorityInput).
|
||||
AddFormItem(approvalCheck).
|
||||
AddFormItem(reviewCheck)
|
||||
form.SetFieldTextColor(tcell.ColorBlack)
|
||||
form.SetButtonTextColor(tcell.ColorBlack)
|
||||
|
||||
|
|
@ -1492,6 +1539,8 @@ func showEditBeadDialog(bead map[string]interface{}) {
|
|||
newDesc := descArea.GetText()
|
||||
newStatus := statusInput.GetText()
|
||||
newPriorityStr := priorityInput.GetText()
|
||||
newRequiresApproval := approvalCheck.IsChecked()
|
||||
newRequiresReview := reviewCheck.IsChecked()
|
||||
|
||||
if newTitle == "" {
|
||||
updateStatus("[red]Title cannot be empty")
|
||||
|
|
@ -1534,6 +1583,24 @@ func showEditBeadDialog(bead map[string]interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
// Toggle requires_approval label
|
||||
if newRequiresApproval != requiresApproval {
|
||||
if newRequiresApproval {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("label %s requires_approval", id)))
|
||||
} else {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("unlabel %s requires_approval", id)))
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle requires_review label
|
||||
if newRequiresReview != requiresReview {
|
||||
if newRequiresReview {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("label %s requires_review", id)))
|
||||
} else {
|
||||
writeFile("beads/ctl", []byte(fmt.Sprintf("unlabel %s requires_review", id)))
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(fmt.Sprintf("[green]Updated bead %s", id))
|
||||
pages.RemovePage("edit-bead")
|
||||
pages.RemovePage("bead")
|
||||
|
|
|
|||
|
|
@ -1481,6 +1481,15 @@ ${msg.body}`;
|
|||
content += `<p><strong>Blockers:</strong> ${bead.blockers.map(b => escapeHtml(b)).join(', ')}</p>`;
|
||||
}
|
||||
|
||||
const hasApproval = bead.labels && bead.labels.includes('requires_approval');
|
||||
const hasReview = bead.labels && bead.labels.includes('requires_review');
|
||||
content += `<p><strong>Requires Approval:</strong> <span style="color:${hasApproval ? 'green' : 'inherit'}">${hasApproval ? 'Yes' : 'No'}</span>
|
||||
<button class="outline" style="margin-left:0.5rem; padding:0.1rem 0.5rem; font-size:0.8rem;"
|
||||
onclick="toggleBeadFlag('${escapeHtml(bead.id)}', 'requires_approval', ${hasApproval})">${hasApproval ? 'Unset' : 'Set'}</button></p>`;
|
||||
content += `<p><strong>Requires Review:</strong> <span style="color:${hasReview ? 'green' : 'inherit'}">${hasReview ? 'Yes' : 'No'}</span>
|
||||
<button class="outline" style="margin-left:0.5rem; padding:0.1rem 0.5rem; font-size:0.8rem;"
|
||||
onclick="toggleBeadFlag('${escapeHtml(bead.id)}', 'requires_review', ${hasReview})">${hasReview ? 'Unset' : 'Set'}</button></p>`;
|
||||
|
||||
document.getElementById('bead-detail-title').textContent = bead.title;
|
||||
document.getElementById('bead-detail-content').innerHTML = content;
|
||||
document.getElementById('bead-detail-dialog').showModal();
|
||||
|
|
@ -1494,6 +1503,26 @@ ${msg.body}`;
|
|||
currentBead = null;
|
||||
}
|
||||
|
||||
async function toggleBeadFlag(id, flag, currentlySet) {
|
||||
const cmd = currentlySet ? `unlabel ${id} ${flag}` : `label ${id} ${flag}`;
|
||||
try {
|
||||
const response = await fetch('/api/beads/ctl', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command: cmd })
|
||||
});
|
||||
if (response.ok) {
|
||||
// Re-open the detail dialog with refreshed data
|
||||
document.getElementById('bead-detail-dialog').close();
|
||||
await viewBead(id);
|
||||
} else {
|
||||
showError(await apiError(response), `Failed to toggle ${flag}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error.message, `Failed to toggle ${flag}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function claimBead(id) {
|
||||
try {
|
||||
const response = await fetch('/api/beads/ctl', {
|
||||
|
|
|
|||
|
|
@ -838,6 +838,8 @@ p, C-p - Previous line
|
|||
(define-key map (kbd "D") #'anvillm-tasks-remove-dependency)
|
||||
(define-key map (kbd "l") #'anvillm-tasks-add-label)
|
||||
(define-key map (kbd "L") #'anvillm-tasks-remove-label)
|
||||
(define-key map (kbd "A") #'anvillm-bead-toggle-approval)
|
||||
(define-key map (kbd "R") #'anvillm-bead-toggle-review)
|
||||
(define-key map (kbd "m") #'anvillm-tasks-comment)
|
||||
(define-key map (kbd "r") #'anvillm-tasks-refresh)
|
||||
(define-key map (kbd "g") #'anvillm-tasks-refresh)
|
||||
|
|
@ -1027,6 +1029,12 @@ p, C-p - Previous line
|
|||
(insert (format "\nBlockers (%d):\n" (length blockers)))
|
||||
(dolist (blocker blockers)
|
||||
(insert (format " - %s\n" blocker))))
|
||||
(let* ((labels (plist-get bead :labels))
|
||||
(label-list (if (vectorp labels) (append labels nil) nil))
|
||||
(has-approval (member "requires_approval" label-list))
|
||||
(has-review (member "requires_review" label-list)))
|
||||
(insert (format "\nRequires Approval: %s\n" (if has-approval "YES" "no")))
|
||||
(insert (format "Requires Review: %s\n" (if has-review "YES" "no"))))
|
||||
(special-mode))
|
||||
(pop-to-buffer buffer))
|
||||
(error
|
||||
|
|
@ -1097,6 +1105,54 @@ p, C-p - Previous line
|
|||
(message "Failed to remove label: %s" (error-message-string err))))))
|
||||
(message "No bead selected")))
|
||||
|
||||
(defun anvillm-bead-toggle-approval ()
|
||||
"Toggle the requires_approval flag on the selected bead."
|
||||
(interactive)
|
||||
(if-let ((bead-id (anvillm--get-selected-bead)))
|
||||
(condition-case err
|
||||
(let* ((json-str (anvillm--9p-read
|
||||
(concat anvillm-agent-path "/beads/" bead-id "/json")))
|
||||
(bead (json-read-from-string json-str))
|
||||
(labels (plist-get bead :labels))
|
||||
(label-list (if (vectorp labels) (append labels nil) nil))
|
||||
(has-flag (member "requires_approval" label-list)))
|
||||
(if has-flag
|
||||
(progn
|
||||
(anvillm--9p-write (concat anvillm-agent-path "/beads/ctl")
|
||||
(format "unlabel %s requires_approval" bead-id))
|
||||
(message "Cleared requires_approval on %s" bead-id))
|
||||
(anvillm--9p-write (concat anvillm-agent-path "/beads/ctl")
|
||||
(format "label %s requires_approval" bead-id))
|
||||
(message "Set requires_approval on %s" bead-id))
|
||||
(anvillm--refresh-tasks))
|
||||
(error
|
||||
(message "Failed to toggle requires_approval: %s" (error-message-string err))))
|
||||
(message "No bead selected")))
|
||||
|
||||
(defun anvillm-bead-toggle-review ()
|
||||
"Toggle the requires_review flag on the selected bead."
|
||||
(interactive)
|
||||
(if-let ((bead-id (anvillm--get-selected-bead)))
|
||||
(condition-case err
|
||||
(let* ((json-str (anvillm--9p-read
|
||||
(concat anvillm-agent-path "/beads/" bead-id "/json")))
|
||||
(bead (json-read-from-string json-str))
|
||||
(labels (plist-get bead :labels))
|
||||
(label-list (if (vectorp labels) (append labels nil) nil))
|
||||
(has-flag (member "requires_review" label-list)))
|
||||
(if has-flag
|
||||
(progn
|
||||
(anvillm--9p-write (concat anvillm-agent-path "/beads/ctl")
|
||||
(format "unlabel %s requires_review" bead-id))
|
||||
(message "Cleared requires_review on %s" bead-id))
|
||||
(anvillm--9p-write (concat anvillm-agent-path "/beads/ctl")
|
||||
(format "label %s requires_review" bead-id))
|
||||
(message "Set requires_review on %s" bead-id))
|
||||
(anvillm--refresh-tasks))
|
||||
(error
|
||||
(message "Failed to toggle requires_review: %s" (error-message-string err))))
|
||||
(message "No bead selected")))
|
||||
|
||||
(defun anvillm-tasks-assign-to-agent ()
|
||||
"Assign the selected bead to an agent to work on."
|
||||
(interactive)
|
||||
|
|
|
|||
Reference in New Issue