@sentry/junior-dashboard 0.258.0 → 0.260.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.
Files changed (65) hide show
  1. package/README.md +5 -3
  2. package/dist/app.js +88 -37
  3. package/dist/chunks/github-light-QHW3AEMR.js +1 -0
  4. package/dist/client.js +86 -50
  5. package/dist/index.js +88 -37
  6. package/dist/tailwind.css +1 -1
  7. package/package.json +3 -3
  8. package/src/client/App.tsx +0 -1
  9. package/src/client/code.tsx +10 -4
  10. package/src/client/components/ProfileMenu.tsx +3 -0
  11. package/src/client/components/README.md +12 -2
  12. package/src/client/components/ThemeSwitcher.tsx +48 -0
  13. package/src/client/components/charts/ActivityChart.tsx +6 -7
  14. package/src/client/components/charts/InputCacheChart.tsx +1 -1
  15. package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
  16. package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
  17. package/src/client/components/charts/cache-input.ts +11 -3
  18. package/src/client/components/layout/DashboardHeader.tsx +2 -9
  19. package/src/client/conversations/ConversationBrief.tsx +35 -9
  20. package/src/client/conversations/ConversationHomeList.tsx +7 -1
  21. package/src/client/conversations/ConversationMeta.tsx +19 -7
  22. package/src/client/conversations/ConversationTranscript.tsx +8 -11
  23. package/src/client/conversations/EventDetails.tsx +9 -5
  24. package/src/client/conversations/README.md +6 -0
  25. package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
  26. package/src/client/conversations/TranscriptEventView.tsx +48 -9
  27. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  28. package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
  29. package/src/client/conversations/TranscriptText.tsx +28 -20
  30. package/src/client/conversations/conversationActivityPreview.ts +6 -0
  31. package/src/client/conversations/eventTranscript.ts +4 -1
  32. package/src/client/conversations/transcriptMentions.tsx +27 -10
  33. package/src/client/pages/admin/AdminPage.tsx +14 -5
  34. package/src/client/pages/admin/ServiceAccountsSection.tsx +231 -0
  35. package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
  36. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
  37. package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
  38. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
  39. package/src/client/pages/code/CodeActivityChart.tsx +4 -4
  40. package/src/client/pages/dev/ComponentsPage.tsx +7 -3
  41. package/src/client/pages/dev/ConversationFixture.tsx +29 -0
  42. package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
  43. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
  44. package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
  45. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
  46. package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
  47. package/src/client/pages/people/ContributionGrid.tsx +8 -8
  48. package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
  49. package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
  50. package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
  51. package/src/client/pages/system/PluginBarChart.tsx +12 -6
  52. package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
  53. package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
  54. package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
  55. package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
  56. package/src/client/pages/system/WorkspaceList.tsx +6 -1
  57. package/src/client/pages/system/WorkspacesPage.tsx +18 -12
  58. package/src/client/pages/system/workspaceDraft.ts +8 -0
  59. package/src/client/theme.ts +52 -0
  60. package/src/client/types.ts +8 -1
  61. package/src/mock-reporting/fixtures.ts +48 -35
  62. package/src/shell.ts +4 -1
  63. package/src/tailwind.css +109 -0
  64. package/src/theme.ts +49 -0
  65. package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
@@ -1,11 +1,17 @@
1
1
  import { memo } from "react";
2
+ import type { ActorIdentity } from "@sentry/junior/api/schema";
2
3
 
3
4
  import { HighlightedCode } from "../code";
4
5
  import { parseMarkdownBlocks, transcriptRoleKind } from "../format";
5
6
  import { TranscriptMarkdown } from "./TranscriptMarkdown";
7
+ import { MentionPeopleProvider } from "./transcriptMentions";
6
8
 
