@sentry/junior-dashboard 0.269.0 → 0.270.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 (55) hide show
  1. package/dist/app.js +2 -2
  2. package/dist/client.js +78 -75
  3. package/dist/index.js +2 -2
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/client/App.tsx +14 -4
  7. package/src/client/components/AutomationCard.tsx +2 -1
  8. package/src/client/components/UserInputForm.tsx +109 -36
  9. package/src/client/conversations/AGENTS.md +0 -4
  10. package/src/client/conversations/ConversationBrief.tsx +55 -36
  11. package/src/client/conversations/ConversationComposer.tsx +27 -8
  12. package/src/client/conversations/ConversationEventLog.tsx +1 -0
  13. package/src/client/conversations/ConversationHomeList.tsx +43 -26
  14. package/src/client/conversations/ConversationPage.tsx +93 -32
  15. package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
  16. package/src/client/conversations/ConversationSidebar.tsx +55 -25
  17. package/src/client/conversations/ConversationTranscript.tsx +3 -2
  18. package/src/client/conversations/ConversationWatches.tsx +4 -1
  19. package/src/client/conversations/ConversationWorkspace.tsx +43 -25
  20. package/src/client/conversations/EventDetails.tsx +6 -9
  21. package/src/client/conversations/PendingMailboxStack.tsx +5 -2
  22. package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
  23. package/src/client/conversations/TranscriptEventView.tsx +9 -3
  24. package/src/client/conversations/TranscriptMessageView.tsx +6 -5
  25. package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
  26. package/src/client/conversations/TranscriptRows.tsx +6 -1
  27. package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
  28. package/src/client/conversations/TranscriptToolView.tsx +2 -1
  29. package/src/client/conversations/TranscriptView.tsx +6 -6
  30. package/src/client/conversations/conversationOutbox.ts +13 -3
  31. package/src/client/conversations/conversationSections.ts +36 -8
  32. package/src/client/conversations/eventTranscript.ts +1 -0
  33. package/src/client/conversations/transcriptBottomPinning.ts +171 -81
  34. package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
  35. package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
  36. package/src/client/display-text/DisplayText.tsx +68 -0
  37. package/src/client/display-text/emoji.ts +54 -0
  38. package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
  39. package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
  40. package/src/client/display-text/mentions.tsx +148 -0
  41. package/src/client/display-text/plainText.ts +29 -0
  42. package/src/client/pages/automations/AutomationFilters.tsx +6 -6
  43. package/src/client/pages/automations/AutomationOverview.tsx +2 -2
  44. package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
  45. package/src/client/pages/code/RepositoryTabs.tsx +6 -1
  46. package/src/client/pages/memory/MemoryDetails.tsx +5 -13
  47. package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
  48. package/src/client/review-desk/ReviewItemActions.tsx +6 -2
  49. package/src/client/review-desk/ReviewItemCard.tsx +58 -17
  50. package/src/client/review-desk/ReviewItemDetail.tsx +2 -2
  51. package/src/client/review-desk/UnsentAnswers.tsx +148 -0
  52. package/src/client/review-desk/reviewItemFormat.ts +29 -1
  53. package/src/client/conversations/TranscriptText.tsx +0 -47
  54. package/src/client/conversations/conversationActivityPreview.ts +0 -16
  55. package/src/client/conversations/transcriptMentions.tsx +0 -134
@@ -13,7 +13,7 @@ import { dashboardDisclosureChevronClass } from "../styles";
13
13
 
14
14
  import { RedactedMarker } from "./TranscriptRedacted";
15
15
  import { DetailsCard } from "./DetailsList";
16
- import { TranscriptText } from "./TranscriptText";
16
+ import { DisplayText } from "../display-text/DisplayText";
17
17
  import { eventLogModel } from "./eventLog";
18
18
  import { formatCostBreakdown } from "../format";
19
19
  import { EventUsage } from "./EventUsage";
@@ -68,7 +68,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
68
68
  ) : null}
69
69
  {summary ? (
70
70
  <DetailSection title="Continuation summary">
71
- <TranscriptText mentions={mentions} text={summary} />
71
+ <DisplayText mentions={mentions} text={summary} />
72
72
  </DetailSection>
73
73
  ) : null}
74
74
  </>
@@ -86,7 +86,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
86
86
  {redacted ? (
87
87
  <RedactedMarker />
88
88
  ) : (
89
- <TranscriptText mentions={mentions} text={text ?? ""} />
89
+ <DisplayText mentions={mentions} text={text ?? ""} />
90
90
  )}
