@sentry/junior-dashboard 0.259.0 → 0.261.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 (71) 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 +6 -6
  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 +14 -3
  12. package/src/client/components/Select.tsx +41 -7
  13. package/src/client/components/ThemeSwitcher.tsx +48 -0
  14. package/src/client/components/charts/ActivityChart.tsx +6 -7
  15. package/src/client/components/charts/InputCacheChart.tsx +1 -1
  16. package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
  17. package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
  18. package/src/client/components/charts/cache-input.ts +11 -3
  19. package/src/client/components/controls/DirectorySortSelect.tsx +6 -4
  20. package/src/client/components/layout/DashboardHeader.tsx +2 -9
  21. package/src/client/conversations/ConversationBrief.tsx +35 -9
  22. package/src/client/conversations/ConversationHomeList.tsx +7 -1
  23. package/src/client/conversations/ConversationMeta.tsx +19 -7
  24. package/src/client/conversations/ConversationTranscript.tsx +8 -11
  25. package/src/client/conversations/ConversationWorkspace.tsx +9 -12
  26. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -4
  27. package/src/client/conversations/EventDetails.tsx +9 -5
  28. package/src/client/conversations/README.md +6 -0
  29. package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
  30. package/src/client/conversations/TranscriptEventView.tsx +48 -9
  31. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  32. package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
  33. package/src/client/conversations/TranscriptText.tsx +28 -20
  34. package/src/client/conversations/conversationActivityPreview.ts +6 -0
  35. package/src/client/conversations/eventTranscript.ts +4 -1
  36. package/src/client/conversations/transcriptMentions.tsx +27 -10
  37. package/src/client/debouncedValue.ts +14 -0
  38. package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
  39. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
  40. package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
  41. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
  42. package/src/client/pages/automations/AutomationFilters.tsx +4 -3
  43. package/src/client/pages/code/CodeActivityChart.tsx +4 -4
  44. package/src/client/pages/code/RepositoryTabs.tsx +3 -2
  45. package/src/client/pages/dev/ComponentsPage.tsx +11 -3
  46. package/src/client/pages/dev/ConversationFixture.tsx +29 -0
  47. package/src/client/pages/dev/SelectFixtures.tsx +62 -0
  48. package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
  49. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
  50. package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
  51. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
  52. package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
  53. package/src/client/pages/people/ContributionGrid.tsx +8 -8
  54. package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
  55. package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
  56. package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
  57. package/src/client/pages/system/PluginBarChart.tsx +12 -6
  58. package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
  59. package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
  60. package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
  61. package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
  62. package/src/client/pages/system/WorkspaceList.tsx +6 -1
  63. package/src/client/pages/system/WorkspacesPage.tsx +18 -12
  64. package/src/client/pages/system/workspaceDraft.ts +8 -0
  65. package/src/client/theme.ts +52 -0
  66. package/src/client/types.ts +8 -1
  67. package/src/mock-reporting/fixtures.ts +48 -35
  68. package/src/shell.ts +4 -1
  69. package/src/tailwind.css +109 -0
  70. package/src/theme.ts +49 -0
  71. package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
@@ -10,6 +10,7 @@ import { EventUsage } from "./EventUsage";
10
10
  /** Present event content as readable sections, with the exact report available on demand. */
11
11
  export function EventDetails({ event }: { event: ConversationReportEvent }) {
12
12
  const data = event.data;
13
+ const mentions = event.mentions ?? [];
13
14
  const model = eventLogModel(event);
14
15
  const { usage, ...callAttributes } = event.modelCall ?? {};
15
16
  let content: ReactNode;
@@ -54,7 +55,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
54
55
  ) : null}
55
56
  {summary ? (
56
57
  <DetailSection title="Continuation summary">
57
- <TranscriptText text={summary} />
58
+ <TranscriptText mentions={mentions} text={summary} />
58
59
  </DetailSection>
59
60
  ) : null}
