Slice 3: activity feed and active project (SSE + MCP tools)
Add internal/activity (thread-safe active project + bounded event feed with subscriber fan-out). Service exposes active-project/activity methods and takes the feed. New MCP tools get_active_project/set_active_project/get_activity and HTTP endpoints GET/POST /api/active-project, /api/activity, and /events (SSE). New <activity-feed> component updates live via EventSource; <repo-list> sets the active project on selection. Verified end-to-end in the running app: user actions push live events and set the active project (actor=user), all readable by Claude over MCP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,10 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"gitmanager/internal/activity"
|
||||
"gitmanager/internal/git"
|
||||
"gitmanager/internal/repos"
|
||||
)
|
||||
@@ -17,11 +19,13 @@ import (
|
||||
type Service struct {
|
||||
git *git.CLI
|
||||
index *repos.Index
|
||||
feed *activity.Feed
|
||||
}
|
||||
|
||||
// New builds a Service over the git boundary and the scanner's repo index.
|
||||
func New(g *git.CLI, index *repos.Index) *Service {
|
||||
return &Service{git: g, index: index}
|
||||
// New builds a Service over the git boundary, the scanner's repo index, and the
|
||||
// activity feed.
|
||||
func New(g *git.CLI, index *repos.Index, feed *activity.Feed) *Service {
|
||||
return &Service{git: g, index: index, feed: feed}
|
||||
}
|
||||
|
||||
// ListRepos returns a snapshot of every discovered repository.
|
||||
@@ -45,3 +49,40 @@ func (s *Service) RepoDetail(ctx context.Context, path string) (repos.Detail, bo
|
||||
}
|
||||
return repos.BuildDetail(ctx, s.git, base), true
|
||||
}
|
||||
|
||||
// --- Activity & active project (§8.2) --------------------------------------
|
||||
|
||||
// ActiveProject returns the current active project path ("" if none).
|
||||
func (s *Service) ActiveProject() string {
|
||||
return s.feed.ActiveProject()
|
||||
}
|
||||
|
||||
// SetActiveProject makes path the active project (or clears it when empty). It
|
||||
// rejects a path that is not an indexed repository — the active project must be
|
||||
// a real repo (§1.3). Returns the recorded event and whether it changed.
|
||||
func (s *Service) SetActiveProject(actor activity.Actor, path string) (activity.Event, bool, error) {
|
||||
if path != "" {
|
||||
path = filepath.Clean(path)
|
||||
if _, ok := s.index.Get(path); !ok {
|
||||
return activity.Event{}, false, fmt.Errorf("unknown repository %q", path)
|
||||
}
|
||||
}
|
||||
ev, changed := s.feed.SetActiveProject(actor, path)
|
||||
return ev, changed, nil
|
||||
}
|
||||
|
||||
// RecordActivity appends an arbitrary event to the feed.
|
||||
func (s *Service) RecordActivity(actor activity.Actor, kind, repo, detail string) activity.Event {
|
||||
return s.feed.Record(actor, kind, repo, detail)
|
||||
}
|
||||
|
||||
// Activity returns up to limit recent events, oldest first.
|
||||
func (s *Service) Activity(limit int) []activity.Event {
|
||||
return s.feed.Events(limit)
|
||||
}
|
||||
|
||||
// SubscribeActivity returns a channel of future events plus an unsubscribe func
|
||||
// the caller must invoke when done.
|
||||
func (s *Service) SubscribeActivity() (<-chan activity.Event, func()) {
|
||||
return s.feed.Subscribe()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user