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:
Steve Yegge
2025-12-23 18:27:35 -08:00
parent b889aa6edb
commit f777093386
4 changed files with 167 additions and 4 deletions

View File

@@ -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"`
}

View File

@@ -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,
}
}

View File

@@ -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:

View File

@@ -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()
}