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"
|
OpExport = "export"
|
||||||
OpImport = "import"
|
OpImport = "import"
|
||||||
OpEpicStatus = "epic_status"
|
OpEpicStatus = "epic_status"
|
||||||
OpGetMutations = "get_mutations"
|
OpGetMutations = "get_mutations"
|
||||||
OpShutdown = "shutdown"
|
OpGetMoleculeProgress = "get_molecule_progress"
|
||||||
OpDelete = "delete"
|
OpShutdown = "shutdown"
|
||||||
OpGetWorkerStatus = "get_worker_status"
|
OpDelete = "delete"
|
||||||
|
OpGetWorkerStatus = "get_worker_status"
|
||||||
|
|
||||||
// Gate operations (bd-likt)
|
// Gate operations (bd-likt)
|
||||||
OpGateCreate = "gate_create"
|
OpGateCreate = "gate_create"
|
||||||
@@ -489,3 +490,25 @@ type WorkerStatus struct {
|
|||||||
type GetWorkerStatusResponse struct {
|
type GetWorkerStatusResponse struct {
|
||||||
Workers []WorkerStatus `json:"workers"`
|
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
|
package rpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/beads/internal/storage"
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerVersion is the version of this RPC server
|
// ServerVersion is the version of this RPC server
|
||||||
@@ -232,3 +234,120 @@ func (s *Server) handleGetMutations(req *Request) Response {
|
|||||||
Data: data,
|
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)
|
resp = s.handleEpicStatus(req)
|
||||||
case OpGetMutations:
|
case OpGetMutations:
|
||||||
resp = s.handleGetMutations(req)
|
resp = s.handleGetMutations(req)
|
||||||
|
case OpGetMoleculeProgress:
|
||||||
|
resp = s.handleGetMoleculeProgress(req)
|
||||||
case OpGetWorkerStatus:
|
case OpGetWorkerStatus:
|
||||||
resp = s.handleGetWorkerStatus(req)
|
resp = s.handleGetWorkerStatus(req)
|
||||||
case OpShutdown:
|
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 {
|
func (s *SQLiteStorage) invalidateBlockedCache(ctx context.Context, exec execer) error {
|
||||||
return s.rebuildBlockedCache(ctx, exec)
|
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