feat: add Slack-like channel and DM messaging UX
- Channel pages now show message feed with compose bar instead of
just member lists. Messages display with agent avatars, names,
and timestamps. Collapsible info panel shows channel details.
- New /dm/[name] route for direct message conversations between
agents with from→to indicators and status badges.
- Sidebar DM links now navigate to /dm/{name} instead of agent
edit forms.
- Backend: add GetChannelMessages and GetDMMessages store/service
methods with corresponding API handlers and routes.
- Makefile: build target now depends on web target so binary
always embeds latest UI assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cad0138337
commit
f5ae132ef4
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -198,6 +199,41 @@ func (h *ChannelsHandler) JoinChannel(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "joined"})
|
||||
}
|
||||
|
||||
// ChannelMessages handles GET /api/channels/{name}/messages.
|
||||
func (h *ChannelsHandler) ChannelMessages(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := OwnerIDFromContext(r.Context())
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
name := chi.URLParam(r, "name")
|
||||
ch, err := h.channelService.GetChannelByName(r.Context(), name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, errorBody("not_found", "Channel not found"))
|
||||
return
|
||||
}
|
||||
|
||||
limit := 100
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
msgs, err := h.msgService.GetChannelMessages(r.Context(), ch.ID, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("get channel messages failed", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to get messages"))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"messages": msgs,
|
||||
"total": len(msgs),
|
||||
})
|
||||
}
|
||||
|
||||
// LeaveChannel handles POST /api/channels/{name}/leave.
|
||||
func (h *ChannelsHandler) LeaveChannel(w http.ResponseWriter, r *http.Request) {
|
||||
ownerID, ok := OwnerIDFromContext(r.Context())
|
||||
|
||||
@@ -425,6 +425,51 @@ func (h *MessagesHandler) GetReplies(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// DMMessages handles GET /api/agents/{name}/messages — returns DM messages with a specific agent.
|
||||
func (h *MessagesHandler) DMMessages(w http.ResponseWriter, r *http.Request) {
|
||||
ownerID, ok := OwnerIDFromContext(r.Context())
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
peerAgent := chi.URLParam(r, "name")
|
||||
|
||||
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
|
||||
if err != nil {
|
||||
h.logger.Error("list agents failed", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to list agents"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(ownedAgents) == 0 {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"messages": []*messaging.Message{}, "total": 0})
|
||||
return
|
||||
}
|
||||
|
||||
agentNames := make([]string, len(ownedAgents))
|
||||
for i, a := range ownedAgents {
|
||||
agentNames[i] = a.Name
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
msgs, err := h.msgService.GetDMMessages(r.Context(), agentNames, peerAgent, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("get dm messages failed", "error", err)
|
||||
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to get messages"))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"messages": msgs,
|
||||
"total": len(msgs),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MessagesHandler) isAgentOwnedBy(r *http.Request, agentName string, ownerID int64) bool {
|
||||
if agentName == "" {
|
||||
return false
|
||||
|
||||
@@ -100,6 +100,7 @@ func NewRouterWithConfig(cfg RouterConfig) chi.Router {
|
||||
r.Post("/api/agents", agentsHandler.RegisterAgent)
|
||||
r.Delete("/api/agents/{name}", agentsHandler.DeleteAgent)
|
||||
r.Post("/api/agents/{name}/revoke-key", agentsHandler.RevokeKey)
|
||||
r.Get("/api/agents/{name}/messages", messagesHandler.DMMessages)
|
||||
})
|
||||
|
||||
// API Keys
|
||||
@@ -124,6 +125,7 @@ func NewRouterWithConfig(cfg RouterConfig) chi.Router {
|
||||
r.Get("/api/channels", channelsHandler.ListChannels)
|
||||
r.Get("/api/channels/{name}", channelsHandler.GetChannel)
|
||||
r.Post("/api/channels", channelsHandler.CreateChannel)
|
||||
r.Get("/api/channels/{name}/messages", channelsHandler.ChannelMessages)
|
||||
r.Post("/api/channels/{name}/join", channelsHandler.JoinChannel)
|
||||
r.Post("/api/channels/{name}/leave", channelsHandler.LeaveChannel)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user