diff --git a/internal/rpc/server_core.go b/internal/rpc/server_core.go index 27c1b751..f2adc64f 100644 --- a/internal/rpc/server_core.go +++ b/internal/rpc/server_core.go @@ -80,6 +80,8 @@ const ( type MutationEvent struct { Type string // One of the Mutation* constants IssueID string // e.g., "bd-42" + Title string // Issue title for display context (may be empty for some operations) + Assignee string // Issue assignee for display context (may be empty) Timestamp time.Time // Optional metadata for richer events (used by status, bonded, etc.) OldStatus string `json:"old_status,omitempty"` // Previous status (for status events) @@ -138,10 +140,13 @@ func NewServer(socketPath string, store storage.Storage, workspacePath string, d // emitMutation sends a mutation event to the daemon's event-driven loop. // Non-blocking: drops event if channel is full (sync will happen eventually). // Also stores in recent mutations buffer for polling. -func (s *Server) emitMutation(eventType, issueID string) { +// Title and assignee provide context for activity feeds; pass empty strings if unknown. +func (s *Server) emitMutation(eventType, issueID, title, assignee string) { s.emitRichMutation(MutationEvent{ - Type: eventType, - IssueID: issueID, + Type: eventType, + IssueID: issueID, + Title: title, + Assignee: assignee, }) } diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index a7100bbc..7a680962 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -350,7 +350,7 @@ func (s *Server) handleCreate(req *Request) Response { } // Emit mutation event for event-driven daemon - s.emitMutation(MutationCreate, issue.ID) + s.emitMutation(MutationCreate, issue.ID, issue.Title, issue.Assignee) data, _ := json.Marshal(issue) return Response{ @@ -470,11 +470,13 @@ func (s *Server) handleUpdate(req *Request) Response { s.emitRichMutation(MutationEvent{ Type: MutationStatus, IssueID: updateArgs.ID, + Title: issue.Title, + Assignee: issue.Assignee, OldStatus: string(issue.Status), NewStatus: *updateArgs.Status, }) } else { - s.emitMutation(MutationUpdate, updateArgs.ID) + s.emitMutation(MutationUpdate, updateArgs.ID, issue.Title, issue.Assignee) } } @@ -544,6 +546,8 @@ func (s *Server) handleClose(req *Request) Response { s.emitRichMutation(MutationEvent{ Type: MutationStatus, IssueID: closeArgs.ID, + Title: issue.Title, + Assignee: issue.Assignee, OldStatus: oldStatus, NewStatus: "closed", }) @@ -640,7 +644,7 @@ func (s *Server) handleDelete(req *Request) Response { } // Emit mutation event for event-driven daemon - s.emitMutation(MutationDelete, issueID) + s.emitMutation(MutationDelete, issueID, issue.Title, issue.Assignee) deletedCount++ } @@ -1421,7 +1425,7 @@ func (s *Server) handleGateCreate(req *Request) Response { } // Emit mutation event - s.emitMutation(MutationCreate, gate.ID) + s.emitMutation(MutationCreate, gate.ID, gate.Title, gate.Assignee) data, _ := json.Marshal(GateCreateResult{ID: gate.ID}) return Response{ @@ -1702,7 +1706,7 @@ func (s *Server) handleGateWait(req *Request) Response { } // Emit mutation event - s.emitMutation(MutationUpdate, gateID) + s.emitMutation(MutationUpdate, gateID, gate.Title, gate.Assignee) } data, _ := json.Marshal(GateWaitResult{AddedCount: addedCount}) diff --git a/internal/rpc/server_labels_deps_comments.go b/internal/rpc/server_labels_deps_comments.go index e48f90ef..f0510131 100644 --- a/internal/rpc/server_labels_deps_comments.go +++ b/internal/rpc/server_labels_deps_comments.go @@ -41,7 +41,8 @@ func (s *Server) handleDepAdd(req *Request) Response { } // Emit mutation event for event-driven daemon - s.emitMutation(MutationUpdate, depArgs.FromID) + // Title/assignee empty for dependency operations (would require extra lookup) + s.emitMutation(MutationUpdate, depArgs.FromID, "", "") return Response{Success: true} } @@ -73,7 +74,8 @@ func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc } // Emit mutation event for event-driven daemon - s.emitMutation(MutationUpdate, issueID) + // Title/assignee empty for simple store operations (would require extra lookup) + s.emitMutation(MutationUpdate, issueID, "", "") return Response{Success: true} } @@ -147,7 +149,8 @@ func (s *Server) handleCommentAdd(req *Request) Response { } // Emit mutation event for event-driven daemon - s.emitMutation(MutationComment, commentArgs.ID) + // Title/assignee empty for comment operations (would require extra lookup) + s.emitMutation(MutationComment, commentArgs.ID, "", "") data, _ := json.Marshal(comment) return Response{ diff --git a/internal/rpc/server_mutations_test.go b/internal/rpc/server_mutations_test.go index 2b2c269d..4f111773 100644 --- a/internal/rpc/server_mutations_test.go +++ b/internal/rpc/server_mutations_test.go @@ -13,7 +13,7 @@ func TestEmitMutation(t *testing.T) { server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Emit a mutation - server.emitMutation(MutationCreate, "bd-123") + server.emitMutation(MutationCreate, "bd-123", "Test Issue", "") // Check that mutation was stored in buffer mutations := server.GetRecentMutations(0) @@ -45,14 +45,14 @@ func TestGetRecentMutations_TimestampFiltering(t *testing.T) { server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Emit mutations with delays - server.emitMutation(MutationCreate, "bd-1") + server.emitMutation(MutationCreate, "bd-1", "Issue 1", "") time.Sleep(10 * time.Millisecond) checkpoint := time.Now().UnixMilli() time.Sleep(10 * time.Millisecond) - server.emitMutation(MutationUpdate, "bd-2") - server.emitMutation(MutationUpdate, "bd-3") + server.emitMutation(MutationUpdate, "bd-2", "Issue 2", "") + server.emitMutation(MutationUpdate, "bd-3", "Issue 3", "") // Get mutations after checkpoint mutations := server.GetRecentMutations(checkpoint) @@ -82,7 +82,7 @@ func TestGetRecentMutations_CircularBuffer(t *testing.T) { // Emit more than maxMutationBuffer (100) mutations for i := 0; i < 150; i++ { - server.emitMutation(MutationCreate, "bd-"+string(rune(i))) + server.emitMutation(MutationCreate, "bd-"+string(rune(i)), "", "") time.Sleep(time.Millisecond) // Ensure different timestamps } @@ -110,7 +110,7 @@ func TestGetRecentMutations_ConcurrentAccess(t *testing.T) { // Writer goroutine go func() { for i := 0; i < 50; i++ { - server.emitMutation(MutationUpdate, "bd-write") + server.emitMutation(MutationUpdate, "bd-write", "", "") time.Sleep(time.Millisecond) } done <- true @@ -141,11 +141,11 @@ func TestHandleGetMutations(t *testing.T) { server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") // Emit some mutations - server.emitMutation(MutationCreate, "bd-1") + server.emitMutation(MutationCreate, "bd-1", "Issue 1", "") time.Sleep(10 * time.Millisecond) checkpoint := time.Now().UnixMilli() time.Sleep(10 * time.Millisecond) - server.emitMutation(MutationUpdate, "bd-2") + server.emitMutation(MutationUpdate, "bd-2", "Issue 2", "") // Create RPC request args := GetMutationsArgs{Since: checkpoint} @@ -213,7 +213,7 @@ func TestMutationEventTypes(t *testing.T) { } for _, mutationType := range types { - server.emitMutation(mutationType, "bd-test") + server.emitMutation(mutationType, "bd-test", "", "") } mutations := server.GetRecentMutations(0) @@ -305,7 +305,7 @@ func TestMutationTimestamps(t *testing.T) { server := NewServer("/tmp/test.sock", store, "/tmp", "/tmp/test.db") before := time.Now() - server.emitMutation(MutationCreate, "bd-123") + server.emitMutation(MutationCreate, "bd-123", "Test Issue", "") after := time.Now() mutations := server.GetRecentMutations(0) @@ -327,7 +327,7 @@ func TestEmitMutation_NonBlocking(t *testing.T) { // Fill the buffer (default size is 512 from BEADS_MUTATION_BUFFER or default) for i := 0; i < 600; i++ { // This should not block even when channel is full - server.emitMutation(MutationCreate, "bd-test") + server.emitMutation(MutationCreate, "bd-test", "", "") } // Verify mutations were still stored in recent buffer