diff --git a/cmd/Assist/main.go b/cmd/Assist/main.go index 8540c11..1d378f8 100644 --- a/cmd/Assist/main.go +++ b/cmd/Assist/main.go @@ -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 } diff --git a/cmd/anvillm/main.go b/cmd/anvillm/main.go index c567dae..22bc0f2 100644 --- a/cmd/anvillm/main.go +++ b/cmd/anvillm/main.go @@ -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") diff --git a/cmd/anvilweb/static/index.html b/cmd/anvilweb/static/index.html index 6dccd79..58212aa 100644 --- a/cmd/anvilweb/static/index.html +++ b/cmd/anvilweb/static/index.html @@ -1481,6 +1481,15 @@ ${msg.body}`; content += `

Blockers: ${bead.blockers.map(b => escapeHtml(b)).join(', ')}

`; } + const hasApproval = bead.labels && bead.labels.includes('requires_approval'); + const hasReview = bead.labels && bead.labels.includes('requires_review'); + content += `

Requires Approval: ${hasApproval ? 'Yes' : 'No'} +

`; + content += `

Requires Review: ${hasReview ? 'Yes' : 'No'} +

`; + 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', { diff --git a/elisp/anvillm.el b/elisp/anvillm.el index 2256dc4..4c81b7c 100644 --- a/elisp/anvillm.el +++ b/elisp/anvillm.el @@ -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)