@sentry/junior-dashboard 0.242.0 → 0.244.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { Bot } from "lucide-react";
3
+ import { Link } from "react-router";
4
+ import {
5
+ conversationChildAgentsReportSchema,
6
+ type ConversationChildAgent,
7
+ } from "@sentry/junior/api/schema";
8
+
9
+ import { StatusChip, type StatusChipTone } from "../components/StatusChip";
10
+ import { formatMessageTimestamp } from "../format";
11
+ import { fetchDashboardJson } from "../http";
12
+ import { conversationPath } from "./conversationRoutes";
13
+
14
+ const statusTone: Record<ConversationChildAgent["status"], StatusChipTone> = {
15
+ blocked: "warning",
16
+ completed: "success",
17
+ failed: "danger",
18
+ paused: "info",
19
+ pending: "neutral",
20
+ running: "info",
21
+ stopped: "neutral",
22
+ };
23
+
24
+ /** Load and show the child agents of one Conversation with runtime status. */
25
+ export function ConversationChildAgents(props: {
26
+ conversationId: string;
27
+ /** Called before a child link leaves the current Conversation. */
28
+ onNavigate(): void;
29
+ }) {
30
+ const query = useQuery({
31
+ queryFn: ({ signal }) =>
32
+ fetchDashboardJson(
33
+ conversationChildAgentsReportSchema,
34
+ `/api/conversations/${encodeURIComponent(props.conversationId)}/agents`,
35
+ signal,
36
+ ),
37
+ queryKey: ["conversation", props.conversationId, "agents"],
38
+ retry: false,
39
+ });
40
+
41
+ if (query.isPending) {
42
+ return (
43
+ <p className="m-0 text-sm text-dashboard-text-muted" role="status">
44
+ Loading subagents…
45
+ </p>
46
+ );
47
+ }
48
+ if (query.error) {
49
+ return (
50
+ <p className="m-0 text-sm text-red-300/80">
51
+ Could not load subagents from this Conversation.
52
+ </p>
53
+ );
54
+ }
55
+ if (query.data.agents.length === 0) {
56
+ return (
57
+ <div className="grid justify-items-center gap-2 py-10 text-center text-dashboard-text-muted">
58
+ <Bot aria-hidden="true" className="size-5 opacity-60" />
59
+ <p className="m-0 text-sm">No subagents were started.</p>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ return (
65
+ <ol aria-label="Subagents" className="m-0 grid list-none gap-3 p-0">
66
+ {query.data.agents.map((agent) => (
67
+ <li
68
+ className="grid gap-2 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface px-3 py-3"
69
+ key={agent.conversationId}
70
+ >
71
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
72
+ <Link
73
+ className="min-w-0 truncate font-mono text-xs font-semibold text-dashboard-text no-underline hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
74
+ onClick={props.onNavigate}
75
+ to={conversationPath(agent.conversationId)}
76
+ >
77
+ {agent.redacted ? "Private subagent" : (agent.name ?? "agent")}
78
+ </Link>
79
+ <StatusChip
80
+ className="ml-auto"
81
+ size="compact"
82
+ tone={statusTone[agent.status]}
83
+ >
84
+ {agent.status}
85
+ </StatusChip>
86
+ </div>
87
+ {agent.title ? (
88
+ <p className="m-0 break-words text-sm font-semibold text-dashboard-text">
89
+ {agent.title}
90
+ </p>
91
+ ) : null}
92
+ {agent.task ? (
93
+ <p className="m-0 line-clamp-4 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
94
+ {agent.task}
95
+ </p>
96
+ ) : null}
97
+ {agent.error ? (
98
+ <p className="m-0 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
99
+ {agent.error}
100
+ </p>
101
+ ) : null}
102
+ <time
103
+ className="font-mono text-xs text-dashboard-text-muted"
104
+ dateTime={agent.updatedAt}
105
+ >
106
+ {formatMessageTimestamp(Date.parse(agent.updatedAt), true)}
107
+ </time>
108
+ </li>
109
+ ))}
110
+ </ol>
111
+ );
112
+ }
@@ -5,12 +5,14 @@ import { formatRelativeTime, formatTime } from "../format";
5
5
  import { ButtonLink } from "../components/Button";
6
6
  import { Drawer } from "../components/Drawer";
7
7
  import { SegmentedTabs } from "../components/SegmentedTabs";
8
+ import { ConversationChildAgents } from "./ConversationChildAgents";
8
9
  import { ConversationMemories } from "./ConversationMemories";
9
10
  import { ShareConversationButton } from "./ShareConversationButton";
10
11
 
11
12
  const tabs = [
12
13
  { label: "Details", value: "details" },
13
14
  { label: "Memories", value: "memories" },
15
+ { label: "Subagents", value: "agents" },
14
16
  ] as const;
15
17
 
16
18
  /** Put conversation context before usage and diagnostic links. */
@@ -26,7 +28,7 @@ export function ConversationDetailsDrawer(props: {
26
28
  stats: ReactNode;
27
29
  title: string;
28
30
  }) {
29
- const [tab, setTab] = useState<"details" | "memories">("details");
31
+ const [tab, setTab] = useState<(typeof tabs)[number]["value"]>("details");
30
32
  const titleId = "conversation-details-drawer-title";
31
33
  const sections = [
32
34
  { content: props.brief, title: "Summary" },
@@ -65,6 +67,11 @@ export function ConversationDetailsDrawer(props: {
65
67
  >
66
68
  {tab === "memories" ? (
67
69
  <ConversationMemories conversationId={props.conversationId} />
70
+ ) : tab === "agents" ? (
71
+ <ConversationChildAgents
72
+ conversationId={props.conversationId}
73
+ onNavigate={props.onClose}
74
+ />
68
75
  ) : sections.length > 0 ? (
69
76
  <div className="grid min-w-0 gap-5">
70
77
  {sections.map((section) => (
@@ -50,7 +50,10 @@ function SubagentTranscriptDrawerContent(props: {
50
50
  const [searchOpen, setSearchOpen] = useState(false);
51
51
 
52
52
  const detail = query.data;
53
- const label = detail?.displayTitle || props.target.part.subagentKind;
53
+ const label =
54
+ props.target.part.title ||
55
+ detail?.displayTitle ||
56
+ props.target.part.subagentKind;
54
57
  const meta = [
55
58
  statusLabel(props.target.part, detail?.status),
56
59
  detail ? formatMessageTimestamp(Date.parse(detail.startedAt)) : undefined,
@@ -169,7 +172,7 @@ function statusLabel(
169
172
  part: TranscriptViewSubagentPart,
170
173
  detailStatus?: string,
171
174
  ): string {
172
- if (part.status === "error" || part.status === "aborted") {
175
+ if (part.status !== "running" && part.status !== "completed") {
173
176
  return part.status;
174
177
  }
175
178
  return detailStatus ?? part.status;
@@ -9,7 +9,8 @@ export function TranscriptSubagentView(props: {
9
9
  part: TranscriptViewSubagentPart;
10
10
  timestamp?: number;
11
11
  }) {
12
- const label = props.part.subagentKind;
12
+ const label = props.part.agentName ?? props.part.subagentKind;
13
+ const summary = props.part.title ?? props.part.task;
13
14
  const status = props.part.status;
14
15
  const meta = [
15
16
  status,
@@ -27,6 +28,11 @@ export function TranscriptSubagentView(props: {
27
28
  <strong className="min-w-0 break-words font-bold text-cyan-100">
28
29
  <HighlightText text={label} />
29
30
  </strong>
31
+ {summary ? (
32
+ <span className="min-w-0 truncate text-dashboard-text-muted">
33
+ <HighlightText text={summary} />
34
+ </span>
35
+ ) : null}
30
36
  {props.part.status === "running" ? (
31
37
  <span
32
38
  aria-hidden="true"
@@ -44,7 +50,7 @@ export function TranscriptSubagentView(props: {
44
50
 
45
51
  return (
46
52
  <button
47
- aria-label={`Open ${props.part.subagentKind} transcript`}
53
+ aria-label={`Open ${label} transcript`}
48
54
  className="block w-full min-w-0 cursor-pointer text-left transition-colors hover:bg-white/[0.035] focus-visible:outline focus-visible:outline-1 focus-visible:outline-cyan-300/55"
49
55
  onClick={() => props.onOpenTranscript?.(props.part)}
50
56
  type="button"
@@ -94,7 +94,7 @@ export function eventLogSummary(data: ConversationReportEventData): string {
94
94
  case "handoff":
95
95
  return "Handoff · New configuration applied";
96
96
  case "subagent":
97
- return `${data.subagentKind} · ${data.status} · ${data.childConversationId}`;
97
+ return `${data.agentName ?? data.subagentKind} · ${data.status} · ${data.childConversationId}`;
98
98
  }
99
99
  }
100
100
 
@@ -111,7 +111,8 @@ export function eventLogTone(data: ConversationReportEventData): string {
111
111
  }
112
112
  if (
113
113
  (data.type === "guardian_action_reviewed" && data.decision === "ask") ||
114
- (data.type === "subagent" && data.status === "aborted")
114
+ (data.type === "subagent" &&
115
+ (data.status === "aborted" || data.status === "blocked"))
115
116
  ) {
116
117
  return "text-amber-200";
117
118
  }
@@ -362,6 +362,9 @@ export function transcriptMessagesFromEvents(
362
362
  childConversationId: data.childConversationId,
363
363
  subagentKind: data.subagentKind,
364
364
  status: data.status,
365
+ ...(data.agentName ? { agentName: data.agentName } : undefined),
366
+ ...(data.task ? { task: data.task } : undefined),
367
+ ...(data.title ? { title: data.title } : undefined),
365
368
  };
366
369
  subagents.set(subagentId, part);
367
370
  messages.push({
@@ -194,7 +194,12 @@ export function messageRawText(message: TranscriptViewMessage): string {
194
194
  if (part.type === "reasoning") return part.text ?? "reasoning redacted";
195
195
  if (part.type === "tool_call") return `tool_call ${part.name}`;
196
196
  if (part.type === "subagent") {
197
- return `subagent ${part.subagentKind}\nstatus ${part.status}`;
197
+ return [
198
+ `subagent ${part.agentName ?? part.subagentKind}`,
199
+ ...(part.title ? [`title ${part.title}`] : []),
200
+ ...(part.task ? [`task ${part.task}`] : []),
201
+ `status ${part.status}`,
202
+ ].join("\n");
198
203
  }
199
204
  if (part.type === "structured_event") {
200
205
  return [
@@ -160,7 +160,12 @@ export function entryMatchesSearch(
160
160
 
161
161
  if (entry.kind === "subagent") {
162
162
  return (
163
- textContains(entry.part.subagentKind, normalizedQuery) ||
163
+ textContains(
164
+ entry.part.agentName ?? entry.part.subagentKind,
165
+ normalizedQuery,
166
+ ) ||
167
+ textContains(entry.part.title, normalizedQuery) ||
168
+ textContains(entry.part.task, normalizedQuery) ||
164
169
  textContains(entry.part.status, normalizedQuery)
165
170
  );
166
171
  }
@@ -396,9 +396,14 @@ function appendSubagent(
396
396
  part: TranscriptViewSubagentPart,
397
397
  timestamp: number | undefined,
398
398
  ): void {
399
- lines.push("", `### Subagent: ${headingText(part.subagentKind)}`);
399
+ lines.push(
400
+ "",
401
+ `### Subagent: ${headingText(part.agentName ?? part.subagentKind)}`,
402
+ );
400
403
  addEventMeta(lines, conversationTranscript, timestamp);
401
404
  addMetaLine(lines, "Status", part.status);
405
+ addMetaLine(lines, "Title", part.title);
406
+ addMetaLine(lines, "Task", part.task);
402
407
  }
403
408
 
404
409
  function appendTool(
@@ -38,10 +38,13 @@ export type TranscriptViewToolCallPart = {
38
38
  };
39
39
 
40
40
  export type TranscriptViewSubagentPart = {
41
+ agentName?: string;
41
42
  childConversationId: string;
42
43
  id: string;
43
- status: "aborted" | "completed" | "error" | "running";
44
+ status: "aborted" | "blocked" | "completed" | "error" | "running";
44
45
  subagentKind: string;
46
+ task?: string;
47
+ title?: string;
45
48
  type: "subagent";
46
49
  };
47
50
 
@@ -8,6 +8,7 @@ import type {
8
8
  ActorProfileReport,
9
9
  ActorSummaryReport,
10
10
  ActorWindowMetrics,
11
+ ConversationChildAgentsReport,
11
12
  ConversationDetailReport,
12
13
  ConversationEventPage,
13
14
  ConversationFeed,
@@ -694,7 +695,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
694
695
  calls: [
695
696
  {
696
697
  toolCallId: "qa-advisor",
697
- name: "advisor",
698
+ name: "spawnAgent",
698
699
  status: "running",
699
700
  },
700
701
  ],
@@ -704,11 +705,11 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
704
705
  calls: [
705
706
  {
706
707
  toolCallId: "qa-advisor",
707
- name: "advisor",
708
+ name: "spawnAgent",
708
709
  status: "running",
709
710
  startedSeq: 1,
710
711
  startedAt: iso(Date.parse(startedAt), 2_000),
711
- input: { automation: "Review the dashboard plan" },
712
+ input: { task: "Review the dashboard plan before editing." },
712
713
  },
713
714
  ],
714
715
  }),
@@ -717,7 +718,10 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
717
718
  startedSeq: 3,
718
719
  startedAt: iso(Date.parse(startedAt), 4_000),
719
720
  childConversationId: DASHBOARD_QA_PLAN_ID,
720
- subagentKind: "advisor",
721
+ subagentKind: "agent",
722
+ agentName: "planner",
723
+ task: "Review the dashboard plan before editing.",
724
+ title: "Review the dashboard plan",
721
725
  parentToolCallId: "qa-advisor",
722
726
  status: "running",
723
727
  }),
@@ -726,7 +730,10 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
726
730
  startedSeq: 3,
727
731
  startedAt: iso(Date.parse(startedAt), 4_000),
728
732
  childConversationId: DASHBOARD_QA_PLAN_ID,
729
- subagentKind: "advisor",
733
+ subagentKind: "agent",
734
+ agentName: "planner",
735
+ task: "Review the dashboard plan before editing.",
736
+ title: "Review the dashboard plan",
730
737
  parentToolCallId: "qa-advisor",
731
738
  status: "completed",
732
739
  }),
@@ -735,11 +742,11 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
735
742
  calls: [
736
743
  {
737
744
  toolCallId: "qa-advisor",
738
- name: "advisor",
745
+ name: "spawnAgent",
739
746
  status: "completed",
740
747
  startedSeq: 1,
741
748
  startedAt: iso(Date.parse(startedAt), 2_000),
742
- output: { child_conversation_id: DASHBOARD_QA_PLAN_ID },
749
+ output: { conversation_id: DASHBOARD_QA_PLAN_ID },
743
750
  },
744
751
  ],
745
752
  }),
@@ -748,7 +755,8 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
748
755
  startedSeq: 6,
749
756
  startedAt: iso(Date.parse(startedAt), 25_000),
750
757
  childConversationId: DASHBOARD_QA_REVIEW_ID,
751
- subagentKind: "advisor",
758
+ subagentKind: "agent",
759
+ agentName: "reviewer",
752
760
  status: "running",
753
761
  }),
754
762
  reportEvent(7, iso(Date.parse(startedAt), 44_000), {
@@ -756,8 +764,9 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
756
764
  startedSeq: 6,
757
765
  startedAt: iso(Date.parse(startedAt), 25_000),
758
766
  childConversationId: DASHBOARD_QA_REVIEW_ID,
759
- subagentKind: "advisor",
760
- status: "completed",
767
+ subagentKind: "agent",
768
+ agentName: "reviewer",
769
+ status: "blocked",
761
770
  }),
762
771
  reportEvent(8, iso(Date.parse(startedAt), 50_000), {
763
772
  type: "tool_calls",
@@ -1816,6 +1825,40 @@ export function readMockConversationPendingMessages(
1816
1825
  };
1817
1826
  }
1818
1827
 
1828
+ /** Return the child agents of one mock Conversation, matching its transcript. */
1829
+ export function readMockConversationChildAgents(
1830
+ conversationId: string,
1831
+ ): ConversationChildAgentsReport | undefined {
1832
+ const conversation = mockConversations(NOW_MS).find(
1833
+ (candidate) => candidate.conversationId === conversationId,
1834
+ );
1835
+ if (!conversation) return undefined;
1836
+ if (conversationId !== DASHBOARD_QA_CONVERSATION_ID) {
1837
+ return { agents: [], conversationId };
1838
+ }
1839
+ const startedAtMs = NOW_MS - 5 * 60 * 60_000;
1840
+ return {
1841
+ agents: [
1842
+ {
1843
+ conversationId: DASHBOARD_QA_REVIEW_ID,
1844
+ error: "GitHub access is needed to read the pull request.",
1845
+ name: "reviewer",
1846
+ status: "blocked",
1847
+ task: "Review the open dashboard pull requests and list risky changes.",
1848
+ updatedAt: iso(startedAtMs, 44_000),
1849
+ },
1850
+ {
1851
+ conversationId: DASHBOARD_QA_PLAN_ID,
1852
+ status: "completed",
1853
+ task: "Review the dashboard plan before editing.",
1854
+ title: "Review the dashboard plan",
1855
+ updatedAt: iso(startedAtMs, 20_000),
1856
+ },
1857
+ ],
1858
+ conversationId,
1859
+ };
1860
+ }
1861
+
1819
1862
  /** Return one canonical-event visual-QA conversation detail fixture. */
1820
1863
  export function readMockConversationDetail(
1821
1864
  conversationId: string,
@@ -7,6 +7,7 @@ import {
7
7
  actorProfileReportSchema,
8
8
  archiveConversationBodySchema,
9
9
  archiveConversationResponseSchema,
10
+ conversationChildAgentsReportSchema,
10
11
  conversationDetailQuerySchema,
11
12
  conversationDetailReportSchema,
12
13
  conversationEventPageSchema,
@@ -38,6 +39,7 @@ import {
38
39
  readMockCodeRepository,
39
40
  } from "./code-repository";
40
41
  import {
42
+ readMockConversationChildAgents,
41
43
  readMockConversationDetail,
42
44
  readMockConversationEvents,
43
45
  readMockConversationFeed,
@@ -193,6 +195,16 @@ export function createMockReportingApi(): Hono<{
193
195
  ? jsonResponse(conversationEventPageSchema, report)
194
196
  : errorResponse("Invalid conversation cursor.", 400);
195
197
  });
198
+ app.get("/conversations/:conversationId/agents", (c) => {
199
+ const params = conversationParamsSchema.safeParse(c.req.param());
200
+ if (!params.success) {
201
+ return errorResponse("Invalid route parameters.", 400);
202
+ }
203
+ const report = readMockConversationChildAgents(params.data.conversationId);
204
+ return report
205
+ ? jsonResponse(conversationChildAgentsReportSchema, report)
206
+ : errorResponse("Conversation not found.", 404);
207
+ });
196
208
  app.get("/conversations/:conversationId/pending-messages", (c) => {
197
209
  const params = conversationParamsSchema.safeParse(c.req.param());
198
210
  if (!params.success) {