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:
+65
-2
@@ -5,6 +5,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"gitmanager/internal/activity"
|
||||
"gitmanager/internal/config"
|
||||
"gitmanager/internal/git"
|
||||
"gitmanager/internal/logging"
|
||||
@@ -51,8 +53,11 @@ func main() {
|
||||
go scanner.Run(scanCtx)
|
||||
log.Info("scanner started", "roots", cfg.RepoRoots, "interval", cfg.ScanInterval.String(), "fetch", cfg.ScanFetchEnabled)
|
||||
|
||||
// Coordination state: active project + activity feed (§8.2).
|
||||
feed := activity.New(log, 200)
|
||||
|
||||
// The one service layer both the HTTP API and the MCP server call (§1.7).
|
||||
svc := service.New(g, scanner.Index)
|
||||
svc := service.New(g, scanner.Index, feed)
|
||||
|
||||
tmpl, err := render.New("web/templates")
|
||||
if err != nil {
|
||||
@@ -95,7 +100,65 @@ func main() {
|
||||
return c.JSON(http.StatusOK, detail)
|
||||
})
|
||||
|
||||
// MCP server — Claude connects here as a custom connector (§8.1). Same
|
||||
// Active project + activity (§8.2).
|
||||
e.GET("/api/active-project", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{"path": svc.ActiveProject()})
|
||||
})
|
||||
e.POST("/api/active-project", func(c echo.Context) error {
|
||||
var body struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := c.Bind(&body); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||
}
|
||||
// A user action in the UI (actor=user) — distinct from Claude's own switches.
|
||||
if _, _, err := svc.SetActiveProject(activity.ActorUser, body.Path); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"path": svc.ActiveProject()})
|
||||
})
|
||||
e.GET("/api/activity", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, svc.Activity(0))
|
||||
})
|
||||
|
||||
// SSE stream of activity events for live UI (§8.2).
|
||||
e.GET("/events", func(c echo.Context) error {
|
||||
w := c.Response()
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Flush()
|
||||
|
||||
ch, unsub := svc.SubscribeActivity()
|
||||
defer unsub()
|
||||
|
||||
keepalive := time.NewTicker(25 * time.Second)
|
||||
defer keepalive.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request().Context().Done():
|
||||
return nil
|
||||
case <-keepalive.C:
|
||||
if _, err := w.Write([]byte(": ping\n\n")); err != nil {
|
||||
return nil
|
||||
}
|
||||
w.Flush()
|
||||
case ev := <-ch:
|
||||
data, err := json.Marshal(ev)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write([]byte("event: activity\ndata: " + string(data) + "\n\n")); err != nil {
|
||||
return nil
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// MCP server — Claude connects via the local stdio bridge (§8.1). Same
|
||||
// service layer as the HTTP API (§1.7); localhost-bound like everything else.
|
||||
mcpSrv := mcpserver.NewServer(svc, "0.1.0")
|
||||
e.Any("/mcp", echo.WrapHandler(mcpserver.Handler(mcpSrv)))
|
||||
|
||||
Reference in New Issue
Block a user