feat: Filter agent session molecule noise from activity feed

Agent session molecules (gt-gastown-crew-joe, gt-gastown-witness, etc.)
update frequently for status tracking, creating noisy entries in the
activity feed. This change:

- Adds IsAgentSessionBead() to identify agent session beads
- Filters out "update" events for agent sessions from the event feed
- Still updates the agent tree so status is visible there
- Still shows create/complete/fail/delete events for agents

The filtering happens in addEvent() in the TUI feed model. Agent session
updates are identified by parsing the bead ID pattern and checking for
known agent roles (mayor, deacon, witness, refinery, crew, polecat).

Resolves: gt-sb6m4

🤖 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-29 23:36:29 -08:00
parent e1fed7f72e
commit f9e820985d
3 changed files with 117 additions and 8 deletions

View File

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

View File

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

View File

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