fix: DM sidebar shows conversation partners, not owned agents
Release / Build darwin/amd64 (push) Canceled after 0s
Release / Build linux/amd64 (push) Canceled after 0s
Release / Build darwin/arm64 (push) Canceled after 0s
Release / Build linux/arm64 (push) Canceled after 0s
Release / Generate Homebrew Formula (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s
Release / Docker Image (push) Canceled after 0s
Release / Publish to MCP Registry (push) Canceled after 0s

- New API: GET /api/dm/partners — returns DM conversation partners
  ordered by most recent message, with unread counts
- Sidebar DM section now shows actual conversation partners (agents
  you've exchanged messages with) instead of owned agents
- Each partner shows name, unread badge, clickable to /dm/{name}
- Fixes issue where all DMs were shown mixed in one view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Algis Dumbris
2026-03-26 09:19:22 +02:00
co-authored by Claude Opus 4.6
parent 7848911a5f
commit 3b97429f20
5 changed files with 133 additions and 34 deletions
+104
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"log/slog"
"net/http"
"sort"
"strconv"
"strings"
"time"
@@ -581,6 +582,109 @@ func (h *MessagesHandler) DMMessages(w http.ResponseWriter, r *http.Request) {
})
}
// DMPartners returns a list of agents the user has DM conversations with,
// ordered by most recent message. Each entry includes the peer agent name,
// last message preview, timestamp, and unread count.
func (h *MessagesHandler) DMPartners(w http.ResponseWriter, r *http.Request) {
ownerID, ok := OwnerIDFromContext(r.Context())
if !ok {
writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required"))
return
}
ownedAgents, err := h.agentService.ListAgents(r.Context(), ownerID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, errorBody("server_error", "Failed to list agents"))
return
}
if len(ownedAgents) == 0 {
writeJSON(w, http.StatusOK, map[string]any{"partners": []any{}})
return
}
ownedNames := make(map[string]bool, len(ownedAgents))
for _, a := range ownedAgents {
ownedNames[a.Name] = true
}
// Get recent DMs for all owned agents
type partner struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
LastMessage string `json:"last_message"`
LastTime string `json:"last_time"`
Unread int `json:"unread"`
}
partnerMap := make(map[string]*partner)
for _, agent := range ownedAgents {
opts := messaging.ReadOptions{
Limit: 200,
IncludeRead: true,
}
result, err := h.msgService.ReadInbox(r.Context(), agent.Name, opts)
if err != nil {
continue
}
for _, msg := range result.Messages {
if msg.ChannelID != nil {
continue // skip channel messages
}
// Determine the peer (the other party in the DM)
peer := msg.FromAgent
if ownedNames[peer] {
peer = msg.ToAgent
}
if peer == "" || ownedNames[peer] {
continue // skip self-to-self
}
existing, exists := partnerMap[peer]
if !exists {
partnerMap[peer] = &partner{
Name: peer,
DisplayName: peer,
LastMessage: truncateStr(msg.Body, 80),
LastTime: msg.CreatedAt.Format(time.RFC3339),
Unread: 0,
}
existing = partnerMap[peer]
}
// Track newest message
lt, _ := time.Parse(time.RFC3339, existing.LastTime)
if msg.CreatedAt.After(lt) {
existing.LastMessage = truncateStr(msg.Body, 80)
existing.LastTime = msg.CreatedAt.Format(time.RFC3339)
}
// Count unread (pending messages TO owned agents)
if ownedNames[msg.ToAgent] && (msg.Status == "pending" || msg.Status == "processing") {
existing.Unread++
}
}
}
// Resolve display names
for peer, p := range partnerMap {
if a, err := h.agentService.GetAgent(r.Context(), peer); err == nil {
p.DisplayName = a.DisplayName
}
}
// Sort by last_time descending
partners := make([]*partner, 0, len(partnerMap))
for _, p := range partnerMap {
partners = append(partners, p)
}
sort.Slice(partners, func(i, j int) bool {
return partners[i].LastTime > partners[j].LastTime
})
writeJSON(w, http.StatusOK, map[string]any{"partners": partners})
}
func (h *MessagesHandler) isAgentOwnedBy(r *http.Request, agentName string, ownerID int64) bool {
if agentName == "" {
return false
+1
View File
@@ -131,6 +131,7 @@ func NewRouterWithConfig(cfg RouterConfig) chi.Router {
r.Delete("/api/agents/{name}", agentsHandler.DeleteAgent)
r.Post("/api/agents/{name}/revoke-key", agentsHandler.RevokeKey)
r.Get("/api/agents/{name}/messages", messagesHandler.DMMessages)
r.Get("/api/dm/partners", messagesHandler.DMPartners)
// Notifications
r.Get("/api/notifications/unread", notificationsHandler.UnreadCounts)