@sentry/junior-dashboard 0.274.0 → 0.276.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 (52) hide show
  1. package/dist/app.js +28 -9
  2. package/dist/client.js +115 -92
  3. package/dist/index.js +28 -9
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +8 -3
  7. package/src/client/App.tsx +11 -0
  8. package/src/client/components/Metric.tsx +0 -32
  9. package/src/client/components/ProfileMenu.tsx +56 -4
  10. package/src/client/components/layout/DashboardChrome.tsx +3 -3
  11. package/src/client/components/layout/DashboardHeader.tsx +41 -4
  12. package/src/client/components/layout/NavRail.tsx +43 -40
  13. package/src/client/conversations/ConversationComposer.tsx +42 -26
  14. package/src/client/conversations/ConversationDetails.tsx +149 -166
  15. package/src/client/conversations/ConversationHeader.tsx +126 -132
  16. package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
  17. package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
  18. package/src/client/conversations/ConversationMemories.tsx +118 -78
  19. package/src/client/conversations/ConversationMeta.tsx +94 -84
  20. package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
  21. package/src/client/conversations/ConversationPage.tsx +197 -422
  22. package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
  23. package/src/client/conversations/ConversationSidebar.tsx +5 -7
  24. package/src/client/conversations/ConversationTranscript.tsx +37 -8
  25. package/src/client/conversations/ConversationWatches.tsx +37 -15
  26. package/src/client/conversations/PendingMailboxStack.tsx +11 -1
  27. package/src/client/conversations/QuickQuestion.tsx +476 -0
  28. package/src/client/conversations/TelemetryMetrics.tsx +2 -15
  29. package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
  30. package/src/client/conversations/TranscriptEventView.tsx +25 -2
  31. package/src/client/conversations/TranscriptMemoryPopover.tsx +179 -0
  32. package/src/client/conversations/TranscriptMessageView.tsx +46 -16
  33. package/src/client/conversations/TranscriptRows.tsx +9 -2
  34. package/src/client/conversations/TranscriptView.tsx +15 -32
  35. package/src/client/conversations/UsageCard.tsx +4 -18
  36. package/src/client/conversations/conversationMemory.ts +138 -0
  37. package/src/client/conversations/conversationSections.ts +18 -14
  38. package/src/client/conversations/eventTranscript.ts +8 -0
  39. package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
  40. package/src/client/review-desk/ReviewItemActions.tsx +1 -1
  41. package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
  42. package/src/client/review-desk/ReviewItemContext.tsx +41 -23
  43. package/src/client/review-desk/reviewItemFormat.ts +16 -0
  44. package/src/client/sentry.ts +77 -0
  45. package/src/client/types.ts +6 -0
  46. package/src/client.tsx +3 -0
  47. package/src/mock-reporting/watches.ts +2 -0
  48. package/src/shell.ts +27 -2
  49. package/src/tailwind.css +12 -19
  50. package/src/client/components/ActiveIndicator.tsx +0 -14
  51. package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
  52. package/src/client/review-desk/ConversationReviewBar.tsx +0 -85
@@ -1,13 +1,5 @@
1
- import {
2
- cloneElement,
3
- isValidElement,
4
- useEffect,
5
- useRef,
6
- useState,
7
- type ReactElement,
8
- type ReactNode,
9
- } from "react";
10
- import { Ellipsis, Menu } from "lucide-react";
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
2
+ import { Menu, PanelRight, ScrollText, X } from "lucide-react";
11
3
 
12
4
  import { SearchInput } from "../components/SearchInput";
13
5
  import { Button } from "../components/ui/button";