60
61
  </>
@@ -70,7 +71,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
70
71
  {redacted ? (
71
72
  <RedactedMarker />
72
73
  ) : (
73
- <TranscriptText text={text ?? ""} />
74
+ <TranscriptText mentions={mentions} text={text ?? ""} />
74
75
  )}
75
76
  </DetailSection>
76
77
  </>
@@ -112,7 +113,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
112
113
  {part.redacted ? (
113
114
  <RedactedMarker />
114
115
  ) : (
115
- <TranscriptText text={part.text ?? ""} />
116
+ <TranscriptText mentions={mentions} text={part.text ?? ""} />
116
117
  )}
117
118
  </DetailSection>
118
119
  ));
@@ -124,12 +125,15 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
124
125
  <DetailValue value={metadata} />
125
126
  <DetailSection title={presentation.title}>
126
127
  {presentation.preview ? (
127
- <TranscriptText text={presentation.preview} />
128
+ <TranscriptText mentions={mentions} text={presentation.preview} />
128
129
  ) : null}
129
130
  {presentation.details?.map((detail, index) => (
130
131
  <DetailSection key={index} title={detail.title}>
131
132
  {detail.description ? (
132
- <TranscriptText text={detail.description} />
133
+ <TranscriptText
134
+ mentions={mentions}
135
+ text={detail.description}
136
+ />
133
137
  ) : null}
134
138
  {detail.content ? <DetailValue value={detail.content} /> : null}
135
139
  {detail.metadata ? (
@@ -34,6 +34,12 @@ shared controls and overlays.
34
34
  a person, so it stays collapsed behind "Show prompt".
35
35
  - `TranscriptAttachments` owns wrapping for both message attachments and file
36
36
  deliveries. `ImageAttachment` owns image previews.
37
+ - Display text keeps Slack tokens such as `<@U123>` and `<#C123|name>`. The
38
+ API resolves the people in that text. It returns them as `mentions` next to
39
+ the object that owns the text: an event, an activity preview, a Brief, or an
40
+ Automation. Render this text with `TranscriptText` or `MentionText`. Both
41
+ require `mentions`, so a caller cannot forget them. Do not resolve names in
42
+ the dashboard, and do not rewrite tokens into links on the server.
37
43
  - `TranscriptTurnContextView` owns context content. The shared `Drawer` owns
38
44
  dismissal, focus trapping, focus return, scroll locking, and panel layout.
39
45
 
@@ -44,7 +44,11 @@ export function TranscriptContextEventView(props: {
44
44
  Continuation summary
45
45
  </summary>
46
46
  <div className="mt-1.5 whitespace-pre-wrap text-xs leading-relaxed text-dashboard-text-muted">
47
- <TranscriptText role="system" text={event.summary} />
47
+ <TranscriptText
48
+ mentions={props.part.mentions ?? []}
49
+ role="system"
50
+ text={event.summary}
51
+ />
48
52
  </div>
49
53
  </details>
50
54
  ) : null}
@@ -1,28 +1,67 @@
1
1
  import { ChevronRight } from "lucide-react";
2
2
 
3
- import { renderMentionText } from "./transcriptMentions";
3
+ import { MentionText } from "./transcriptMentions";
4
4
  import { messageRawText } from "./transcriptRenderModel";
5
5
  import { HighlightText } from "./transcriptSearch";
6
6
  import { RedactedMarker } from "./TranscriptRedacted";
7
7
  import type { TranscriptViewMessage } from "../types";
8
8
  import { TranscriptSummary } from "./TranscriptSummary";
9
9
 
10
+ function EventTypeChip(props: { eventType: string }) {
11
+ return (
12
+ <span className="break-all rounded-md border border-dashboard-border px-1.5 py-0.5 font-mono text-2xs leading-none text-dashboard-text-muted">
13
+ <HighlightText text={props.eventType} />
14
+ </span>
15
+ );
16
+ }
17
+
18
+ /** Heading for several Events that arrived in one message. */
19
+ function batchSummary(events: readonly { label?: string }[]): string {
20
+ // Name the resource only when every Event is about it.
21
+ const label = events[0]?.label;
22
+ const shared = label && events.every((event) => event.label === label);
23
+ return `${events.length} events${shared ? ` · ${label}` : ""}`;
24
+ }
25
+
10
26
  /** Render an Event Message whose heading expands its full text. */
11
27
  export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
12
28
  const text = messageRawText(props.message);
13
29
  const redacted = props.message.parts.some(
14
30
  (part) => part.type === "text" && part.redacted,
15
31
  );
16
- const summary = props.message.trustedSummary ?? props.message.eventType ?? "";
32
+ const events = props.message.events ?? [];
33
+ const mentions = props.message.mentions ?? [];
34
+ const isBatch = events.length > 1;
35
+ const summary = isBatch
36
+ ? batchSummary(events)
37
+ : (props.message.trustedSummary ?? props.message.eventType ?? "");
17
38
  const heading = (
18
- <span className="flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
19
- <span className="min-w-0 break-words font-display text-sm font-semibold leading-6 text-dashboard-text">
20
- {renderMentionText(summary, "event-summary")}
21
- </span>
22
- {props.message.eventType ? (
23
- <span className="break-all rounded-md border border-dashboard-border px-1.5 py-0.5 font-mono text-2xs leading-none text-dashboard-text-muted">
24
- <HighlightText text={props.message.eventType} />
39
+ <span className="flex min-w-0 flex-col gap-1">
40
+ <span className="flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
41
+ <span className="min-w-0 break-words font-display text-sm font-semibold leading-6 text-dashboard-text">
42
+ <MentionText mentions={mentions} text={summary} />
25
43
  </span>
44
+ {!isBatch && props.message.eventType ? (
45
+ <EventTypeChip eventType={props.message.eventType} />
46
+ ) : null}
47
+ </span>
48
+ {isBatch ? (
49
+ <ul className="flex min-w-0 flex-col gap-1">
50
+ {events.map((event, index) => (
51
+ <li
52
+ key={index}
53
+ className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-5 text-dashboard-text-muted"
54
+ >
55
+ <span className="min-w-0 break-words">
56
+ <MentionText
57
+ mentions={mentions}
58
+ text={event.trustedSummary ?? event.eventType}
59
+ />
60
+ </span>
61
+ <EventTypeChip eventType={event.eventType} />
62
+ </li>
63
+ ))}
64
+ </ul>
26
65
  ) : null}
27
66
  </span>
28
67
  );
@@ -54,7 +54,12 @@ export const TranscriptMessageView = memo(
54
54
  <div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
55
55
  {props.message.parts.map((part, index) =>
56
56
  part.type === "text" ? (
57
- <TranscriptText key={index} role={role} text={part.text ?? ""} />
57
+ <TranscriptText
58
+ key={index}
59
+ mentions={props.message.mentions ?? []}
60
+ role={role}
61
+ text={part.text ?? ""}
62
+ />
58
63
  ) : null,
59
64
  )}
60
65
  </div>
@@ -1,5 +1,6 @@
1
1
  import { formatMessageTimestamp } from "../format";
2
2
  import type { TranscriptViewStructuredEventPart } from "../types";
3
+ import { MentionText } from "./transcriptMentions";
3
4
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
4
5
  import { TranscriptSummary } from "./TranscriptSummary";
5
6
 
@@ -14,6 +15,7 @@ export function TranscriptStructuredEventView(props: {
14
15
  const presentation = props.part.presentation;
15
16
  const timestamp = formatMessageTimestamp(props.timestamp);
16
17
  const details = presentation.details ?? [];
18
+ const mentions = props.part.mentions ?? [];
17
19
  // The rail icon box is h-6. A leading-6 title shares its center line.
18
20
  const surfaceClass = "min-w-0 font-sans text-xs";
19
21
  // A lone detail often repeats the event title, such as "Loaded AGENTS.md"
@@ -32,7 +34,7 @@ export function TranscriptStructuredEventView(props: {
32
34
  )}
33
35
  {detail.description ? (
34
36
  <div className="mt-1 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
35
- <HighlightText text={detail.description} />
37
+ <MentionText mentions={mentions} text={detail.description} />
36
38
  </div>
37
39
  ) : null}
38
40
  {detail.content ? (
@@ -64,7 +66,7 @@ export function TranscriptStructuredEventView(props: {
64
66
  </div>
65
67
  {presentation.preview ? (
66
68
  <div className="mt-0.5 truncate text-xs text-dashboard-text-muted">
67
- <HighlightText text={presentation.preview} />
69
+ <MentionText mentions={mentions} text={presentation.preview} />
68
70
  </div>
69
71
  ) : null}
70
72
  </div>
@@ -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
  >
@@ -0,0 +1,14 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Return `value` once it stops changing for `delayMs`. Use it for search text
5
+ * that is local state, so each keystroke does not send a request.
6
+ */
7
+ export function useDebouncedValue<T>(value: T, delayMs = 250): T {
8
+ const [debounced, setDebounced] = useState(value);
9
+ useEffect(() => {
10
+ const timeout = window.setTimeout(() => setDebounced(value), delayMs);
11
+ return () => window.clearTimeout(timeout);
12
+ }, [delayMs, value]);
13
+ return debounced;
14
+ }
@@ -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. */
@@ -9,6 +9,7 @@ import { Button, ToggleButton } from "../../components/Button";
9
9
  import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
10
10
  import { FilterGroup } from "../../components/FilterBar";
11
11
  import { SearchInput } from "../../components/SearchInput";
12
+ import { Select } from "../../components/Select";
12
13
 
13
14
  /** Keep collection controls visible while a filtered page loads. */
14
15
  export function AutomationFilters(props: {
@@ -157,9 +158,9 @@ function AutomationFilterSelect(props: {
157
158
  }) {
158
159
  return (
159
160
  <FilterGroup label={props.label}>
160
- <select
161
+ <Select
161
162
  aria-label={props.ariaLabel ?? `Filter by ${props.label.toLowerCase()}`}
162
- className="h-9 w-full min-w-0 rounded-lg border border-dashboard-border-emphasis bg-dashboard-control px-2 text-sm text-dashboard-text transition-colors hover:border-dashboard-border-interactive focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25 [color-scheme:var(--dashboard-color-scheme,dark)]"
163
+ variant="filter"
163
164
  value={props.value}
164
165
  onChange={(event) => props.onChange(event.currentTarget.value)}
165
166
  >
@@ -173,7 +174,7 @@ function AutomationFilterSelect(props: {
173
174
  {option.label}
174
175
  </option>
175
176
  ))}
176
- </select>
177
+ </Select>
177
178
  </FilterGroup>
178
179
  );
179
180
  }
@@ -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)}
@@ -1,4 +1,4 @@
1
- import { useDeferredValue, useEffect, useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
  import type { CodeRepositoryReport } from "@sentry/junior/api/schema";
3
3
 
4
4
  import { useCodeRepositoryConversationsData } from "../../api";
@@ -17,6 +17,7 @@ import {
17
17
  import { Card } from "../../components/layout/Card";
18
18
  import { ConversationHomeList } from "../../conversations/ConversationHomeList";
19
19
  import { buildConversations, getDashboardTimeZone } from "../../format";
20
+ import { useDebouncedValue } from "../../debouncedValue";
20
21
  import { CodeActivityChart } from "./CodeActivityChart";
21
22
  import { CodeChangeRow } from "./CodeChangeRow";
22
23
  import { repositoryActivity } from "./RepositorySections";
@@ -28,7 +29,7 @@ const noFinishedConversations = new Set<string>();
28
29
  export function RepositoryConversationsTab(props: { repositoryId: string }) {
29
30
  const [query, setQuery] = useState("");
30
31
  const [page, setPage] = useState(1);
31
- const search = useDeferredValue(query.trim());
32
+ const search = useDebouncedValue(query.trim());
32
33
  const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
33
34
  const conversations = buildConversations(feed.data?.conversations ?? []);
34
35
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  import { FormNotice } from "../../components/FormNotice";
2
2
  import { SearchInput } from "../../components/SearchInput";
3
3
  import { Select } from "../../components/Select";
4
+ import { SelectFixtures } from "./SelectFixtures";
4
5
  import { ObjectFixtures } from "./ObjectFixtures";
5
6
  import { ChartFixtures } from "./ChartFixtures";
6
7
  import { SegmentedNavFixture, SegmentedTabsFixture } from "./SegmentedFixtures";
@@ -50,6 +51,7 @@ import { StatCard } from "../../components/metrics/StatCard";
50
51
  import { StatusChip } from "../../components/StatusChip";
51
52
  import { StatusDot } from "../../components/StatusDot";
52
53
  import { TextArea, TextInput } from "../../components/TextInput";
54
+ import { ThemeSwitcher } from "../../components/ThemeSwitcher";
53
55
  import { TranscriptMarkdown } from "../../conversations/TranscriptMarkdown";
54
56
  import { TranscriptText } from "../../conversations/TranscriptText";
55
57
  import { TranscriptMessageShell } from "../../conversations/TranscriptMessageShell";
@@ -228,10 +230,12 @@ function FoundationsGalleryPage() {
228
230
  Your edits are still here. Review the latest version before saving.
229
231
  </FormNotice>
230
232
  </Fixture>
233
+ <Fixture title="Select controls">
234
+ <SelectFixtures />
235
+ </Fixture>
231
236
  <Fixture title="Conversation navigation">
232
237
  <DashboardChromeProvider>
233
238
  <DashboardHeader
234
- compact
235
239
  mobileNavigationOpen={navigationOpen}
236
240
  navItems={[
237
241
  { key: "code", label: "Code", to: "/code" },
@@ -301,6 +305,9 @@ function FoundationsGalleryPage() {
301
305
  </Drawer>
302
306
  ) : null}
303
307
  </Fixture>
308
+ <Fixture title="Theme">
309
+ <ThemeSwitcher className="max-w-64" />
310
+ </Fixture>
304
311
  <Fixture title="Color tokens">
305
312
  <div className="grid gap-4">
306
313
  <TokenSwatchRow
@@ -733,6 +740,7 @@ function TranscriptsGalleryPage() {
733
740
  Junior
734
741
  </span>
735
742
  <TranscriptText
743
+ mentions={[]}
736
744
  role="assistant"
737
745
  text={
738
746
  "Your reminder is set.\n\nI'll remind you to drink water every 30 minutes until you ask me to stop."
@@ -793,10 +801,10 @@ function TranscriptsGalleryPage() {
793
801
  <TranscriptMarkdown text={GFM_SAMPLE} />
794
802
  </Fixture>
795
803
  <Fixture title="TranscriptText assistant">
796
- <TranscriptText role="assistant" text={MIXED_ASSISTANT} />
804
+ <TranscriptText mentions={[]} role="assistant" text={MIXED_ASSISTANT} />
797
805
  </Fixture>
798
806
  <Fixture title="TranscriptText user">
799
- <TranscriptText role="user" text={EVENT_NOTIFICATION} />
807
+ <TranscriptText mentions={[]} role="user" text={EVENT_NOTIFICATION} />
800
808
  </Fixture>
801
809
  <Fixture title="Tool calls">
802
810
  <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 = {