feat(rpc): add GetMoleculeProgress endpoint (bd-0oqz)
New RPC endpoint to get detailed progress for a molecule (parent issue with child steps). Returns moleculeID, title, assignee, and list of steps with their status (done/current/ready/blocked) and timestamps. Used when user expands a worker in the activity feed TUI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,11 @@ const (
|
||||
OpExport = "export"
|
||||
OpImport = "import"
|
||||
OpEpicStatus = "epic_status"
|
||||
OpGetMutations = "get_mutations"
|
||||
OpShutdown = "shutdown"
|
||||
OpDelete = "delete"
|
||||
OpGetWorkerStatus = "get_worker_status"
|
||||
OpGetMutations = "get_mutations"
|
||||
OpGetMoleculeProgress = "get_molecule_progress"
|
||||
OpShutdown = "shutdown"
|
||||
OpDelete = "delete"
|
||||
OpGetWorkerStatus = "get_worker_status"
|
||||
|
||||
// Gate operations (bd-likt)
|
||||
OpGateCreate = "gate_create"
|
||||
@@ -489,3 +490,25 @@ type WorkerStatus struct {
|
||||
type GetWorkerStatusResponse struct {
|
||||
Workers []WorkerStatus `json:"workers"`
|
||||
}
|
||||
|
||||
// GetMoleculeProgressArgs represents arguments for the get_molecule_progress operation
|
||||
type GetMoleculeProgressArgs struct {
|
||||
MoleculeID string `json:"molecule_id"` // The ID of the molecule (parent issue)
|
||||
}
|
||||
|
||||
// MoleculeStep represents a single step within a molecule
|
||||
type MoleculeStep struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"` // "done", "current", "ready", "blocked"
|
||||
StartTime *string `json:"start_time"` // ISO 8601 timestamp when step was created
|
||||
CloseTime *string `json:"close_time"` // ISO 8601 timestamp when step was closed (if done)
|
||||
}
|
||||
|
||||
// MoleculeProgress represents the progress of a molecule (parent issue with steps)
|
||||
type MoleculeProgress struct {
|
||||
MoleculeID string `json:"molecule_id"`
|
||||
Title string `json:"title"`
|
||||
Assignee string `json:"assignee"`
|
||||
Steps []MoleculeStep `json:"steps"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// ServerVersion is the version of this RPC server
|
||||
@@ -232,3 +234,120 @@ func (s *Server) handleGetMutations(req *Request) Response {
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetMoleculeProgress handles the get_molecule_progress RPC operation
|
||||
// Returns detailed progress for a molecule (parent issue with child steps)
|
||||
func (s *Server) handleGetMoleculeProgress(req *Request) Response {
|
||||
var args GetMoleculeProgressArgs
|
||||
if err := json.Unmarshal(req.Args, &args); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("invalid arguments: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
store := s.storage
|
||||
if store == nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: "storage not available",
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
|
||||
// Get the molecule (parent issue)
|
||||
molecule, err := store.GetIssue(ctx, args.MoleculeID)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to get molecule: %v", err),
|
||||
}
|
||||
}
|
||||
if molecule == nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("molecule not found: %s", args.MoleculeID),
|
||||
}
|
||||
}
|
||||
|
||||
// Get children (issues that have parent-child dependency on this molecule)
|
||||
var children []*types.IssueWithDependencyMetadata
|
||||
if sqliteStore, ok := store.(interface {
|
||||
GetDependentsWithMetadata(ctx context.Context, issueID string) ([]*types.IssueWithDependencyMetadata, error)
|
||||
}); ok {
|
||||
allDependents, err := sqliteStore.GetDependentsWithMetadata(ctx, args.MoleculeID)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to get molecule children: %v", err),
|
||||
}
|
||||
}
|
||||
// Filter for parent-child relationships only
|
||||
for _, dep := range allDependents {
|
||||
if dep.DependencyType == types.DepParentChild {
|
||||
children = append(children, dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get blocked issue IDs for status computation
|
||||
blockedIDs := make(map[string]bool)
|
||||
if sqliteStore, ok := store.(interface {
|
||||
GetBlockedIssueIDs(ctx context.Context) ([]string, error)
|
||||
}); ok {
|
||||
ids, err := sqliteStore.GetBlockedIssueIDs(ctx)
|
||||
if err == nil {
|
||||
for _, id := range ids {
|
||||
blockedIDs[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build steps from children
|
||||
steps := make([]MoleculeStep, 0, len(children))
|
||||
for _, child := range children {
|
||||
step := MoleculeStep{
|
||||
ID: child.ID,
|
||||
Title: child.Title,
|
||||
}
|
||||
|
||||
// Compute step status
|
||||
switch child.Status {
|
||||
case types.StatusClosed:
|
||||
step.Status = "done"
|
||||
case types.StatusInProgress:
|
||||
step.Status = "current"
|
||||
default: // open, blocked, etc.
|
||||
if blockedIDs[child.ID] {
|
||||
step.Status = "blocked"
|
||||
} else {
|
||||
step.Status = "ready"
|
||||
}
|
||||
}
|
||||
|
||||
// Set timestamps
|
||||
startTime := child.CreatedAt.Format(time.RFC3339)
|
||||
step.StartTime = &startTime
|
||||
|
||||
if child.ClosedAt != nil {
|
||||
closeTime := child.ClosedAt.Format(time.RFC3339)
|
||||
step.CloseTime = &closeTime
|
||||
}
|
||||
|
||||
steps = append(steps, step)
|
||||
}
|
||||
|
||||
progress := MoleculeProgress{
|
||||
MoleculeID: molecule.ID,
|
||||
Title: molecule.Title,
|
||||
Assignee: molecule.Assignee,
|
||||
Steps: steps,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(progress)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,8 @@ func (s *Server) handleRequest(req *Request) Response {
|
||||
resp = s.handleEpicStatus(req)
|
||||
case OpGetMutations:
|
||||
resp = s.handleGetMutations(req)
|
||||
case OpGetMoleculeProgress:
|
||||
resp = s.handleGetMoleculeProgress(req)
|
||||
case OpGetWorkerStatus:
|
||||
resp = s.handleGetWorkerStatus(req)
|
||||
case OpShutdown:
|
||||
|
||||
@@ -246,3 +246,22 @@ func (s *SQLiteStorage) rebuildBlockedCache(ctx context.Context, exec execer) er
|
||||
func (s *SQLiteStorage) invalidateBlockedCache(ctx context.Context, exec execer) error {
|
||||
return s.rebuildBlockedCache(ctx, exec)
|
||||
}
|
||||
|
||||
// GetBlockedIssueIDs returns all issue IDs currently in the blocked cache
|
||||
func (s *SQLiteStorage) GetBlockedIssueIDs(ctx context.Context) ([]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, "SELECT issue_id FROM blocked_issues_cache")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query blocked_issues_cache: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan blocked issue ID: %w", err)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user