@@ -20,29 +12,41 @@ import {
20
12
  import {
21
13
  ConversationHeaderActions,
22
14
  HeaderIconButton,
23
- TranscriptViewToggle,
15
+ TranscriptSearchToggle,
24
16
  type ConversationArchiveAction,
25
17
  } from "./ConversationHeaderActions";
18
+ import { ConversationOverflowMenu } from "./ConversationOverflowMenu";
19
+ import { ShareConversationButton } from "./ShareConversationButton";
26
20
  import type { TranscriptViewMode } from "./transcriptRenderModel";
27
21
 
28
- /** Show the conversation title, search, and details. */
22
+ /**
23
+ * Show the conversation title bar: where it runs, the title, its cost, and
24
+ * actions. Other run facts, linked work, and the Brief live in Conversation
25
+ * details.
26
+ */
29
27
  export function ConversationHeader(props: {
30
28
  /** Parent chain of a child Conversation. Root Conversations omit it. */
31
29
  agentTree?: ReactNode;
30
+ /** A `menu` layout copy action for the `⋯` menu. */
32
31
  copyAction?: ReactNode;
32
+ /** Estimated cost. Desktop only; mobile shows it in Conversation details. */
33
+ cost?: ReactNode;
33
34
  archive: ConversationArchiveAction;
34
35
  conversationId: string;
35
36
  detailsOpen: boolean;
36
37
  onDetailsClick(): void;
37
38
  /** Links to the fork source and to forks of this conversation. */
38
39
  forks?: ReactNode;
40
+ /** Mobile only. Desktop shows linked work in Conversation details. */
39
41
  linkedWork?: ReactNode;
40
42
  live: boolean;
41
- meta?: ReactNode;
43
+ /** Where the conversation runs, such as a Slack channel. */
44
+ location?: ReactNode;
42
45
  onSearchChange(value: string): void;
43
46
  onViewChange(value: TranscriptViewMode): void;
44
47
  privacy: ReactNode;
45
48
  search: string;
49
+ sentryConversationUrl?: string;
46
50
  title: string;
47
51
  view: TranscriptViewMode;
48
52
  }) {
@@ -51,9 +55,7 @@ export function ConversationHeader(props: {
51
55
  const menuButtonRef = useRef<HTMLButtonElement>(null);
52
56
  const wasDetailsOpen = useRef(detailsOpen);
53
57
  const [searchOpen, setSearchOpen] = useState(false);
54
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
55
58
  const searchInputRef = useRef<HTMLInputElement>(null);
56
- const mobileMenuRef = useRef<HTMLDivElement>(null);
57
59
  const searchOpenVisible =
58
60
  props.view === "rich" && (searchOpen || props.search.length > 0);
59
61
  const openMobileNavigation = useOpenMobileNavigation();
@@ -77,27 +79,6 @@ export function ConversationHeader(props: {
77
79
  searchInputRef.current?.focus();
78
80
  }, [searchOpenVisible]);
79
81
 
80
- useEffect(() => {
81
- if (!mobileMenuOpen) return;
82
-
83
- function closeOnOutsideClick(event: PointerEvent) {
84
- if (!mobileMenuRef.current?.contains(event.target as Node)) {
85
- setMobileMenuOpen(false);
86
- }
87
- }
88
-
89
- function closeOnEscape(event: KeyboardEvent) {
90
- if (event.key === "Escape") setMobileMenuOpen(false);
91
- }
92
-
93
- document.addEventListener("pointerdown", closeOnOutsideClick);
94
- document.addEventListener("keydown", closeOnEscape);
95
- return () => {
96
- document.removeEventListener("pointerdown", closeOnOutsideClick);
97
- document.removeEventListener("keydown", closeOnEscape);
98
- };
99
- }, [mobileMenuOpen]);
100
-
101
82
  const toggleSearch = () => {
102
83
  if (searchOpenVisible) {
103
84
  setSearchOpen(false);
@@ -107,64 +88,29 @@ export function ConversationHeader(props: {
107
88
  setSearchOpen(true);
108
89
  };
109
90
 
110
- const menuCopyAction =
111
- isValidElement(props.copyAction) &&
112
- typeof props.copyAction.type !== "string"
113
- ? cloneElement(
114
- props.copyAction as ReactElement<{ layout?: "icon" | "menu" }>,
115
- { layout: "menu" },
116
- )
117
- : props.copyAction;
118
-
119
- const mobileOverflow = (
120
- <div className="relative" ref={mobileMenuRef}>
121
- <HeaderIconButton
122
- ref={menuButtonRef}
123
- label={mobileMenuOpen ? "Close conversation menu" : "Conversation menu"}
124
- onClick={() => setMobileMenuOpen((open) => !open)}
125
- pressed={mobileMenuOpen}
126
- >
127
- <Ellipsis aria-hidden="true" size={18} strokeWidth={2} />
128
- </HeaderIconButton>
129
- {mobileMenuOpen ? (
130
- <div className="absolute right-0 top-[calc(100%+0.35rem)] z-40 w-56 rounded-xl border border-dashboard-border-strong bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl">
131
- <ConversationHeaderActions
132
- archive={props.archive}
133
- conversationId={props.conversationId}
134
- copyAction={menuCopyAction}
135
- detailsOpen={detailsOpen}
136
- layout="menu"
137
- onDetailsClick={() => {
138
- setMobileMenuOpen(false);
139
- props.onDetailsClick();
140
- }}
141
- onSearchClick={() => {
142
- setMobileMenuOpen(false);
143
- toggleSearch();
144
- }}
145
- onViewChange={(value) => {
146
- setMobileMenuOpen(false);
147
- props.onViewChange(value);
148
- }}
149
- searchOpen={searchOpenVisible}
150
- view={props.view}
151
- />
152
- {openMobileNavigation ? (
153
- <Button
154
- className="mt-0.5"
155
- onClick={() => {
156
- setMobileMenuOpen(false);
157
- openMobileNavigation();
158
- }}
159
- size="menu"
160
- >
161
- <Menu aria-hidden="true" size={16} strokeWidth={2} />
162
- App menu
163
- </Button>
164
- ) : null}
165
- </div>
166
- ) : null}
167
- </div>
91
+ const menuActions = (placement: "desktop" | "mobile", close: () => void) => (
92
+ <ConversationHeaderActions
93
+ archive={props.archive}
94
+ conversationId={props.conversationId}
95
+ copyAction={props.copyAction}
96
+ detailsOpen={detailsOpen}
97
+ onDetailsClick={() => {
98
+ close();
99
+ props.onDetailsClick();
100
+ }}
101
+ onSearchClick={() => {
102
+ close();
103
+ toggleSearch();
104
+ }}
105
+ onViewChange={(value) => {
106
+ close();
107
+ props.onViewChange(value);
108
+ }}
109
+ placement={placement}
110
+ searchOpen={searchOpenVisible}
111
+ sentryConversationUrl={props.sentryConversationUrl}
112
+ view={props.view}
113
+ />
168
114
  );
169
115
 
170
116
  const showMobileHeader =
@@ -184,48 +130,103 @@ export function ConversationHeader(props: {
184
130
 
185
131
  return (
186
132
  <>
187
- <MobileHeaderActionsPortal>{mobileOverflow}</MobileHeaderActionsPortal>
133
+ <MobileHeaderActionsPortal>
134
+ <ConversationOverflowMenu buttonRef={menuButtonRef}>
135
+ {(close) => (
136
+ <>
137
+ {menuActions("mobile", close)}
138
+ {openMobileNavigation ? (
139
+ <Button
140
+ className="mt-0.5"
141
+ onClick={() => {
142
+ close();
143
+ openMobileNavigation();
144
+ }}
145
+ size="menu"
146
+ >
147
+ <Menu aria-hidden="true" size={16} strokeWidth={2} />
148
+ App menu
149
+ </Button>
150
+ ) : null}
151
+ </>
152
+ )}
153
+ </ConversationOverflowMenu>
154
+ </MobileHeaderActionsPortal>
188
155
  <MobileHeaderLivePortal>{liveIndicator}</MobileHeaderLivePortal>
156
+ {/* Desktop: above the event log's sticky toolbar, so the `⋯` menu
157
+ covers it. Mobile: below the app header, which holds the menu. */}
189
158
  <header
190
159
  className={cn(
191
- "relative z-10 border-b border-dashboard-border bg-dashboard-bg",
160
+ "relative z-10 border-b md:z-30 border-dashboard-border bg-dashboard-bg",
192
161
  !showMobileHeader && "hidden md:block",
193
162
  )}
194
163
  >
195
- <div className="hidden min-w-0 items-start justify-between gap-4 px-7 py-5 md:flex">
196
- <div className="min-w-0">
164
+ <div className="hidden h-12 min-w-0 items-center justify-between gap-4 pl-7 pr-4 md:flex">
165
+ <div className="flex min-w-0 items-center gap-2">
197
166
  {props.agentTree ? (
198
- <div className="mb-1.5 min-w-0">{props.agentTree}</div>
167
+ <div className="min-w-0 shrink-[2]">{props.agentTree}</div>
168
+ ) : props.location ? (
169
+ <span className="shrink-0 text-sm text-dashboard-text-muted">
170
+ {props.location}
171
+ </span>
199
172
  ) : null}
200
- <div className="flex min-w-0 items-center gap-2">
201
- <h2 className="m-0 line-clamp-1 min-w-0 font-display text-xl font-medium leading-tight tracking-tight">
202
- {props.title}
203
- </h2>
204
- {liveIndicator}
205
- <span className="shrink-0">{props.privacy}</span>
206
- </div>
207
- {props.meta ? (
208
- <div className="mt-2 min-w-0 text-xs leading-snug text-dashboard-text-muted">
209
- {props.meta}
210
- </div>
173
+ {props.agentTree || props.location ? (
174
+ <span aria-hidden="true" className="text-dashboard-text-faint">
175
+ ›
176
+ </span>
211
177
  ) : null}
212
- {props.forks ? (
213
- <div className="mt-1.5 min-w-0">{props.forks}</div>
178
+ <h2 className="m-0 line-clamp-1 min-w-0 font-display text-base font-medium leading-tight tracking-tight">
179
+ {props.title}
180
+ </h2>
181
+ {liveIndicator}
182
+ <span className="shrink-0">{props.privacy}</span>
183
+ </div>
184
+ <div className="flex shrink-0 items-center gap-0.5">
185
+ {props.cost ? (
186
+ <span className="mr-2 text-xs tabular-nums text-dashboard-text-muted">
187
+ {props.cost}
188
+ </span>
214
189
  ) : null}
190
+ {props.view === "raw" ? (
191
+ // Event log is a debug view reached from the menu. Say which
192
+ // view this is, and give one click back.
193
+ <Button
194
+ aria-label="Close event log"
195
+ className="mr-1 h-8 px-2.5"
196
+ onClick={() => props.onViewChange("rich")}
197
+ variant="quiet"
198
+ >
199
+ <ScrollText aria-hidden="true" size={14} strokeWidth={2} />
200
+ Event log
201
+ <X aria-hidden="true" size={14} strokeWidth={2} />
202
+ </Button>
203
+ ) : (
204
+ <TranscriptSearchToggle
205
+ onClick={toggleSearch}
206
+ open={searchOpenVisible}
207
+ />
208
+ )}
209
+ <ShareConversationButton
210
+ key={props.conversationId}
211
+ conversationId={props.conversationId}
212
+ layout="icon"
213
+ />
214
+ <ConversationOverflowMenu>
215
+ {(close) => menuActions("desktop", close)}
216
+ </ConversationOverflowMenu>
217
+ <HeaderIconButton
218
+ ref={detailsButtonRef}
219
+ label="Conversation details"
220
+ onClick={props.onDetailsClick}
221
+ pressed={detailsOpen}
222
+ >
223
+ <PanelRight aria-hidden="true" size={15} strokeWidth={2} />
224
+ </HeaderIconButton>
215
225
  </div>
216
- <ConversationHeaderActions
217
- archive={props.archive}
218
- conversationId={props.conversationId}
219
- copyAction={props.copyAction}
220
- detailsOpen={detailsOpen}
221
- detailsButtonRef={detailsButtonRef}
222
- onDetailsClick={props.onDetailsClick}
223
- onSearchClick={toggleSearch}
224
- onViewChange={props.onViewChange}
225
- searchOpen={searchOpenVisible}
226
- view={props.view}
227
- />
228
226
  </div>
227
+ {props.forks ? (
228
+ <div className="hidden px-7 pb-2.5 md:block">{props.forks}</div>
229
+ ) : null}
229
230
  {props.agentTree ? (
230
231
  <div className="px-4 py-2 md:hidden">{props.agentTree}</div>
231
232
  ) : null}
@@ -235,18 +236,11 @@ export function ConversationHeader(props: {
235
236
  {props.linkedWork ? (
236
237
  <div
237
238
  aria-label="Linked work"
238
- className="border-t border-dashboard-border-subtle px-4 py-2.5 md:px-7"
239
+ className="border-t border-dashboard-border-subtle px-4 py-2.5 md:hidden"
239
240
  >
240
241
  {props.linkedWork}
241
242
  </div>
242
243
  ) : null}
243
- <div className="hidden border-t border-dashboard-border-subtle px-7 md:flex">
244
- <TranscriptViewToggle
245
- labelled
246
- onChange={props.onViewChange}
247
- value={props.view}
248
- />
249
- </div>
250
244
  {searchOpenVisible ? (
251
245
  <div className="px-4 py-3 md:px-7">
252
246
  <SearchInput
@@ -3,14 +3,13 @@ import {
3
3
  Archive,
4
4
  ArchiveRestore,
5
5
  Info,
6
- PanelRight,
7
6
  MessagesSquare,
8
7
  ScrollText,
9
8
  Search,
10
9
  X,
11
10
  } from "lucide-react";
12
11
 
13
- import { Button } from "../components/ui/button";
12
+ import { Button, buttonVariants } from "../components/ui/button";
14
13
  import {
15
14
  Tooltip,
16
15
  TooltipContent,
@@ -29,59 +28,74 @@ export type ConversationArchiveAction = {
29
28
  pending: boolean;
30
29
  };
31
30
 
32
- /** Show conversation actions in the header or mobile menu. */
31
+ /**
32
+ * Show conversation actions in the `⋯` menu. On desktop the title bar shows
33
+ * search, share, and details, so the menu leaves them out.
34
+ */
33
35
  export function ConversationHeaderActions(props: {
34
36
  archive: ConversationArchiveAction;
35
37
  conversationId: string;
36
38
  copyAction?: ReactNode;
37
39
  detailsOpen: boolean;
38
- detailsButtonRef?: Ref<HTMLButtonElement>;
39
- /** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
40
- layout?: "bar" | "menu";
41
40
  onDetailsClick(): void;
42
41
  onSearchClick(): void;
43
42
  onViewChange(value: TranscriptViewMode): void;
43
+ placement: "desktop" | "mobile";
44
44
  searchOpen: boolean;
45
+ sentryConversationUrl?: string;
45
46
  view: TranscriptViewMode;
46
47
  }) {
47
- const layout = props.layout ?? "bar";
48
- if (layout === "menu") {
49
- return (
50
- <div className="grid gap-0.5">
51
- {props.view === "rich" ? (
52
- <MenuActionButton
53
- label={props.searchOpen ? "Hide search" : "Search transcript"}
54
- onClick={props.onSearchClick}
55
- pressed={props.searchOpen}
56
- >
57
- {props.searchOpen ? (
58
- <X aria-hidden="true" size={16} strokeWidth={2} />
59
- ) : (
60
- <Search aria-hidden="true" size={16} strokeWidth={2} />
61
- )}
62
- </MenuActionButton>
63
- ) : null}
64
- <MenuActionButton
65
- label="Conversation"
66
- onClick={() => props.onViewChange("rich")}
67
- pressed={props.view === "rich"}
68
- >
69
- <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
70
- </MenuActionButton>
48
+ const mobile = props.placement === "mobile";
49
+ return (
50
+ <div className="grid gap-0.5">
51
+ {mobile && props.view === "rich" ? (
71
52
  <MenuActionButton
72
- label="Event log"
73
- onClick={() => props.onViewChange("raw")}
74
- pressed={props.view === "raw"}
53
+ label={props.searchOpen ? "Hide search" : "Search transcript"}
54
+ onClick={props.onSearchClick}
55
+ pressed={props.searchOpen}
75
56
  >
76
- <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
57
+ {props.searchOpen ? (
58
+ <X aria-hidden="true" size={16} strokeWidth={2} />
59
+ ) : (
60
+ <Search aria-hidden="true" size={16} strokeWidth={2} />
61
+ )}
77
62
  </MenuActionButton>
63
+ ) : null}
64
+ <MenuActionButton
65
+ label="Conversation"
66
+ onClick={() => props.onViewChange("rich")}
67
+ pressed={props.view === "rich"}
68
+ >
69
+ <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
70
+ </MenuActionButton>
71
+ <MenuActionButton
72
+ label="Event log"
73
+ onClick={() => props.onViewChange("raw")}
74
+ pressed={props.view === "raw"}
75
+ >
76
+ <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
77
+ </MenuActionButton>
78
+ {mobile ? (
78
79
  <ShareConversationButton
79
80
  key={props.conversationId}
80
81
  conversationId={props.conversationId}
81
82
  layout="menu"
82
83
  />
83
- {props.copyAction}
84
- <ArchiveConversationButton {...props.archive} layout="menu" />
84
+ ) : null}
85
+ {props.copyAction}
86
+ <ArchiveConversationButton {...props.archive} />
87
+ {props.sentryConversationUrl ? (
88
+ <a
89
+ className={buttonVariants({ size: "menu" })}
90
+ href={props.sentryConversationUrl}
91
+ rel="noopener noreferrer"
92
+ target="_blank"
93
+ >
94
+ <SentryGlyph />
95
+ Open in Sentry
96
+ </a>
97
+ ) : null}
98
+ {mobile ? (
85
99
  <MenuActionButton
86
100
  label="Conversation details"
87
101
  onClick={props.onDetailsClick}
@@ -89,57 +103,29 @@ export function ConversationHeaderActions(props: {
89
103
  >
90
104
  <Info aria-hidden="true" size={16} strokeWidth={2} />
91
105
  </MenuActionButton>
92
- </div>
93
- );
94
- }
95
-
96
- return (
97
- <div className="flex shrink-0 items-center gap-0.5">
98
- {props.view === "rich" ? (
99
- <TranscriptSearchToggle
100
- onClick={props.onSearchClick}
101
- open={props.searchOpen}
102
- />
103
106
  ) : null}
104
- {props.copyAction}
105
- <ArchiveConversationButton {...props.archive} layout="bar" />
106
- <Button
107
- ref={props.detailsButtonRef}
108
- aria-label="Conversation details"
109
- aria-pressed={props.detailsOpen}
110
- className="ml-2"
111
- onClick={props.onDetailsClick}
112
- >
113
- <PanelRight aria-hidden="true" size={15} strokeWidth={2} />
114
- Details
115
- </Button>
116
107
  </div>
117
108
  );
118
109
  }
119
110
 
120
111
  /** Switch between the conversation and event log. */
121
112
  export function TranscriptViewToggle(props: {
122
- labelled?: boolean;
123
113
  onChange(value: TranscriptViewMode): void;
124
114
  value: TranscriptViewMode;
125
115
  }) {
126
116
  return (
127
117
  <ToggleGroup
128
118
  aria-label="Transcript view"
129
- className={
130
- props.labelled
131
- ? "inline-flex gap-6"
132
- : "inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
133
- }
119
+ className="inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
134
120
  onValueChange={([next]) => {
135
121
  if (next) props.onChange(next);
136
122
  }}
137
123
  value={[props.value]}
138
124
  >
139
- <ViewModeItem label="Conversation" labelled={props.labelled} value="rich">
125
+ <ViewModeItem label="Conversation" value="rich">
140
126
  <MessagesSquare aria-hidden="true" size={14} strokeWidth={2} />
141
127
  </ViewModeItem>
142
- <ViewModeItem label="Event log" labelled={props.labelled} value="raw">
128
+ <ViewModeItem label="Event log" value="raw">
143
129
  <ScrollText aria-hidden="true" size={14} strokeWidth={2} />
144
130
  </ViewModeItem>
145
131
  </ToggleGroup>
@@ -149,28 +135,21 @@ export function TranscriptViewToggle(props: {
149
135
  function ViewModeItem(props: {
150
136
  children: ReactNode;
151
137
  label: string;
152
- labelled?: boolean;
153
138
  value: TranscriptViewMode;
154
139
  }) {
155
- const item = (
156
- <ToggleGroupItem
157
- aria-label={props.label}
158
- className={
159
- props.labelled
160
- ? "relative h-11 min-w-0 gap-2 rounded-none border-0 px-0 text-sm hover:bg-transparent after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 data-pressed:border-0 data-pressed:bg-transparent data-pressed:after:bg-dashboard-text-solid"
161
- : "size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
162
- }
163
- value={props.value}
164
- >
165
- {props.children}
166
- {props.labelled ? props.label : null}
167
- </ToggleGroupItem>
168
- );
169
- return props.labelled ? (
170
- item
171
- ) : (
140
+ return (
172
141
  <Tooltip>
173
- <TooltipTrigger render={item} />
142
+ <TooltipTrigger
143
+ render={
144
+ <ToggleGroupItem
145
+ aria-label={props.label}
146
+ className="size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
147
+ value={props.value}
148
+ >
149
+ {props.children}
150
+ </ToggleGroupItem>
151
+ }
152
+ />
174
153
  <TooltipContent size="compact">{props.label}</TooltipContent>
175
154
  </Tooltip>
176
155
  );
@@ -183,9 +162,11 @@ export function HeaderIconButton(props: {
183
162
  label: string;
184
163
  onClick(): void;
185
164
  pressed?: boolean;
165
+ /** Hide the tooltip, such as while the menu this button opens covers it. */
166
+ tooltipDisabled?: boolean;
186
167
  }) {
187
168
  return (
188
- <Tooltip>
169
+ <Tooltip disabled={props.tooltipDisabled}>
189
170
  <TooltipTrigger
190
171
  render={
191
172
  <Button
@@ -248,45 +229,36 @@ function MenuActionButton(props: {
248
229
  );
249
230
  }
250
231
 
251
- function ArchiveConversationButton(
252
- props: ConversationArchiveAction & { layout: "bar" | "menu" },
253
- ) {
232
+ function ArchiveConversationButton(props: ConversationArchiveAction) {
254
233
  const label = props.pending
255
234
  ? "Saving archive state"
256
235
  : props.archived
257
236
  ? "Unarchive"
258
237
  : "Archive";
259
238
  const Icon = props.archived ? ArchiveRestore : Archive;
260
- if (props.layout === "menu") {
261
- return (
262
- <Button
263
- aria-label={label}
264
- disabled={props.disabled}
265
- onClick={props.onClick}
266
- size="menu"
267
- >
268
- <Icon aria-hidden="true" size={16} strokeWidth={2} />
269
- <span>{label}</span>
270
- </Button>
271
- );
272
- }
273
239
  return (
274
- <Tooltip>
275
- <TooltipTrigger
276
- render={
277
- <Button
278
- focusableWhenDisabled
279
- aria-label={label}
280
- className="hidden shrink-0 text-dashboard-text-muted md:grid"
281
- disabled={props.disabled}
282
- onClick={props.onClick}
283
- size="icon"
284
- >
285
- <Icon aria-hidden="true" size={15} strokeWidth={2} />
286
- </Button>
287
- }
288
- />
289
- <TooltipContent size="compact">{label}</TooltipContent>
290
- </Tooltip>
240
+ <Button
241
+ aria-label={label}
242
+ disabled={props.disabled}
243
+ onClick={props.onClick}
244
+ size="menu"
245
+ >
246
+ <Icon aria-hidden="true" size={16} strokeWidth={2} />
247
+ <span>{label}</span>
248
+ </Button>
249
+ );
250
+ }
251
+
252
+ /** Draw the Sentry glyph in the current text color, sized like a menu icon. */
253
+ function SentryGlyph() {
254
+ return (
255
+ <svg
256
+ aria-hidden="true"
257
+ className="size-4 shrink-0"
258
+ fill="currentColor"
259
+ viewBox="0 0 24 24"
260
+ >
261
+ <path d="M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z" />
262
+ </svg>
291
263
  );
292
264
  }