From f2a94cfac926924cdd923252eb9866961e869f06 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 08:27:51 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20session=20browser=20=E2=80=94=20vie?= =?UTF-8?q?w=20all=20agent=20sessions=20on=20a=20canvas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - New RPC: ListCanvasSessions — returns all sessions for a canvas with user name, status, and last activity time - Joins agent_sessions with accounts for user info Frontend: - 'All sessions' button at top of agent chat - Session list view with avatar, name, status, time ago - Read-only view of other users' sessions (composer hidden) - Back navigation between views Signed-off-by: Bender Rodriguez --- pkg/agents/service.go | 43 +++++++++++ pkg/grpc/actions/agents/common.go | 1 + .../actions/agents/list_canvas_sessions.go | 51 ++++++++++++ pkg/grpc/agent_service.go | 8 ++ protos/agents.proto | 28 +++++++ .../CanvasToolSidebar/AgentTabPanel.tsx | 74 ++++++++++++++++-- .../CanvasToolSidebar/SessionListView.tsx | 77 +++++++++++++++++++ web_src/src/hooks/useAgentChats.ts | 32 ++++++++ 8 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 pkg/grpc/actions/agents/list_canvas_sessions.go create mode 100644 web_src/src/components/CanvasToolSidebar/SessionListView.tsx diff --git a/pkg/agents/service.go b/pkg/agents/service.go index 231b910a54..14f555972e 100644 --- a/pkg/agents/service.go +++ b/pkg/agents/service.go @@ -375,3 +375,46 @@ func sessionTitle(organizationID, canvasID uuid.UUID) string { } return org.Name + " - " + canvas.Name } + +// CanvasSessionInfo holds session + user info for the session browser. +type CanvasSessionInfo struct { + SessionID string + UserID string + UserName string + Status string + LastActiveAt *time.Time +} + +// ListCanvasSessions returns all agent sessions for a canvas with user info. +func (s *Service) ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]CanvasSessionInfo, error) { + var results []struct { + SessionID string + UserID string + UserName string + Status string + LastActiveAt *time.Time + } + + err := database.Conn(). + Table("agent_sessions"). + Select("agent_sessions.id as session_id, agent_sessions.user_id, accounts.name as user_name, agent_sessions.status, agent_sessions.last_active_at"). + Joins("LEFT JOIN accounts ON accounts.id = agent_sessions.user_id"). + Where("agent_sessions.organization_id = ? AND agent_sessions.canvas_id = ?", organizationID, canvasID). + Order("agent_sessions.updated_at DESC"). + Scan(&results).Error + if err != nil { + return nil, fmt.Errorf("list canvas sessions: %w", err) + } + + entries := make([]CanvasSessionInfo, len(results)) + for i, r := range results { + entries[i] = CanvasSessionInfo{ + SessionID: r.SessionID, + UserID: r.UserID, + UserName: r.UserName, + Status: r.Status, + LastActiveAt: r.LastActiveAt, + } + } + return entries, nil +} diff --git a/pkg/grpc/actions/agents/common.go b/pkg/grpc/actions/agents/common.go index 10515d7a10..78b041fe8f 100644 --- a/pkg/grpc/actions/agents/common.go +++ b/pkg/grpc/actions/agents/common.go @@ -22,6 +22,7 @@ type AgentsService interface { SendMessage(ctx context.Context, organizationID, userID, sessionID uuid.UUID, content string, mode ...string) (*models.AgentSessionMessage, error) InterruptSession(ctx context.Context, organizationID, userID, sessionID uuid.UUID) error DefineOutcome(ctx context.Context, organizationID, userID, sessionID uuid.UUID, description, rubric string, maxIterations int) error + ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]agentservice.CanvasSessionInfo, error) } func agentModeFromProto(mode pb.AgentMode) string { diff --git a/pkg/grpc/actions/agents/list_canvas_sessions.go b/pkg/grpc/actions/agents/list_canvas_sessions.go new file mode 100644 index 0000000000..96e48e53ca --- /dev/null +++ b/pkg/grpc/actions/agents/list_canvas_sessions.go @@ -0,0 +1,51 @@ +package agents + +import ( + "context" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + pb "github.com/superplanehq/superplane/pkg/protos/agents" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func ListCanvasSessions(ctx context.Context, svc AgentsService, orgID, userID string, req *pb.ListCanvasSessionsRequest) (*pb.ListCanvasSessionsResponse, error) { + org, _, err := parseOrgUser(orgID, userID) + if err != nil { + return nil, err + } + + canvasID, err := uuid.Parse(req.CanvasId) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid canvas id") + } + + if err := ensureCanvas(org, canvasID); err != nil { + return nil, err + } + + entries, err := svc.ListCanvasSessions(org, canvasID) + if err != nil { + log.WithError(err).Error("failed to list canvas sessions") + return nil, status.Error(codes.Internal, "failed to list canvas sessions") + } + + sessions := make([]*pb.CanvasSessionInfo, len(entries)) + for i, e := range entries { + var lastActivity *timestamppb.Timestamp + if e.LastActiveAt != nil { + lastActivity = timestamppb.New(*e.LastActiveAt) + } + sessions[i] = &pb.CanvasSessionInfo{ + Id: e.SessionID, + UserId: e.UserID, + UserName: e.UserName, + Status: e.Status, + LastActivityAt: lastActivity, + } + } + + return &pb.ListCanvasSessionsResponse{Sessions: sessions}, nil +} diff --git a/pkg/grpc/agent_service.go b/pkg/grpc/agent_service.go index 58c8320223..a0984f4702 100644 --- a/pkg/grpc/agent_service.go +++ b/pkg/grpc/agent_service.go @@ -80,3 +80,11 @@ func (s *AgentsService) DefineAgentOutcome(ctx context.Context, req *pb.DefineAg } return agentsActions.DefineAgentOutcome(ctx, s.service, orgID, userID, req) } + +func (s *AgentsService) ListCanvasSessions(ctx context.Context, req *pb.ListCanvasSessionsRequest) (*pb.ListCanvasSessionsResponse, error) { + orgID, userID, err := s.requestContext(ctx) + if err != nil { + return nil, err + } + return agentsActions.ListCanvasSessions(ctx, s.service, orgID, userID, req) +} diff --git a/protos/agents.proto b/protos/agents.proto index 537ad0e3fa..49137bef2e 100644 --- a/protos/agents.proto +++ b/protos/agents.proto @@ -81,6 +81,17 @@ service Agents { tags: "Agent"; }; } + + rpc ListCanvasSessions(ListCanvasSessionsRequest) returns (ListCanvasSessionsResponse) { + option (google.api.http) = { + get: "/api/v1/agents/canvases/{canvas_id}/sessions" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "List all agent sessions for a canvas"; + description: "Returns all user sessions for the given canvas, including user info and last activity."; + tags: "Agent"; + }; + } } enum AgentMode { @@ -135,6 +146,23 @@ message DefineAgentOutcomeRequest { message DefineAgentOutcomeResponse {} +message ListCanvasSessionsRequest { + string canvas_id = 1; +} + +message ListCanvasSessionsResponse { + repeated CanvasSessionInfo sessions = 1; +} + +message CanvasSessionInfo { + string id = 1; + string user_id = 2; + string user_name = 3; + string user_avatar_url = 4; + string status = 5; + google.protobuf.Timestamp last_activity_at = 6; +} + message AgentChatInfo { string id = 1; string canvas_id = 2; diff --git a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx index 5c429614ad..f3dd639fde 100644 --- a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx +++ b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx @@ -2,6 +2,8 @@ import { Loader2 } from "lucide-react"; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import type { AgentMode } from "@/components/AgentSidebar/agentMode"; import { AccountContext } from "@/contexts/accountContextState"; +import { SessionListView } from "./SessionListView"; +import { Users } from "lucide-react"; import { createSystemMessage } from "@/components/AgentSidebar/systemMessages"; import { ChatComposer } from "@/components/AgentSidebar/ChatComposer"; import { useChatScroll } from "@/components/AgentSidebar/useChatScroll"; @@ -12,6 +14,7 @@ import type { RubricCategory } from "@/components/AgentSidebar/widgets/parser"; import { useAgentChatMessages, useCanvasAgentChat, + useCanvasSessions, useDefineAgentOutcome, useInterruptAgentChat, useSendAgentChatMessage, @@ -39,6 +42,7 @@ type ChatConversationProps = { agentMode: AgentMode; onModeSwitch: (mode: AgentMode) => void; isEditing: boolean; + readOnly?: boolean; }; type DraftActionsBarProps = { @@ -67,6 +71,9 @@ export function AgentTabPanel({ toolSidebarState }: { toolSidebarState: CanvasTo const chatId = chatQuery.data?.id ?? null; const { account } = useContext(AccountContext); const firstName = account?.name?.split(" ")[0] ?? "there"; + const [viewMode, setViewMode] = useState<"my-session" | "session-list" | "viewing-session">("my-session"); + const [viewingSessionId, setViewingSessionId] = useState(null); + const sessionsQuery = useCanvasSessions(canvasId, organizationId, viewMode === "session-list"); if (chatQuery.isLoading || !chatId) { return ( @@ -85,15 +92,63 @@ export function AgentTabPanel({ toolSidebarState }: { toolSidebarState: CanvasTo ); } + if (viewMode === "session-list") { + return ( + { + setViewingSessionId(sessionId); + setViewMode("viewing-session"); + }} + onBack={() => setViewMode("my-session")} + /> + ); + } + + if (viewMode === "viewing-session" && viewingSessionId) { + return ( +
+
+ +
+ +
+ ); + } + return ( - +
+
+ +
+ +
); } @@ -104,6 +159,7 @@ function ChatConversation({ agentMode, onModeSwitch, isEditing, + readOnly, }: ChatConversationProps) { const messagesQuery = useAgentChatMessages(chatId, organizationId, true); const sendMutation = useSendAgentChatMessage(organizationId, canvasId); @@ -209,6 +265,7 @@ function ChatConversation({ onVersionPublished={() => setOutcomeState(null)} /> + {!readOnly && ( + )} ); } diff --git a/web_src/src/components/CanvasToolSidebar/SessionListView.tsx b/web_src/src/components/CanvasToolSidebar/SessionListView.tsx new file mode 100644 index 0000000000..076e4cf137 --- /dev/null +++ b/web_src/src/components/CanvasToolSidebar/SessionListView.tsx @@ -0,0 +1,77 @@ +import { ArrowLeft, Circle } from "lucide-react"; +import type { CanvasSession } from "@/hooks/useAgentChats"; + +interface SessionListViewProps { + sessions: CanvasSession[]; + currentUserId: string; + onSelectSession: (sessionId: string) => void; + onBack: () => void; +} + +export function SessionListView({ sessions, currentUserId, onSelectSession, onBack }: SessionListViewProps) { + return ( +
+
+ +
+
+
+

Sessions on this canvas

+ {sessions.length === 0 ? ( +

No sessions yet.

+ ) : ( +
+ {sessions.map((session) => ( + + ))} +
+ )} +
+
+
+ ); +} + +function formatTimeAgo(timestamp: string | null): string { + if (!timestamp) return "No activity"; + const diff = Date.now() - new Date(timestamp).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return "Just now"; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} diff --git a/web_src/src/hooks/useAgentChats.ts b/web_src/src/hooks/useAgentChats.ts index 2d23b05723..58bb68483a 100644 --- a/web_src/src/hooks/useAgentChats.ts +++ b/web_src/src/hooks/useAgentChats.ts @@ -148,3 +148,35 @@ export function useDefineAgentOutcome(organizationId: string | undefined) { }, }); } + +export type CanvasSession = { + id: string; + userId: string; + userName: string; + userAvatarUrl: string; + status: string; + lastActivityAt: string | null; +}; + +export function useCanvasSessions(canvasId: string | undefined, organizationId: string | undefined, enabled: boolean) { + return useQuery({ + queryKey: [...agentChatKeys.all, "sessions", canvasId], + enabled: enabled && Boolean(canvasId), + queryFn: async (): Promise => { + const res = await fetch(`/api/v1/agents/canvases/${canvasId}/sessions`, { + headers: { "x-organization-id": organizationId ?? "" }, + credentials: "include", + }); + if (!res.ok) return []; + const data = await res.json(); + return (data.sessions ?? []).map((s: Record) => ({ + id: s.id ?? "", + userId: s.userId ?? "", + userName: s.userName ?? "", + userAvatarUrl: s.userAvatarUrl ?? "", + status: s.status ?? "idle", + lastActivityAt: s.lastActivityAt ?? null, + })); + }, + }); +} From d0e88a897987f1c712fbb83a28f5a716bcc686d2 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 09:01:47 +0000 Subject: [PATCH 2/6] fix: register ListCanvasSessions in RBAC interceptor Signed-off-by: Bender Rodriguez --- pkg/authorization/interceptor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/authorization/interceptor.go b/pkg/authorization/interceptor.go index 141f4de158..2f150a1c30 100644 --- a/pkg/authorization/interceptor.go +++ b/pkg/authorization/interceptor.go @@ -178,6 +178,12 @@ func NewAuthorizationInterceptor(authService Authorization) *AuthorizationInterc DomainType: models.DomainTypeOrganization, RequiredExperimentalFeatures: []string{features.FeatureClaudeManagedAgents}, }, + pbAgents.Agents_ListCanvasSessions_FullMethodName: { + Resource: "agents", + Action: "read", + DomainType: models.DomainTypeOrganization, + RequiredExperimentalFeatures: []string{features.FeatureClaudeManagedAgents}, + }, pbAgents.Agents_ListAgentChatMessages_FullMethodName: { Resource: "agents", Action: "read", From bcd323ff45b9fb40c7dab4dd1ab491fa9453d5a6 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 09:08:08 +0000 Subject: [PATCH 3/6] fix: resolve user names, allow org-scoped message reads for session browser Signed-off-by: Bender Rodriguez --- pkg/agents/service.go | 9 ++++++++- pkg/grpc/actions/agents/common.go | 1 + pkg/grpc/actions/agents/list_agent_chat_messages.go | 11 +++++++---- pkg/models/agent_session.go | 12 ++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pkg/agents/service.go b/pkg/agents/service.go index 14f555972e..6db84003e8 100644 --- a/pkg/agents/service.go +++ b/pkg/agents/service.go @@ -391,13 +391,14 @@ func (s *Service) ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]Canv SessionID string UserID string UserName string + UserEmail string Status string LastActiveAt *time.Time } err := database.Conn(). Table("agent_sessions"). - Select("agent_sessions.id as session_id, agent_sessions.user_id, accounts.name as user_name, agent_sessions.status, agent_sessions.last_active_at"). + Select("agent_sessions.id as session_id, agent_sessions.user_id, COALESCE(NULLIF(accounts.name, ''), accounts.email, 'Unknown') as user_name, COALESCE(accounts.email, '') as user_email, agent_sessions.status, agent_sessions.last_active_at"). Joins("LEFT JOIN accounts ON accounts.id = agent_sessions.user_id"). Where("agent_sessions.organization_id = ? AND agent_sessions.canvas_id = ?", organizationID, canvasID). Order("agent_sessions.updated_at DESC"). @@ -418,3 +419,9 @@ func (s *Service) ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]Canv } return entries, nil } + +// GetSessionInOrg finds a session by ID within an organization, regardless of owner. +// Used for the session browser read-only view. +func (s *Service) GetSessionInOrg(organizationID, sessionID uuid.UUID) (*models.AgentSession, error) { + return models.FindAgentSessionInOrg(organizationID, sessionID) +} diff --git a/pkg/grpc/actions/agents/common.go b/pkg/grpc/actions/agents/common.go index 78b041fe8f..42b9478bd3 100644 --- a/pkg/grpc/actions/agents/common.go +++ b/pkg/grpc/actions/agents/common.go @@ -23,6 +23,7 @@ type AgentsService interface { InterruptSession(ctx context.Context, organizationID, userID, sessionID uuid.UUID) error DefineOutcome(ctx context.Context, organizationID, userID, sessionID uuid.UUID, description, rubric string, maxIterations int) error ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]agentservice.CanvasSessionInfo, error) + GetSessionInOrg(organizationID, sessionID uuid.UUID) (*models.AgentSession, error) } func agentModeFromProto(mode pb.AgentMode) string { diff --git a/pkg/grpc/actions/agents/list_agent_chat_messages.go b/pkg/grpc/actions/agents/list_agent_chat_messages.go index 0623ca997b..db37bb0bec 100644 --- a/pkg/grpc/actions/agents/list_agent_chat_messages.go +++ b/pkg/grpc/actions/agents/list_agent_chat_messages.go @@ -24,12 +24,15 @@ func ListAgentChatMessages(_ context.Context, svc AgentsService, orgID, userID s return nil, status.Error(codes.InvalidArgument, "invalid chat id") } + // Try user-scoped first, then fall back to org-scoped (for session browser read-only view) if _, err := svc.GetSession(org, user, chatID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, status.Error(codes.NotFound, "agent chat not found") + if _, orgErr := svc.GetSessionInOrg(org, chatID); orgErr != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.Error(codes.NotFound, "agent chat not found") + } + log.WithError(err).WithField("chat_id", chatID).Error("failed to load agent chat") + return nil, status.Error(codes.Internal, "failed to load agent chat") } - log.WithError(err).WithField("chat_id", chatID).Error("failed to load agent chat") - return nil, status.Error(codes.Internal, "failed to load agent chat") } var beforeID uuid.UUID diff --git a/pkg/models/agent_session.go b/pkg/models/agent_session.go index 401cf322e4..e509d6dcce 100644 --- a/pkg/models/agent_session.go +++ b/pkg/models/agent_session.go @@ -166,3 +166,15 @@ func FailStuckStreamingSessions(cutoff time.Time) ([]AgentSession, error) { } return stuck, nil } + +// FindAgentSessionInOrg finds a session by org and session ID without filtering by user. +func FindAgentSessionInOrg(organizationID, sessionID uuid.UUID) (*AgentSession, error) { + var session AgentSession + err := database.Conn(). + Where("organization_id = ? AND id = ?", organizationID, sessionID). + First(&session).Error + if err != nil { + return nil, err + } + return &session, nil +} From 14ce384084be472e45daa04a22d68537ebc41cd6 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 09:11:12 +0000 Subject: [PATCH 4/6] fix: skip greeting and boot in read-only session view Signed-off-by: Bender Rodriguez --- .../AgentConversationTranscript.tsx | 33 +++++++++++++++++-- .../CanvasToolSidebar/AgentTabPanel.tsx | 6 ++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/web_src/src/components/CanvasToolSidebar/AgentConversationTranscript.tsx b/web_src/src/components/CanvasToolSidebar/AgentConversationTranscript.tsx index 12696708c1..7d795ab89f 100644 --- a/web_src/src/components/CanvasToolSidebar/AgentConversationTranscript.tsx +++ b/web_src/src/components/CanvasToolSidebar/AgentConversationTranscript.tsx @@ -1,5 +1,6 @@ import { Bot, ChevronRight, Loader2, SquareTerminal } from "lucide-react"; -import { memo, useCallback, useEffect, useState, type RefObject } from "react"; +import { memo, useCallback, useContext, useEffect, useRef, useState, type RefObject } from "react"; +import { AccountContext } from "@/contexts/accountContextState"; import { formatSystemNotification, isSystemNotification } from "@/components/AgentSidebar/systemMessages"; import type { RubricCategory } from "@/components/AgentSidebar/widgets/parser"; import { RichMessage } from "@/components/AgentSidebar/widgets/RichMessage"; @@ -134,6 +135,7 @@ const MessageRow = memo(function MessageRow({ } const isUser = message.role === "user"; + const { account } = useContext(AccountContext); return (
@@ -159,7 +161,23 @@ const MessageRow = memo(function MessageRow({ )}
{message.createdAt ? ( - {formatTime(message.createdAt)} +
+ {isUser && account ? ( + <> + {formatTime(message.createdAt)} + {account.avatar_url ? ( + + ) : ( +
+ {(account.name?.[0] ?? "?").toUpperCase()} +
+ )} + {account.name?.split(" ")[0]} + + ) : ( + {formatTime(message.createdAt)} + )} +
) : null} ); @@ -226,8 +244,17 @@ function truncateQuestion(question: string): string { } function ToolGroupRow({ messages }: { messages: AgentMessage[] }) { - const [expanded, setExpanded] = useState(true); const hasRunning = messages.some((message) => message.toolStatus === "started"); + const [expanded, setExpanded] = useState(hasRunning); + const prevRunning = useRef(hasRunning); + + useEffect(() => { + if (prevRunning.current && !hasRunning) { + setExpanded(false); + } + prevRunning.current = hasRunning; + }, [hasRunning]); + const count = messages.length; const label = hasRunning ? `Running command${count > 1 ? ` (${count})` : ""}...` diff --git a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx index f3dd639fde..9edfcd6cb0 100644 --- a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx +++ b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx @@ -172,8 +172,9 @@ function ChatConversation({ const { account } = useContext(AccountContext); const greetingFirstName = account?.name?.split(" ")[0] ?? "there"; - // Prepend a synthetic greeting as the first message so it never disappears + // Prepend a synthetic greeting as the first message (only for own session) const messages = useMemo(() => { + if (readOnly) return rawMessages; const greeting: AgentMessage = { id: "__greeting__", role: "assistant", @@ -184,13 +185,14 @@ function ChatConversation({ toolStatus: "", }; return [greeting, ...rawMessages]; - }, [rawMessages, greetingFirstName]); + }, [rawMessages, greetingFirstName, readOnly]); const showThinking = useThinkingIndicator(rawMessages, status); // Auto-kickoff: send a boot message when session is new (no messages yet) const bootState = useRef<"idle" | "sending" | "sent">("idle"); useEffect(() => { + if (readOnly) return; if (bootState.current !== "idle") return; if (!messagesQuery.data || messagesQuery.isLoading) return; From d26bbe9a629a767696d6f84472135c306a770e77 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 09:16:09 +0000 Subject: [PATCH 5/6] fix: join users table instead of accounts for session user names Signed-off-by: Bender Rodriguez --- pkg/agents/service.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/agents/service.go b/pkg/agents/service.go index 6db84003e8..1c92f7759d 100644 --- a/pkg/agents/service.go +++ b/pkg/agents/service.go @@ -391,15 +391,14 @@ func (s *Service) ListCanvasSessions(organizationID, canvasID uuid.UUID) ([]Canv SessionID string UserID string UserName string - UserEmail string Status string LastActiveAt *time.Time } err := database.Conn(). Table("agent_sessions"). - Select("agent_sessions.id as session_id, agent_sessions.user_id, COALESCE(NULLIF(accounts.name, ''), accounts.email, 'Unknown') as user_name, COALESCE(accounts.email, '') as user_email, agent_sessions.status, agent_sessions.last_active_at"). - Joins("LEFT JOIN accounts ON accounts.id = agent_sessions.user_id"). + Select("agent_sessions.id as session_id, agent_sessions.user_id, COALESCE(NULLIF(users.name, ''), 'Unknown') as user_name, agent_sessions.status, agent_sessions.last_active_at"). + Joins("LEFT JOIN users ON users.id = agent_sessions.user_id"). Where("agent_sessions.organization_id = ? AND agent_sessions.canvas_id = ?", organizationID, canvasID). Order("agent_sessions.updated_at DESC"). Scan(&results).Error From ec09969c4aae6af3afc76de08e9a40b4e886e594 Mon Sep 17 00:00:00 2001 From: Bender Rodriguez Date: Fri, 22 May 2026 09:20:43 +0000 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20collaborative=20sessions=20?= =?UTF-8?q?=E2=80=94=20any=20org=20member=20can=20send,=20user=20identity?= =?UTF-8?q?=20injected=20in=20preamble?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove read-only mode: any org member can send messages to any session - Inject current_user_id and current_user_name in every preamble - Agent knows who is talking at each turn Signed-off-by: Bender Rodriguez --- pkg/agents/service.go | 11 +++++++++-- .../components/CanvasToolSidebar/AgentTabPanel.tsx | 1 - 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/agents/service.go b/pkg/agents/service.go index 1c92f7759d..9ded2a7d69 100644 --- a/pkg/agents/service.go +++ b/pkg/agents/service.go @@ -160,7 +160,7 @@ func (s *Service) SendMessage(ctx context.Context, organizationID, userID, sessi return nil, fmt.Errorf("message content is required") } - session, err := models.FindAgentSessionForUser(organizationID, userID, sessionID) + session, err := models.FindAgentSessionInOrg(organizationID, sessionID) if err != nil { return nil, err } @@ -218,8 +218,15 @@ func (s *Service) buildPreamble(session *models.AgentSession, organizationID, us session.CanvasID.String(), session.CanvasID.String(), ) + + // Inject user identity so the agent knows who is talking + userIdentity := fmt.Sprintf("current_user_id: %s", userID.String()) + if user, err := models.FindUnscopedUserByID(userID.String()); err == nil && user.Name != "" { + userIdentity = fmt.Sprintf("current_user_id: %s\ncurrent_user_name: %s", userID.String(), user.Name) + } + draftStatus := getDraftStatus(session.CanvasID) - return base + "\n\n" + modeInstructions(mode) + "\n\n" + draftStatus, nil + return base + "\n" + userIdentity + "\n\n" + modeInstructions(mode) + "\n\n" + draftStatus, nil } func (s *Service) enqueueStream(sessionID, organizationID, userID uuid.UUID) error { diff --git a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx index 9edfcd6cb0..ba3e5843d1 100644 --- a/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx +++ b/web_src/src/components/CanvasToolSidebar/AgentTabPanel.tsx @@ -122,7 +122,6 @@ export function AgentTabPanel({ toolSidebarState }: { toolSidebarState: CanvasTo agentMode={toolSidebarState.agentMode} onModeSwitch={toolSidebarState.switchAgentMode} isEditing={toolSidebarState.isEditing} - readOnly /> );