diff --git a/packages/junior-dashboard/src/client/types.ts b/packages/junior-dashboard/src/client/types.ts
index c4ddf8c4ab..bd2d128e23 100644
--- a/packages/junior-dashboard/src/client/types.ts
+++ b/packages/junior-dashboard/src/client/types.ts
@@ -163,6 +163,7 @@ export type Conversation = {
locationId?: string;
locationUrl?: string;
actorIdentity?: ConversationSummaryReport["actorIdentity"];
+ participants?: ConversationSummaryReport["participants"];
sentryTraceUrl?: string;
startedAt: string;
status: ConversationSummaryReport["status"];
diff --git a/packages/junior-dashboard/src/mock-reporting/fixtures.ts b/packages/junior-dashboard/src/mock-reporting/fixtures.ts
index c7f7a32232..cdd71c6630 100644
--- a/packages/junior-dashboard/src/mock-reporting/fixtures.ts
+++ b/packages/junior-dashboard/src/mock-reporting/fixtures.ts
@@ -341,6 +341,12 @@ function activeConversation(nowMs: number): ConversationDetailReport {
channel: "CQA123",
channelName: "proj-checkout",
actorIdentity: actor("dev@example.com", "Morgan Lee", "morgan"),
+ participants: [
+ actor("dev@example.com", "Morgan Lee", "morgan"),
+ actor("avery@sentry.io", "Avery Chen", "avery"),
+ actor("riley@sentry.io", "Riley Park", "riley"),
+ actor("jordan@sentry.io", "Jordan Blake", "jordan"),
+ ],
assignedWork: true,
unfinishedWork: true,
isPriority: true,
diff --git a/packages/junior/src/api/conversations/detail.ts b/packages/junior/src/api/conversations/detail.ts
index c5075ac747..f5b7496c10 100644
--- a/packages/junior/src/api/conversations/detail.ts
+++ b/packages/junior/src/api/conversations/detail.ts
@@ -23,6 +23,7 @@ import { listConversationAnnotations } from "@/chat/plugins/annotations";
import { readLatestConversationBrief } from "@/chat/briefs/store";
import { readConversationSourceTask } from "@/chat/automations/read";
import { readConversationArchivedAt } from "./archive";
+import { readConversationParticipants } from "./participants";
/** Project stored metadata and a bounded event page into a signed history cursor. */
function projectConversationDetail(args: {
@@ -36,6 +37,7 @@ function projectConversationDetail(args: {
events: ConversationDetailReport["events"];
locationId?: string;
modelUsage: NonNullable;
+ participants: NonNullable;
previousSeq?: number;
sourceTask?: ConversationDetailReport["sourceTask"];
teamDomainByTeamId?: ReadonlyMap;
@@ -66,6 +68,9 @@ function projectConversationDetail(args: {
annotations: canExposePayload ? args.annotations : [],
...(canExposePayload && args.brief ? { brief: args.brief } : undefined),
events: args.events,
+ ...(canExposePayload && args.participants.length > 0
+ ? { participants: args.participants }
+ : undefined),
...(args.previousSeq !== undefined
? {
previousCursor: encodeConversationCursor({
@@ -109,6 +114,7 @@ async function readConversationDetailFromSql(
briefVersion,
modelUsage,
metricsByRoot,
+ participantsByConversation,
sourceTask,
teamDomainByTeamId,
] = await Promise.all([
@@ -128,6 +134,7 @@ async function readConversationDetailFromSql(
getDb(),
includeDescendantMetrics ? [conversationId] : [],
),
+ readConversationParticipants(getDb(), [conversationId]),
readConversationSourceTask({
conversationId,
...(options.viewer ? { viewer: options.viewer } : undefined),
@@ -166,6 +173,7 @@ async function readConversationDetailFromSql(
durationMs: metrics?.durationMs ?? record.durationMs,
events: page.events,
modelUsage,
+ participants: participantsByConversation.get(conversationId) ?? [],
...(sourceTask ? { sourceTask } : undefined),
teamDomainByTeamId,
...(page.previousSeq === undefined
diff --git a/packages/junior/src/api/conversations/list.ts b/packages/junior/src/api/conversations/list.ts
index 048c7af90f..c614af629b 100644
--- a/packages/junior/src/api/conversations/list.ts
+++ b/packages/junior/src/api/conversations/list.ts
@@ -36,6 +36,7 @@ import { listConversationWork } from "@/chat/plugins/unfinished-work";
import { isConversationPriority } from "./priority";
import { readLastUserMessageAtByConversation } from "./user-message-activity";
import { readConversationActivityPreviews } from "./activity-preview";
+import { readConversationParticipants } from "./participants";
const CONVERSATION_FEED_LIMIT = 50;
@@ -306,6 +307,7 @@ export async function readConversationFeedFromSql(
annotationsByConversation,
auxiliaryCostsByRoot,
metricsByRoot,
+ participantsByConversation,
teamDomainByTeamId,
conversationWork,
lastUserMessageAtByConversation,
@@ -326,6 +328,7 @@ export async function readConversationFeedFromSql(
includeDescendants: true,
}),
readRootConversationMetricsFromSql(db, conversationIds),
+ readConversationParticipants(db, conversationIds),
resolveSlackTeamDomains(
conversations.flatMap((conversation) =>
conversation.location?.provider === "slack"
@@ -392,9 +395,15 @@ export async function readConversationFeedFromSql(
const activityPreview = access?.canViewPrivateContent
? activityPreviewByConversation.get(conversation.conversationId)
: undefined;
+ const participants = access?.canViewPrivateContent
+ ? participantsByConversation.get(conversation.conversationId)
+ : undefined;
return {
...summary,
...work,
+ ...(participants && participants.length > 0
+ ? { participants }
+ : undefined),
...(activityPreview ? { activityPreview } : undefined),
...(annotations.length > 0 ? { annotations } : undefined),
...(sidebarAnnotationsByConversation[conversation.conversationId]
diff --git a/packages/junior/src/api/conversations/participants.ts b/packages/junior/src/api/conversations/participants.ts
new file mode 100644
index 0000000000..131b2db50f
--- /dev/null
+++ b/packages/junior/src/api/conversations/participants.ts
@@ -0,0 +1,131 @@
+import { asc, eq, inArray } from "drizzle-orm";
+import type { JuniorDatabase } from "@/db/db";
+import {
+ juniorConversationEvents,
+ juniorConversations,
+ juniorIdentities,
+ juniorUsers,
+} from "@/db/schema";
+import type { ActorIdentity } from "../schema/conversation";
+
+const identityColumns = {
+ displayName: juniorIdentities.displayName,
+ email: juniorIdentities.email,
+ handle: juniorIdentities.handle,
+ identityId: juniorIdentities.id,
+ provider: juniorIdentities.provider,
+ providerSubjectId: juniorIdentities.providerSubjectId,
+ userDisplayName: juniorUsers.displayName,
+ userEmail: juniorUsers.primaryEmail,
+ userId: juniorIdentities.userId,
+};
+
+type ParticipantIdentityRow = {
+ displayName: string | null;
+ email: string | null;
+ handle: string | null;
+ identityId: string;
+ provider: string;
+ providerSubjectId: string;
+ userDisplayName: string | null;
+ userEmail: string | null;
+ userId: string | null;
+};
+
+function participantIdentity(
+ row: ParticipantIdentityRow,
+): ActorIdentity | undefined {
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
+ const email = row.email?.trim() || row.userEmail?.trim();
+ const slack = row.provider === "slack";
+ const participant = {
+ ...(email ? { email } : undefined),
+ ...(fullName ? { fullName } : undefined),
+ ...(slack && row.providerSubjectId
+ ? { slackUserId: row.providerSubjectId }
+ : undefined),
+ ...(slack && row.handle ? { slackUserName: row.handle } : undefined),
+ };
+ return Object.keys(participant).length > 0 ? participant : undefined;
+}
+
+/** Read actor identities in first-appearance order for each Conversation. */
+export async function readConversationParticipants(
+ db: JuniorDatabase,
+ conversationIds: readonly string[],
+): Promise