7
- /** Render transcript prose as Markdown with explicit fenced code blocks. */
9
+ /**
10
+ * Render transcript prose as Markdown with explicit fenced code blocks.
11
+ * `mentions` are the people that the API resolved for this text.
12
+ */
8
13
  export const TranscriptText = memo(function TranscriptText(props: {
14
+ mentions: readonly ActorIdentity[];
9
15
  role?: string;
10
16
  text: string;
11
17
  }) {
@@ -13,27 +19,29 @@ export const TranscriptText = memo(function TranscriptText(props: {
13
19
  const blocks = parseMarkdownBlocks(props.text);
14
20
 
15
21
  return (
16
- <div className="grid min-w-0 gap-2">
17
- {blocks.map((block, index) => {
18
- if (block.language === "markdown" && !block.fenced) {
22
+ <MentionPeopleProvider mentions={props.mentions}>
23
+ <div className="grid min-w-0 gap-2">
24
+ {blocks.map((block, index) => {
25
+ if (block.language === "markdown" && !block.fenced) {
26
+ return (
27
+ <TranscriptMarkdown
28
+ compact={roleKind === "assistant" || roleKind === "user"}
29
+ key={index}
30
+ text={block.code}
31
+ />
32
+ );
33
+ }
34
+
19
35
  return (
20
- <TranscriptMarkdown
21
- compact={roleKind === "assistant" || roleKind === "user"}
36
+ <div
37
+ className="min-w-0 overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5"
22
38
  key={index}
23
- text={block.code}
24
- />
39
+ >
40
+ <HighlightedCode code={block.code} language={block.language} />
41
+ </div>
25
42
  );
26
- }
27
-
28
- return (
29
- <div
30
- className="min-w-0 overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5"
31
- key={index}
32
- >
33
- <HighlightedCode code={block.code} language={block.language} />
34
- </div>
35
- );
36
- })}
37
- </div>
43
+ })}
44
+ </div>
45
+ </MentionPeopleProvider>
38
46
  );
39
47
  });
@@ -1,6 +1,12 @@
1
+ // Slack writes links as `<https://example.com>` or `<https://example.com|label>`.
2
+ const SLACK_LINK_RE = /<((?:https?|mailto):[^\s|<>]+)(?:\|([^<>]*))?>/g;
3
+
1
4
  /** Reduce message Markdown to compact plain text for conversation previews. */
2
5
  export function formatConversationActivityPreview(text: string): string {
3
6
  return text
7
+ .replace(SLACK_LINK_RE, (_, url: string, label?: string) =>
8
+ label?.trim() ? label.trim() : url,
9
+ )
4
10
  .replace(/\[([^\]]+)\]\([^\s)]+(?:\s+"[^"]*")?\)/g, "$1")
5
11
  .replace(/(^|\s)([*_~`]{1,3})(?=\S)/g, "$1")
6
12
  .replace(/([*_~`]{1,3})(?=\s|$|[.,!?;:])/g, "")
@@ -37,6 +37,7 @@ function eventMessage(
37
37
  role,
38
38
  sourceSeq: event.seq,
39
39
  timestamp: eventTimestamp(event),
40
+ ...(event.mentions ? { mentions: event.mentions } : undefined),
40
41
  };
41
42
  }
42
43
 
@@ -213,12 +214,12 @@ export function transcriptMessagesFromEvents(
213
214
  ...(data.actorIdentity
214
215
  ? { actorIdentity: data.actorIdentity }
215
216
  : undefined),
216
- ...(data.mentions ? { mentions: data.mentions } : undefined),
217
217
  ...(data.eventType ? { eventType: data.eventType } : undefined),
218
218
  eventObjectType: data.eventObjectType,
219
219
  ...(data.trustedSummary
220
220
  ? { trustedSummary: data.trustedSummary }
221
221
  : undefined),
222
+ ...(data.events ? { events: data.events } : undefined),
222
223
  ...(data.explicitMention !== undefined
223
224
  ? { explicitMention: data.explicitMention }
224
225
  : undefined),
@@ -388,6 +389,7 @@ export function transcriptMessagesFromEvents(
388
389
  eventMessage(event, "system", [
389
390
  {
390
391
  type: "structured_event",
392
+ ...(event.mentions ? { mentions: event.mentions } : undefined),
391
393
  namespace: data.namespace,
392
394
  name: data.name,
393
395
  version: data.version,
@@ -415,6 +417,7 @@ export function transcriptMessagesFromEvents(
415
417
  eventMessage(event, "system", [
416
418
  {
417
419
  type: "context_event",
420
+ ...(event.mentions ? { mentions: event.mentions } : undefined),
418
421
  event:
419
422
  data.type === "handoff"
420
423
  ? {
@@ -3,7 +3,7 @@ import { Link } from "react-router";
3
3
  import type { ActorIdentity } from "@sentry/junior/api/schema";
4
4
 
5
5
  import { peoplePath } from "../format";
6
- import type { TranscriptViewMessage } from "../types";
6
+ import { cn } from "../styles";
7
7
  import { TRANSCRIPT_ANCHOR_CLASS } from "./transcriptMarkdownLinks";
8
8
  import { HighlightText } from "./transcriptSearch";
9
9
 
@@ -22,16 +22,17 @@ const TranscriptMentionPeople = createContext<
22
22
  ReadonlyMap<string, ActorIdentity>
23
23
  >(new Map());
24
24
 
25
- /** Share the people that the API resolved for mentions in one transcript. */
26
- export function TranscriptMentionPeopleProvider(props: {
25
+ /**
26
+ * Share the people that the API resolved for one block of display text.
27
+ * Use `TranscriptText` or `MentionText`, which require the API mentions.
28
+ */
29
+ export function MentionPeopleProvider(props: {
27
30
  children: ReactNode;
28
- messages: readonly TranscriptViewMessage[];
31
+ mentions: readonly ActorIdentity[];
29
32
  }) {
30
33
  const people = new Map<string, ActorIdentity>();
31
- for (const message of props.messages) {
32
- for (const person of message.mentions ?? []) {
33
- if (person.slackUserId) people.set(person.slackUserId, person);
34
- }
34
+ for (const person of props.mentions) {
35
+ if (person.slackUserId) people.set(person.slackUserId, person);
35
36
  }
36
37
  return (
37
38
  <TranscriptMentionPeople.Provider value={people}>
@@ -40,7 +41,22 @@ export function TranscriptMentionPeopleProvider(props: {
40
41
  );
41
42
  }
42
43
 
43
- /** Render text with Slack `<@U123>` and `<#C123|name>` tokens as readable mentions. */
44
+ /** Render one line of display text with the people the API resolved for it. */
45
+ export function MentionText(props: {
46
+ mentions: readonly ActorIdentity[];
47
+ text: string;
48
+ }) {
49
+ return (
50
+ <MentionPeopleProvider mentions={props.mentions}>
51
+ {renderMentionText(props.text, "mention-text")}
52
+ </MentionPeopleProvider>
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Render text with Slack `<@U123>` and `<#C123|name>` tokens as readable
58
+ * mentions. It reads people from the nearest `MentionPeopleProvider`.
59
+ */
44
60
  export function renderMentionText(text: string, keyBase: string): ReactNode[] {
45
61
  const nodes: ReactNode[] = [];
46
62
  let cursor = 0;
@@ -88,7 +104,8 @@ function MentionView(props: { mention: TranscriptMention }) {
88
104
  if (person?.email) {
89
105
  return (
90
106
  <Link
91
- className={TRANSCRIPT_ANCHOR_CLASS}
107
+ // Previews sit in cards that disable pointer events on their text.
108
+ className={cn("pointer-events-auto", TRANSCRIPT_ANCHOR_CLASS)}
92
109
  title={id}
93
110
  to={peoplePath(person.email)}
94
111
  >
@@ -6,15 +6,23 @@ import { Card } from "../../components/layout/Card";
6
6
  import { PageHeader } from "../../components/layout/PageHeader";
7
7
  import { dashboardContainerClass } from "../../styles";
8
8
  import { pluginUserPagePath } from "../user/PluginUserPage";
9
+ import {
10
+ ServiceAccountsSection,
11
+ useServiceAccounts,
12
+ } from "./ServiceAccountsSection";
9
13
 
10
14
  /**
11
- * List admin-only plugin pages for a Junior admin.
15
+ * Show service accounts and admin-only plugin pages to a Junior admin.
12
16
  *
13
- * The server hides admin pages from other viewers and refuses to serve them,
14
- * so this page only arranges links.
17
+ * The server hides admin pages and service accounts from other viewers and
18
+ * refuses their requests, so this page only arranges them.
15
19
  */
16
20
  export function AdminPage(props: { pages: PluginUserPageLink[] }) {
17
21
  const adminPages = props.pages.filter((page) => page.navigation === "admin");
22
+ const serviceAccounts = useServiceAccounts();
23
+ const nothingToSet =
24
+ adminPages.length === 0 &&
25
+ serviceAccounts.data?.serviceAccounts.length === 0;
18
26
  return (
19
27
  <div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
20
28
  <section className="mx-auto grid w-full max-w-3xl gap-6">
@@ -28,7 +36,8 @@ export function AdminPage(props: { pages: PluginUserPageLink[] }) {
28
36
  }
29
37
  title="Admin"
30
38
  />
31
- {adminPages.length === 0 ? (
39
+ <ServiceAccountsSection accountsQuery={serviceAccounts} />
40
+ {nothingToSet ? (
32
41
  <Card padding="md">
33
42
  <div className="flex items-center gap-4">
34
43
  <div className="grid size-10 shrink-0 place-items-center rounded-md border border-dashboard-border bg-dashboard-fill-soft text-dashboard-text-muted">
@@ -39,7 +48,7 @@ export function AdminPage(props: { pages: PluginUserPageLink[] }) {
39
48
  </p>
40
49
  </div>
41
50
  </Card>
42
- ) : (
51
+ ) : adminPages.length === 0 ? null : (
43
52
  <div className="grid gap-3">
44
53
  {adminPages.map((page) => (
45
54
  <Link
@@ -0,0 +1,231 @@
1
+ import {
2
+ useMutation,
3
+ useQuery,
4
+ useQueryClient,
5
+ type UseQueryResult,
6
+ } from "@tanstack/react-query";
7
+ import { KeyRound } from "lucide-react";
8
+ import type { z } from "zod";
9
+ import {
10
+ disconnectServiceAccountResponseSchema,
11
+ serviceAccountListSchema,
12
+ startServiceAccountSignInResponseSchema,
13
+ type serviceAccountSchema,
14
+ } from "@sentry/junior/api/schema";
15
+ import { getDashboardAgentName } from "../../agentName";
16
+ import { Button } from "../../components/Button";
17
+ import { InlineError } from "../../components/InlineError";
18
+ import { StatusChip } from "../../components/StatusChip";
19
+ import { Card } from "../../components/layout/Card";
20
+ import { formatRelativeTime } from "../../format";
21
+ import { del, fetchDashboardJson, post } from "../../http";
22
+
23
+ type ServiceAccount = z.infer<typeof serviceAccountSchema>;
24
+
25
+ const serviceAccountsQueryKey = ["dashboard", "service-accounts"] as const;
26
+
27
+ function serviceAccountPath(provider: string): string {
28
+ return `/api/service-accounts/${encodeURIComponent(provider)}`;
29
+ }
30
+
31
+ /** Load the plugins that support a service account, with their status. */
32
+ export function useServiceAccounts() {
33
+ return useQuery({
34
+ queryKey: serviceAccountsQueryKey,
35
+ queryFn: ({ signal }) =>
36
+ fetchDashboardJson(
37
+ serviceAccountListSchema,
38
+ "/api/service-accounts",
39
+ signal,
40
+ ),
41
+ // OAuth finishes in another tab. Refresh when the admin returns.
42
+ refetchOnWindowFocus: true,
43
+ retry: false,
44
+ });
45
+ }
46
+
47
+ function statusText(account: ServiceAccount): string {
48
+ const usage = !account.userOAuth
49
+ ? `Everyone uses this account. People cannot connect their own ${account.displayName} account.`
50
+ : `People who have not connected their own ${account.displayName} account use this account.`;
51
+ if (!account.connected) {
52
+ const blocked = !account.userOAuth
53
+ ? ` ${account.displayName} is not available until you connect it.`
54
+ : "";
55
+ return `Not connected.${blocked} ${usage} Sign in as the shared account, not as yourself.`;
56
+ }
57
+ const by = account.connectedBy ?? "the CLI";
58
+ return `Connected by ${by} ${formatRelativeTime(account.connectedAt)}. ${usage}`;
59
+ }
60
+
61
+ /**
62
+ * Connect and disconnect the shared provider accounts that Junior uses for
63
+ * people who have not connected their own account.
64
+ */
65
+ export function ServiceAccountsSection(props: {
66
+ accountsQuery: UseQueryResult<z.infer<typeof serviceAccountListSchema>>;
67
+ }) {
68
+ const { accountsQuery } = props;
69
+ const queryClient = useQueryClient();
70
+ const connectMutation = useMutation({
71
+ mutationFn: (input: { provider: string; popup: Window | null }) =>
72
+ post(
73
+ startServiceAccountSignInResponseSchema,
74
+ serviceAccountPath(input.provider),
75
+ {},
76
+ ),
77
+ onSuccess: ({ authorizationUrl }, { popup }) => {
78
+ // A blocked popup returns null, so use this tab. A tab the admin
79
+ // closed means they cancelled; keep them on the dashboard.
80
+ if (!popup) {
81
+ window.location.assign(authorizationUrl);
82
+ return;
83
+ }
84
+ if (popup.closed) return;
85
+ popup.opener = null;
86
+ popup.location.href = authorizationUrl;
87
+ },
88
+ onError: (_error, { popup }) => {
89
+ popup?.close();
90
+ },
91
+ });
92
+ const disconnectMutation = useMutation({
93
+ mutationFn: (account: ServiceAccount) =>
94
+ del(
95
+ disconnectServiceAccountResponseSchema,
96
+ serviceAccountPath(account.provider),
97
+ ),
98
+ onSuccess: () =>
99
+ queryClient.invalidateQueries({ queryKey: serviceAccountsQueryKey }),
100
+ });
101
+
102
+ const accounts = accountsQuery.data?.serviceAccounts ?? [];
103
+ if (accountsQuery.isSuccess && accounts.length === 0) {
104
+ return null;
105
+ }
106
+ const busy = connectMutation.isPending || disconnectMutation.isPending;
107
+ const pendingProvider = connectMutation.isPending
108
+ ? connectMutation.variables?.provider
109
+ : disconnectMutation.isPending
110
+ ? disconnectMutation.variables?.provider
111
+ : undefined;
112
+ const listFailed = !accountsQuery.data && accountsQuery.isError;
113
+ const displayedError = connectMutation.isError
114
+ ? "Could not start the sign-in. Try again."
115
+ : disconnectMutation.isError
116
+ ? "Could not disconnect the account. Try again."
117
+ : listFailed
118
+ ? "Could not load service accounts. Try again."
119
+ : undefined;
120
+
121
+ return (
122
+ <Card as="section" padding="md">
123
+ <h2 className="m-0 font-display text-base font-medium text-dashboard-text">
124
+ Service accounts
125
+ </h2>
126
+ <p className="mt-1 mb-4 text-sm text-dashboard-text-muted">
127
+ A shared account that Junior uses for a plugin. It can do only what its
128
+ plugin allows, usually read. Everyone who can talk to{" "}
129
+ {getDashboardAgentName()} can use it, so give it only access that all of
130
+ them may have.
131
+ </p>
132
+ {displayedError ? (
133
+ <InlineError className={listFailed ? undefined : "mb-4"}>
134
+ {displayedError}
135
+ </InlineError>
136
+ ) : null}
137
+ {listFailed ? null : accountsQuery.isPending ? (
138
+ <p className="m-0 text-sm text-dashboard-text-muted">
139
+ Loading service accounts…
140
+ </p>
141
+ ) : (
142
+ <div className="space-y-2">
143
+ {accounts.map((account) => (
144
+ <div
145
+ className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
146
+ key={account.provider}
147
+ >
148
+ <KeyRound
149
+ aria-hidden="true"
150
+ className="shrink-0 text-dashboard-accent"
151
+ size={16}
152
+ />
153
+ <div className="min-w-0 flex-1">
154
+ <div className="flex flex-wrap items-center gap-2">
155
+ <p className="m-0 truncate text-sm font-semibold">
156
+ {account.displayName}
157
+ </p>
158
+ <StatusChip
159
+ size="compact"
160
+ tone={
161
+ account.connected
162
+ ? "success"
163
+ : !account.userOAuth
164
+ ? "warning"
165
+ : "neutral"
166
+ }
167
+ >
168
+ {account.connected ? "connected" : "not connected"}
169
+ </StatusChip>
170
+ {!account.userOAuth ? (
171
+ <StatusChip size="compact" tone="neutral">
172
+ shared only
173
+ </StatusChip>
174
+ ) : null}
175
+ </div>
176
+ <p className="mt-1 mb-0 break-words text-xs text-dashboard-text-muted">
177
+ {statusText(account)}
178
+ </p>
179
+ <p className="mt-1 mb-0 break-words font-mono text-xs text-dashboard-text-faint">
180
+ {account.tools.join(", ")}
181
+ </p>
182
+ </div>
183
+ {account.connected ? (
184
+ <Button
185
+ aria-busy={
186
+ disconnectMutation.isPending &&
187
+ pendingProvider === account.provider
188
+ }
189
+ aria-label={`Disconnect the ${account.displayName} service account`}
190
+ disabled={busy}
191
+ onClick={() => {
192
+ connectMutation.reset();
193
+ disconnectMutation.mutate(account);
194
+ }}
195
+ tone="danger"
196
+ >
197
+ Disconnect
198
+ </Button>
199
+ ) : null}
200
+ <Button
201
+ aria-busy={
202
+ connectMutation.isPending &&
203
+ pendingProvider === account.provider
204
+ }
205
+ aria-label={`${account.connected ? "Reconnect" : "Connect"} the ${account.displayName} service account`}
206
+ disabled={busy}
207
+ onClick={() => {
208
+ disconnectMutation.reset();
209
+ // Open the tab during the click so popup blockers allow
210
+ // it, then point it at the provider.
211
+ const popup = window.open("about:blank", "_blank");
212
+ connectMutation.mutate({
213
+ provider: account.provider,
214
+ popup,
215
+ });
216
+ }}
217
+ >
218
+ {connectMutation.isPending &&
219
+ pendingProvider === account.provider
220
+ ? "Connecting…"
221
+ : account.connected
222
+ ? "Reconnect"
223
+ : "Connect"}
224
+ </Button>
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </Card>
230
+ );
231
+ }
@@ -18,7 +18,7 @@ import {
18
18
  import { Card } from "../../components/layout/Card";
19
19
  import { formatCostSummary } from "../../format";
20
20
 
21
- const COST_COLOR = "#67e8f9";
21
+ const COST_COLOR = "var(--color-cyan-300)";
22
22
 
23
23
  /** Render linked conversation spend for completed automation executions. */
24
24
  export function AutomationCostChart(props: {
@@ -103,7 +103,7 @@ export function AutomationCostChart(props: {
103
103
  format={(value) => formatCostSummary({ total: value })}
104
104
  layout={layout}
105
105
  maximum={maximum}
106
- stroke="#e2e8f0"
106
+ stroke="var(--color-slate-200)"
107
107
  />
108
108
  <ActivityChartDateLabels
109
109
  dates={days.map((day) => day.date)}
@@ -88,7 +88,10 @@ export function AutomationDetailsDrawer(props: {
88
88
  <h3 className="mt-0 mb-2 text-sm font-medium text-dashboard-text-muted">
89
89
  What to do
90
90
  </h3>
91
- <TranscriptText text={automation.instruction} />
91
+ <TranscriptText
92
+ mentions={automation.mentions ?? []}
93
+ text={automation.instruction}
94
+ />
92
95
  </div>
93
96
  <AutomationLifecycle
94
97
  key={`${automation.kind}:${automation.id}`}
@@ -18,7 +18,7 @@ import {
18
18
  import { Card } from "../../components/layout/Card";
19
19
  import { formatActivityChartAverage } from "../../format";
20
20
 
21
- const EXECUTION_COLOR = "#fbbf24";
21
+ const EXECUTION_COLOR = "var(--color-amber-400)";
22
22
 
23
23
  /** Render completed automation executions over a trailing window. */
24
24
  export function AutomationExecutionChart(props: {
@@ -95,7 +95,7 @@ export function AutomationExecutionChart(props: {
95
95
  format={formatActivityChartAverage}
96
96
  layout={layout}
97
97
  maximum={maximum}
98
- stroke="#e2e8f0"
98
+ stroke="var(--color-slate-200)"
99
99
  />
100
100
  <ActivityChartDateLabels
101
101
  dates={days.map((day) => day.date)}
@@ -15,9 +15,9 @@ import {
15
15
  import { Card } from "../../components/layout/Card";
16
16
 
17
17
  const series = [
18
- { color: "#6ee7b7", key: "completed", label: "Completed" },
19
- { color: "#fda4af", key: "failed", label: "Failed" },
20
- { color: "#fcd34d", key: "blocked", label: "Blocked" },
18
+ { color: "var(--color-emerald-300)", key: "completed", label: "Completed" },
19
+ { color: "var(--color-rose-300)", key: "failed", label: "Failed" },
20
+ { color: "var(--color-amber-300)", key: "blocked", label: "Blocked" },
21
21
  ] as const;
22
22
 
23
23
  /** Render one automation's terminal executions by status over a trailing window. */
@@ -19,9 +19,9 @@ import { ChartLegend } from "../../components/charts/ChartLegend";
19
19
  import { Card } from "../../components/layout/Card";
20
20
  import { formatActivityChartAverage } from "../../format";
21
21
 
22
- const CREATED_COLOR = "#67e8f9";
23
- const MERGED_COLOR = "#6ee7b7";
24
- const CLOSED_COLOR = "#fb7185";
22
+ const CREATED_COLOR = "var(--color-cyan-300)";
23
+ const MERGED_COLOR = "var(--color-emerald-300)";
24
+ const CLOSED_COLOR = "var(--color-rose-400)";
25
25
 
26
26
  /** Render created, merged, and closed code changes over a trailing window. */
27
27
  export function CodeActivityChart(props: {
@@ -143,7 +143,7 @@ export function CodeActivityChart(props: {
143
143
  format={formatActivityChartAverage}
144
144
  layout={layout}
145
145
  maximum={maximum}
146
- stroke="#e2e8f0"
146
+ stroke="var(--color-slate-200)"
147
147
  />
148
148
  <ActivityChartDateLabels
149
149
  dates={days.map((day) => day.date)}
@@ -50,6 +50,7 @@ import { StatCard } from "../../components/metrics/StatCard";
50
50
  import { StatusChip } from "../../components/StatusChip";
51
51
  import { StatusDot } from "../../components/StatusDot";
52
52
  import { TextArea, TextInput } from "../../components/TextInput";
53
+ import { ThemeSwitcher } from "../../components/ThemeSwitcher";
53
54
  import { TranscriptMarkdown } from "../../conversations/TranscriptMarkdown";
54
55
  import { TranscriptText } from "../../conversations/TranscriptText";
55
56
  import { TranscriptMessageShell } from "../../conversations/TranscriptMessageShell";
@@ -231,7 +232,6 @@ function FoundationsGalleryPage() {
231
232
  <Fixture title="Conversation navigation">
232
233
  <DashboardChromeProvider>
233
234
  <DashboardHeader
234
- compact
235
235
  mobileNavigationOpen={navigationOpen}
236
236
  navItems={[
237
237
  { key: "code", label: "Code", to: "/code" },
@@ -301,6 +301,9 @@ function FoundationsGalleryPage() {
301
301
  </Drawer>
302
302
  ) : null}
303
303
  </Fixture>
304
+ <Fixture title="Theme">
305
+ <ThemeSwitcher className="max-w-64" />
306
+ </Fixture>
304
307
  <Fixture title="Color tokens">
305
308
  <div className="grid gap-4">
306
309
  <TokenSwatchRow
@@ -733,6 +736,7 @@ function TranscriptsGalleryPage() {
733
736
  Junior
734
737
  </span>
735
738
  <TranscriptText
739
+ mentions={[]}
736
740
  role="assistant"
737
741
  text={
738
742
  "Your reminder is set.\n\nI'll remind you to drink water every 30 minutes until you ask me to stop."
@@ -793,10 +797,10 @@ function TranscriptsGalleryPage() {
793
797
  <TranscriptMarkdown text={GFM_SAMPLE} />
794
798
  </Fixture>
795
799
  <Fixture title="TranscriptText assistant">
796
- <TranscriptText role="assistant" text={MIXED_ASSISTANT} />
800
+ <TranscriptText mentions={[]} role="assistant" text={MIXED_ASSISTANT} />
797
801
  </Fixture>
798
802
  <Fixture title="TranscriptText user">
799
- <TranscriptText role="user" text={EVENT_NOTIFICATION} />
803
+ <TranscriptText mentions={[]} role="user" text={EVENT_NOTIFICATION} />
800
804
  </Fixture>
801
805
  <Fixture title="Tool calls">
802
806
  <ToolCallGallery />
@@ -107,6 +107,35 @@ const EVENTS: ConversationReportEventData[] = [
107
107
  trustedSummary: "GitHub PR getsentry/junior#1200 received a review.",
108
108
  text: "The preview is ready for visual review.",
109
109
  },
110
+ {
111
+ type: "message",
112
+ messageId: "gallery-event-batch",
113
+ role: "user",
114
+ eventType: "pull_request.review.approved",
115
+ eventObjectType: "code_change",
116
+ trustedSummary: "GitHub PR getsentry/junior#1200 was approved by dcramer.",
117
+ events: [
118
+ {
119
+ eventType: "pull_request.review.approved",
120
+ label: "GitHub PR getsentry/junior#1200",
121
+ trustedSummary:
122
+ "GitHub PR getsentry/junior#1200 was approved by dcramer.",
123
+ },
124
+ {
125
+ eventType: "pull_request.review.commented",
126
+ label: "GitHub PR getsentry/junior#1200",
127
+ trustedSummary:
128
+ "GitHub PR getsentry/junior#1200 received a review comment from cursor[bot].",
129
+ },
130
+ {
131
+ eventType: "pull_request.review_comment.created",
132
+ label: "GitHub PR getsentry/junior#1200",
133
+ trustedSummary:
134
+ "GitHub PR getsentry/junior#1200 received an inline review comment from cursor[bot].",
135
+ },
136
+ ],
137
+ text: "[task]\n\nThis is a task, not a message from a person.\n\nYou received 3 events.",
138
+ },
110
139
  ];
111
140
 
112
141
  const CONVERSATION: ConversationDetailReport = {