diff --git a/internal/beads/beads.go b/internal/beads/beads.go index fa3b2105..4555c98e 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -822,3 +822,20 @@ func ParseAgentBeadID(id string) (rig, role, name string, ok bool) { return "", "", "", false } } + +// IsAgentSessionBead returns true if the bead ID represents an agent session molecule. +// Agent session beads follow patterns like gt-mayor, gt-gastown-witness, gt-gastown-crew-joe. +// These are used to track agent state and update frequently, which can create noise. +func IsAgentSessionBead(beadID string) bool { + _, role, _, ok := ParseAgentBeadID(beadID) + if !ok { + return false + } + // Known agent roles + switch role { + case "mayor", "deacon", "witness", "refinery", "crew", "polecat": + return true + default: + return false + } +} diff --git a/internal/beads/beads_test.go b/internal/beads/beads_test.go index 9ec8c7ca..ad2d1871 100644 --- a/internal/beads/beads_test.go +++ b/internal/beads/beads_test.go @@ -967,3 +967,81 @@ func TestResolveBeadsDir(t *testing.T) { } }) } + +func TestParseAgentBeadID(t *testing.T) { + tests := []struct { + input string + wantRig string + wantRole string + wantName string + wantOK bool + }{ + // Town-level agents + {"gt-mayor", "", "mayor", "", true}, + {"gt-deacon", "", "deacon", "", true}, + // Rig-level singletons + {"gt-gastown-witness", "gastown", "witness", "", true}, + {"gt-gastown-refinery", "gastown", "refinery", "", true}, + // Rig-level named agents + {"gt-gastown-crew-joe", "gastown", "crew", "joe", true}, + {"gt-gastown-crew-max", "gastown", "crew", "max", true}, + {"gt-gastown-polecat-capable", "gastown", "polecat", "capable", true}, + // Names with hyphens + {"gt-gastown-polecat-my-agent", "gastown", "polecat", "my-agent", true}, + // Parseable but not valid agent roles (IsAgentSessionBead will reject) + {"gt-abc123", "", "abc123", "", true}, // Parses as town-level but not valid role + // Truly invalid patterns + {"bd-gastown-crew-joe", "", "", "", false}, // Wrong prefix + {"", "", "", "", false}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + rig, role, name, ok := ParseAgentBeadID(tt.input) + if ok != tt.wantOK { + t.Errorf("ParseAgentBeadID(%q) ok = %v, want %v", tt.input, ok, tt.wantOK) + return + } + if rig != tt.wantRig { + t.Errorf("ParseAgentBeadID(%q) rig = %q, want %q", tt.input, rig, tt.wantRig) + } + if role != tt.wantRole { + t.Errorf("ParseAgentBeadID(%q) role = %q, want %q", tt.input, role, tt.wantRole) + } + if name != tt.wantName { + t.Errorf("ParseAgentBeadID(%q) name = %q, want %q", tt.input, name, tt.wantName) + } + }) + } +} + +func TestIsAgentSessionBead(t *testing.T) { + tests := []struct { + beadID string + want bool + }{ + // Agent session beads (should return true) + {"gt-mayor", true}, + {"gt-deacon", true}, + {"gt-gastown-witness", true}, + {"gt-gastown-refinery", true}, + {"gt-gastown-crew-joe", true}, + {"gt-gastown-polecat-capable", true}, + // Regular work beads (should return false) + {"gt-abc123", false}, + {"gt-sb6m4", false}, + {"gt-u7dxq", false}, + // Invalid beads + {"bd-abc123", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.beadID, func(t *testing.T) { + got := IsAgentSessionBead(tt.beadID) + if got != tt.want { + t.Errorf("IsAgentSessionBead(%q) = %v, want %v", tt.beadID, got, tt.want) + } + }) + } +} diff --git a/internal/tui/feed/model.go b/internal/tui/feed/model.go index d0e43278..24bb737e 100644 --- a/internal/tui/feed/model.go +++ b/internal/tui/feed/model.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/steveyegge/gastown/internal/beads" ) // Panel represents which panel has focus @@ -252,14 +253,7 @@ func (m *Model) updateViewContent() { // addEvent adds an event and updates the agent tree func (m *Model) addEvent(e Event) { - m.events = append(m.events, e) - - // Keep max 1000 events - if len(m.events) > 1000 { - m.events = m.events[len(m.events)-1000:] - } - - // Update agent tree + // Update agent tree first (always do this for status tracking) if e.Rig != "" { rig, ok := m.rigs[e.Rig] if !ok { @@ -287,6 +281,26 @@ func (m *Model) addEvent(e Event) { } } + // Filter out noisy agent session updates from the event feed. + // Agent session molecules (like gt-gastown-crew-joe) update frequently + // for status tracking. These updates are visible in the agent tree, + // so we don't need to clutter the event feed with them. + // We still show create/complete/fail/delete events for agent sessions. + if e.Type == "update" && beads.IsAgentSessionBead(e.Target) { + // Skip adding to event feed, but still refresh the view + // (agent tree was updated above) + m.updateViewContent() + return + } + + // Add to event feed + m.events = append(m.events, e) + + // Keep max 1000 events + if len(m.events) > 1000 { + m.events = m.events[len(m.events)-1000:] + } + m.updateViewContent() }