91
91
  </DetailSection>
92
92
  </>
@@ -128,7 +128,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
128
128
  {part.redacted ? (
129
129
  <RedactedMarker />
130
130
  ) : (
131
- <TranscriptText mentions={mentions} text={part.text ?? ""} />
131
+ <DisplayText mentions={mentions} text={part.text ?? ""} />
132
132
  )}
133
133
  </DetailSection>
134
134
  ));
@@ -142,15 +142,12 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
142
142
  </DetailSection>
143
143
  <DetailSection title={presentation.title}>
144
144
  {presentation.preview ? (
145
- <TranscriptText mentions={mentions} text={presentation.preview} />
145
+ <DisplayText mentions={mentions} text={presentation.preview} />
146
146
  ) : null}
147
147
  {presentation.details?.map((detail, index) => (
148
148
  <DetailSubsection key={index} title={detail.title}>
149
149
  {detail.description ? (
150
- <TranscriptText
151
- mentions={mentions}
152
- text={detail.description}
153
- />
150
+ <DisplayText mentions={mentions} text={detail.description} />
154
151
  ) : null}
155
152
  {detail.content ? <DetailValue value={detail.content} /> : null}
156
153
  {detail.metadata ? (
@@ -38,6 +38,8 @@ import {
38
38
  TranscriptHeadingRow,
39
39
  } from "./TranscriptHeadingRow";
40
40
  import { SlackMark } from "./SlackMark";
41
+ import { DisplayText } from "../display-text/DisplayText";
42
+ import { displayPlainText } from "../display-text/plainText";
41
43
 
42
44
  // Rows are one line each, so the stack can show five before it collapses.
43
45
  const MAX_EXPANDED_PENDING_ROWS = 5;
@@ -306,6 +308,7 @@ function PendingRow(props: {
306
308
  steerPending: boolean;
307
309
  }) {
308
310
  const text = props.message.text ?? "";
311
+ const mentions = props.message.mentions ?? [];
309
312
  const redacted = Boolean(props.message.redacted);
310
313
  const showSlack = props.message.source === "slack";
311
314
  const canRetry =
@@ -334,9 +337,9 @@ function PendingRow(props: {
334
337
  // One line keeps the queue short. The title shows the full text.
335
338
  <p
336
339
  className="m-0 min-w-0 truncate font-sans text-sm leading-snug text-dashboard-text-subtle"
337
- title={text}
340
+ title={displayPlainText(text, mentions)}
338
341
  >
339
- {text}
342
+ <DisplayText inline mentions={mentions} text={text} />
340
343
  </p>
341
344
  ) : null}
342
345
  </>
@@ -8,7 +8,7 @@ import {
8
8
  import { dashboardDisclosureChevronClass } from "../styles";
9
9
  import { formatCompactNumber, formatMessageTimestamp } from "../format";
10
10
  import type { TranscriptViewContextEventPart } from "../types";
11
- import { TranscriptText } from "./TranscriptText";
11
+ import { DisplayText } from "../display-text/DisplayText";
12
12
 
13
13
  /** Render a structural context change from the privacy-safe event API. */
14
14
  export function TranscriptContextEventView(props: {
@@ -57,9 +57,8 @@ export function TranscriptContextEventView(props: {
57
57
  </CollapsibleTrigger>
58
58
  <CollapsibleContent hiddenUntilFound>
59
59
  <div className="mt-1.5 whitespace-pre-wrap text-xs leading-relaxed text-dashboard-text-muted">
60
- <TranscriptText
61
- mentions={props.part.mentions ?? []}
62
- role="system"
60
+ <DisplayText
61
+ mentions={props.part.mentions}
63
62
  text={event.summary}
64
63
  />
65
64
  </div>
@@ -3,7 +3,7 @@ import { useState } from "react";
3
3
 
4
4
  import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
5
5
  import { cn, dashboardDisclosureChevronClass } from "../styles";
6
- import { MentionText } from "./transcriptMentions";
6
+ import { DisplayText } from "../display-text/DisplayText";
7
7
  import { messageRawText } from "./transcriptRenderModel";
8
8
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
9
9
  import { RedactedMarker } from "./TranscriptRedacted";
@@ -47,7 +47,12 @@ export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
47
47
  <span className="flex min-w-0 flex-col gap-1">
48
48
  <span className="flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
49
49
  <span className="min-w-0 break-words font-display text-sm font-semibold leading-6 text-dashboard-text">
50
- <MentionText links={links} mentions={mentions} text={summary} />
50
+ <DisplayText
51
+ inline
52
+ links={links}
53
+ mentions={mentions}
54
+ text={summary}
55
+ />
51
56
  </span>
52
57
  {!isBatch && props.message.eventType ? (
53
58
  <EventTypeChip eventType={props.message.eventType} />
@@ -62,7 +67,8 @@ export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
62
67
  className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-5 text-dashboard-text-muted"
63
68
  >
64
69
  <span className="min-w-0 break-words">
65
- <MentionText
70
+ <DisplayText
71
+ inline
66
72
  links={links}
67
73
  mentions={mentions}
68
74
  text={event.trustedSummary ?? event.eventType}
@@ -21,7 +21,7 @@ import { messageRawText } from "./transcriptRenderModel";
21
21
  import { TranscriptMessageHeading } from "./TranscriptHeadingRow";
22
22
  import { RedactedMarker } from "./TranscriptRedacted";
23
23
  import { SlackMark } from "./SlackMark";
24
- import { TranscriptText } from "./TranscriptText";
24
+ import { DisplayText } from "../display-text/DisplayText";
25
25
  import { TranscriptTurnContextView } from "./TranscriptTurnContextView";
26
26
  import { TranscriptTimestamp } from "./TranscriptTimestamp";
27
27
  import { showsSlackSourceIcon } from "./transcriptSource";
@@ -33,7 +33,8 @@ export const TranscriptMessageView = memo(
33
33
  conversation: ConversationTranscript;
34
34
  }) {
35
35
  const rawText = messageRawText(props.message);
36
- const role = props.message.role;
36
+ const roleKind = transcriptRoleKind(props.message.role);
37
+ const chatText = roleKind === "assistant" || roleKind === "user";
37
38
 
38
39
  return (
39
40
  <TranscriptMessageShell
@@ -58,10 +59,10 @@ export const TranscriptMessageView = memo(
58
59
  <div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
59
60
  {props.message.parts.map((part, index) =>
60
61
  part.type === "text" ? (
61
- <TranscriptText
62
+ <DisplayText
63
+ chat={chatText}
62
64
  key={index}
63
- mentions={props.message.mentions ?? []}
64
- role={role}
65
+ mentions={props.message.mentions}
65
66
  text={part.text ?? ""}
66
67
  />
67
68
  ) : null,
@@ -6,13 +6,14 @@ import {
6
6
  TraceCollapsibleContent,
7
7
  TraceCollapsibleTrigger,
8
8
  } from "../components/ui/trace";
9
+ import { DisplayText } from "../display-text/DisplayText";
9
10
  import { formatMessageTimestamp } from "../format";
10
11
  import type { TranscriptViewReasoningPart } from "../types";
11
12
  import {
12
13
  TranscriptHeadingMeta,
13
14
  TranscriptHeadingRow,
14
15
  } from "./TranscriptHeadingRow";
15
- import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
16
+ import { useTranscriptSearch } from "./transcriptSearch";
16
17
 
17
18
  /** Render reasoning collapsed with a short preview until expanded or searched. */
18
19
  export function TranscriptReasoningView(props: {
@@ -50,7 +51,7 @@ export function TranscriptReasoningView(props: {
50
51
  </span>
51
52
  ) : (
52
53
  <span className="block min-w-0 truncate italic text-dashboard-text-muted">
53
- {rendered}
54
+ <DisplayText inline links={false} text={rendered} />
54
55
  </span>
55
56
  )
56
57
  }
@@ -67,9 +68,7 @@ export function TranscriptReasoningView(props: {
67
68
  </TraceCollapsibleTrigger>
68
69
  {/* Keep closed reasoning in the page, so browser find can reach it. */}
69
70
  <TraceCollapsibleContent className="px-3 py-2.5" hiddenUntilFound>
70
- <div className="min-w-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-subtle">
71
- <HighlightText text={rendered} />
72
- </div>
71
+ <DisplayText text={rendered} />
73
72
  </TraceCollapsibleContent>
74
73
  </TraceCollapsible>
75
74
  );
@@ -17,11 +17,16 @@ export function TranscriptRows({
17
17
 
18
18
  /** Leave room for control outlines inside the mobile row's paint bounds. */
19
19
  export function TranscriptRow(props: {
20
+ /** Stable row id that holds the reader's place while layout changes. */
21
+ anchorKey?: string;
20
22
  children: ReactNode;
21
23
  indent?: boolean;
22
24
  }) {
23
25
  return (
24
- <div className="mobile-transcript-row -mx-2 px-2 py-1.5">
26
+ <div
27
+ className="mobile-transcript-row -mx-2 px-2 py-1.5"
28
+ data-scroll-anchor={props.anchorKey}
29
+ >
25
30
  {props.indent ? (
26
31
  <div className="pl-11">{props.children}</div>
27
32
  ) : (
@@ -1,7 +1,7 @@
1
1
  import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
2
2
  import { formatMessageTimestamp } from "../format";
3
3
  import type { TranscriptViewStructuredEventPart } from "../types";
4
- import { MentionText } from "./transcriptMentions";
4
+ import { DisplayText } from "../display-text/DisplayText";
5
5
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
6
6
  import { TranscriptSummaryTrigger } from "./TranscriptSummary";
7
7
 
@@ -34,8 +34,8 @@ export function TranscriptStructuredEventView(props: {
34
34
  </div>
35
35
  )}
36
36
  {detail.description ? (
37
- <div className="mt-1 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
38
- <MentionText mentions={mentions} text={detail.description} />
37
+ <div className="mt-1">
38
+ <DisplayText mentions={mentions} text={detail.description} />
39
39
  </div>
40
40
  ) : null}
41
41
  {detail.content ? (
@@ -70,7 +70,8 @@ export function TranscriptStructuredEventView(props: {
70
70
  </span>
71
71
  {presentation.preview ? (
72
72
  <span className="mt-0.5 block truncate text-xs text-dashboard-text-muted">
73
- <MentionText
73
+ <DisplayText
74
+ inline
74
75
  links={!collapsible}
75
76
  mentions={mentions}
76
77
  text={presentation.preview}
@@ -15,6 +15,7 @@ import { reviewItemIdFromToolCall } from "../review-desk/reviewItemFormat";
15
15
  import { cn } from "../styles";
16
16
  import { ToolFrame } from "./ToolFrame";
17
17
  import { toolCallPreview } from "./toolCallPreview";
18
+ import { transcriptPinStartProps } from "./transcriptBottomPinning";
18
19
  import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
19
20
 
20
21
  /** Render one tool invocation as it advances from running to a terminal result. */
@@ -71,7 +72,7 @@ export const TranscriptToolView = memo(function TranscriptToolView(props: {
71
72
  <div className="min-w-0">
72
73
  {frame}
73
74
  {reviewItemId ? (
74
- <div className="mt-2">
75
+ <div className="mt-2" {...transcriptPinStartProps(reviewItemId)}>
75
76
  <TranscriptReviewItemCard itemId={reviewItemId} />
76
77
  </div>
77
78
  ) : null}
@@ -63,7 +63,6 @@ export function Transcript(props: {
63
63
  enabled: props.live ?? false,
64
64
  historyVersion: props.historyVersion ?? "empty",
65
65
  juniorMessageVersion: transcriptJuniorMessageVersion(props.transcript),
66
- loadingPreviousPage: props.loadingPreviousPage ?? false,
67
66
  pinRequestVersion: props.pinRequestVersion,
68
67
  view,
69
68
  versions: {
@@ -72,10 +71,7 @@ export function Transcript(props: {
72
71
  },
73
72
  });
74
73
 
75
- const loadPreviousPage = () => {
76
- bottomPinning.preserveViewportForPrepend();
77
- props.onLoadPreviousPage?.();
78
- };
74
+ const loadPreviousPage = () => props.onLoadPreviousPage?.();
79
75
  const previousPageRef = useAutoLoadPreviousPage({
80
76
  enabled: Boolean(
81
77
  props.onLoadPreviousPage &&
@@ -97,7 +93,11 @@ export function Transcript(props: {
97
93
 
98
94
  return (
99
95
  <TranscriptSearchProvider query={search}>
100
- <div className="grid min-w-0" ref={bottomPinning.contentRef}>
96
+ {/* The pinning hook holds the reader's place; browser anchoring would fight it. */}
97
+ <div
98
+ className="grid min-w-0 [overflow-anchor:none]"
99
+ ref={bottomPinning.contentRef}
100
+ >
101
101
  {redacted ? (
102
102
  <div className="mb-2 min-w-0 break-words text-sm leading-relaxed text-dashboard-text-muted">
103
103
  Hidden because this conversation is not public.
@@ -140,12 +140,22 @@ function sameMailboxMessage(
140
140
  left.createdAt === right.createdAt &&
141
141
  left.receivedAt === right.receivedAt &&
142
142
  left.idempotencyKey === right.idempotencyKey &&
143
- // Pending rows render actorLabel(actorIdentity). Live polls may enrich
144
- // identity after the first accept; keep that change visible.
145
- sameActorIdentity(left.actorIdentity, right.actorIdentity)
143
+ // Pending rows render actorLabel(actorIdentity) and mention names. Live
144
+ // polls may enrich them after the first accept; keep that change visible.
145
+ sameActorIdentity(left.actorIdentity, right.actorIdentity) &&
146
+ sameMentions(left.mentions, right.mentions)
146
147
  );
147
148
  }
148
149
 
150
+ function sameMentions(
151
+ left: ConversationMailboxMessage["mentions"],
152
+ right: ConversationMailboxMessage["mentions"],
153
+ ): boolean {
154
+ if (left === right) return true;
155
+ if (!left || !right || left.length !== right.length) return false;
156
+ return left.every((person, index) => sameActorIdentity(person, right[index]));
157
+ }
158
+
149
159
  function sameActorIdentity(
150
160
  left: ConversationMailboxMessage["actorIdentity"],
151
161
  right: ConversationMailboxMessage["actorIdentity"],
@@ -8,6 +8,7 @@ export type ConversationSection = {
8
8
 
9
9
  const DAY_MS = 24 * 60 * 60 * 1000;
10
10
  const SECTION_ORDER = [
11
+ "attention",
11
12
  "priority",
12
13
  "today",
13
14
  "yesterday",
@@ -17,10 +18,18 @@ const SECTION_ORDER = [
17
18
  "older",
18
19
  ] as const;
19
20
 
20
- /** Group conversations into progressively broader activity sections. */
21
+ /**
22
+ * Group conversations into progressively broader activity sections.
23
+ *
24
+ * Conversations in `attentionIds` go to the first section, before Priority.
25
+ */
21
26
  export function buildConversationSections(
22
27
  conversations: Conversation[],
23
- options: { nowMs: number; timeZone: string },
28
+ options: {
29
+ attentionIds?: ReadonlySet<string>;
30
+ nowMs: number;
31
+ timeZone: string;
32
+ },
24
33
  ): ConversationSection[] {
25
34
  const nowDay = calendarDay(options.nowMs, options.timeZone);
26
35
  const sections = new Map<string, ConversationSection>();
@@ -30,12 +39,9 @@ export function buildConversationSections(
30
39
 
31
40
  for (const conversation of sorted) {
32
41
  const time = activityTime(conversation);
33
- const section = conversationSection(
34
- conversation,
35
- time,
36
- nowDay,
37
- options.timeZone,
38
- );
42
+ const section = options.attentionIds?.has(conversation.id)
43
+ ? { key: "attention", label: "Needs attention" }
44
+ : conversationSection(conversation, time, nowDay, options.timeZone);
39
45
  const existing = sections.get(section.key);
40
46
  if (existing) {
41
47
  existing.conversations.push(conversation);
@@ -49,6 +55,28 @@ export function buildConversationSections(
49
55
  );
50
56
  }
51
57
 
58
+ /** Join the conversations that wait on the viewer or finished unseen. */
59
+ export function attentionConversationIds(
60
+ waitingConversationIds: ReadonlySet<string>,
61
+ finishedConversationIds: ReadonlySet<string>,
62
+ ): ReadonlySet<string> {
63
+ return new Set([...waitingConversationIds, ...finishedConversationIds]);
64
+ }
65
+
66
+ /**
67
+ * Sort attention conversations to the front. The sort is stable, so each group
68
+ * keeps the feed order. Paginated lists call this before paging, so page 1
69
+ * holds every attention conversation.
70
+ */
71
+ export function attentionFirst(
72
+ conversations: Conversation[],
73
+ attentionIds: ReadonlySet<string>,
74
+ ): Conversation[] {
75
+ const rank = (conversation: Conversation) =>
76
+ attentionIds.has(conversation.id) ? 0 : 1;
77
+ return [...conversations].sort((left, right) => rank(left) - rank(right));
78
+ }
79
+
52
80
  function activityTime(conversation: Conversation): number {
53
81
  const time = Date.parse(conversation.lastSeenAt);
54
82
  return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
@@ -93,6 +93,7 @@ export function pendingTranscriptMessage(
93
93
  ...(message.actorIdentity
94
94
  ? { actorIdentity: message.actorIdentity }
95
95
  : undefined),
96
+ ...(message.mentions ? { mentions: message.mentions } : undefined),
96
97
  };
97
98
  }
98
99