@sentry/junior-dashboard 0.242.0 → 0.243.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,107 @@
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.task ? (
88
+ <p className="m-0 line-clamp-4 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
89
+ {agent.task}
90
+ </p>
91
+ ) : null}
92
+ {agent.error ? (
93
+ <p className="m-0 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
94
+ {agent.error}
95
+ </p>
96
+ ) : null}
97
+ <time
98
+ className="font-mono text-xs text-dashboard-text-muted"
99
+ dateTime={agent.updatedAt}
100
+ >
101
+ {formatMessageTimestamp(Date.parse(agent.updatedAt), true)}
102
+ </time>
103
+ </li>
104
+ ))}
105
+ </ol>
106
+ );
107
+ }
@@ -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) => (
@@ -169,7 +169,7 @@ function statusLabel(
169
169
  part: TranscriptViewSubagentPart,
170
170
  detailStatus?: string,
171
171
  ): string {
172
- if (part.status === "error" || part.status === "aborted") {
172
+ if (part.status !== "running" && part.status !== "completed") {
173
173
  return part.status;
174
174
  }
175
175
  return detailStatus ?? part.status;
@@ -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
  }
@@ -40,7 +40,7 @@ export type TranscriptViewToolCallPart = {
40
40
  export type TranscriptViewSubagentPart = {
41
41
  childConversationId: string;
42
42
  id: string;
43
- status: "aborted" | "completed" | "error" | "running";
43
+ status: "aborted" | "blocked" | "completed" | "error" | "running";
44
44
  subagentKind: string;
45
45
  type: "subagent";
46
46
  };
@@ -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,7 @@ 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",
721
722
  parentToolCallId: "qa-advisor",
722
723
  status: "running",
723
724
  }),
@@ -726,7 +727,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
726
727
  startedSeq: 3,
727
728
  startedAt: iso(Date.parse(startedAt), 4_000),
728
729
  childConversationId: DASHBOARD_QA_PLAN_ID,
729
- subagentKind: "advisor",
730
+ subagentKind: "agent",
730
731
  parentToolCallId: "qa-advisor",
731
732
  status: "completed",
732
733
  }),
@@ -735,11 +736,11 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
735
736
  calls: [
736
737
  {
737
738
  toolCallId: "qa-advisor",
738
- name: "advisor",
739
+ name: "spawnAgent",
739
740
  status: "completed",
740
741
  startedSeq: 1,
741
742
  startedAt: iso(Date.parse(startedAt), 2_000),
742
- output: { child_conversation_id: DASHBOARD_QA_PLAN_ID },
743
+ output: { conversation_id: DASHBOARD_QA_PLAN_ID },
743
744
  },
744
745
  ],
745
746
  }),
@@ -748,7 +749,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
748
749
  startedSeq: 6,
749
750
  startedAt: iso(Date.parse(startedAt), 25_000),
750
751
  childConversationId: DASHBOARD_QA_REVIEW_ID,
751
- subagentKind: "advisor",
752
+ subagentKind: "agent",
752
753
  status: "running",
753
754
  }),
754
755
  reportEvent(7, iso(Date.parse(startedAt), 44_000), {
@@ -756,8 +757,8 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
756
757
  startedSeq: 6,
757
758
  startedAt: iso(Date.parse(startedAt), 25_000),
758
759
  childConversationId: DASHBOARD_QA_REVIEW_ID,
759
- subagentKind: "advisor",
760
- status: "completed",
760
+ subagentKind: "agent",
761
+ status: "blocked",
761
762
  }),
762
763
  reportEvent(8, iso(Date.parse(startedAt), 50_000), {
763
764
  type: "tool_calls",
@@ -1816,6 +1817,39 @@ export function readMockConversationPendingMessages(
1816
1817
  };
1817
1818
  }
1818
1819
 
1820
+ /** Return the child agents of one mock Conversation, matching its transcript. */
1821
+ export function readMockConversationChildAgents(
1822
+ conversationId: string,
1823
+ ): ConversationChildAgentsReport | undefined {
1824
+ const conversation = mockConversations(NOW_MS).find(
1825
+ (candidate) => candidate.conversationId === conversationId,
1826
+ );
1827
+ if (!conversation) return undefined;
1828
+ if (conversationId !== DASHBOARD_QA_CONVERSATION_ID) {
1829
+ return { agents: [], conversationId };
1830
+ }
1831
+ const startedAtMs = NOW_MS - 5 * 60 * 60_000;
1832
+ return {
1833
+ agents: [
1834
+ {
1835
+ conversationId: DASHBOARD_QA_REVIEW_ID,
1836
+ error: "GitHub access is needed to read the pull request.",
1837
+ name: "reviewer",
1838
+ status: "blocked",
1839
+ task: "Review the open dashboard pull requests and list risky changes.",
1840
+ updatedAt: iso(startedAtMs, 44_000),
1841
+ },
1842
+ {
1843
+ conversationId: DASHBOARD_QA_PLAN_ID,
1844
+ status: "completed",
1845
+ task: "Review the dashboard plan before editing.",
1846
+ updatedAt: iso(startedAtMs, 20_000),
1847
+ },
1848
+ ],
1849
+ conversationId,
1850
+ };
1851
+ }
1852
+
1819
1853
  /** Return one canonical-event visual-QA conversation detail fixture. */
1820
1854
  export function readMockConversationDetail(
1821
1855
  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) {