@sentry/junior-dashboard 0.279.0 → 0.280.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 (147) hide show
  1. package/dist/app.js +100 -31
  2. package/dist/client-manifest.json +1 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +100 -31
  5. package/dist/public/chunks/AdminPage-YEKZOKH6.js +1 -0
  6. package/dist/public/chunks/AutomationPage-5GS3IUAK.js +3 -0
  7. package/dist/public/chunks/AutomationRunsPage-IPYEZU4L.js +1 -0
  8. package/dist/public/chunks/AutomationsPage-HSAYPC3C.js +1 -0
  9. package/dist/public/chunks/CodePage-IPFKXWFB.js +1 -0
  10. package/dist/public/chunks/ConnectionsPage-5XUEK6FS.js +1 -0
  11. package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
  12. package/dist/public/chunks/LocationDetailPage-27ETUGR6.js +1 -0
  13. package/dist/public/chunks/LocationsPage-WOKMQAGM.js +1 -0
  14. package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
  15. package/dist/public/chunks/PeoplePage-6YQLDOUL.js +1 -0
  16. package/dist/public/chunks/PersonProfilePage-GYMAZFIK.js +1 -0
  17. package/dist/public/chunks/PersonalTokensPage-4BCR744B.js +1 -0
  18. package/dist/public/chunks/PluginUserPage-IP6WL7MD.js +1 -0
  19. package/dist/public/chunks/RepositoryPage-TRD44LQ3.js +1 -0
  20. package/dist/public/chunks/SettingsPage-4E3SLQ36.js +1 -0
  21. package/dist/public/chunks/SystemPage-C3XBKXKW.js +1 -0
  22. package/dist/public/chunks/WorkspaceFormPage-2LS2X67Q.js +2 -0
  23. package/dist/public/chunks/WorkspacesPage-5WB3CUTX.js +1 -0
  24. package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
  25. package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
  26. package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
  27. package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
  28. package/dist/public/chunks/chunk-564V7YHG.js +1 -0
  29. package/dist/public/chunks/chunk-5GZDBGAR.js +1 -0
  30. package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
  31. package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
  32. package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
  33. package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
  34. package/dist/public/chunks/chunk-6GQL3EVJ.js +1 -0
  35. package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
  36. package/dist/public/chunks/chunk-A2QWWA7O.js +41 -0
  37. package/dist/public/chunks/chunk-AYXSBMWA.js +1 -0
  38. package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
  39. package/dist/public/chunks/chunk-B6HGRBBX.js +1 -0
  40. package/dist/public/chunks/chunk-BKOACADP.js +1 -0
  41. package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
  42. package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
  43. package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
  44. package/dist/public/chunks/chunk-EFEREKX4.js +1 -0
  45. package/dist/public/chunks/chunk-FE522UEA.js +1 -0
  46. package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
  47. package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
  48. package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
  49. package/dist/public/chunks/chunk-GO45RQDL.js +1 -0
  50. package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
  51. package/dist/public/chunks/chunk-GRMX2HX6.js +1 -0
  52. package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
  53. package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
  54. package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
  55. package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
  56. package/dist/public/chunks/chunk-IA6BJDKN.js +1 -0
  57. package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
  58. package/dist/public/chunks/chunk-J37MXUED.js +1 -0
  59. package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
  60. package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
  61. package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
  62. package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
  63. package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
  64. package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
  65. package/dist/public/chunks/chunk-N4AONKD3.js +1 -0
  66. package/dist/public/chunks/chunk-NQMDED2R.js +2 -0
  67. package/dist/public/chunks/chunk-NQOJAZFQ.js +1 -0
  68. package/dist/public/chunks/chunk-PEQNT5MO.js +1 -0
  69. package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
  70. package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
  71. package/dist/public/chunks/chunk-QNA7F7QB.js +1 -0
  72. package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
  73. package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
  74. package/dist/public/chunks/chunk-R4ORRE77.js +1 -0
  75. package/dist/public/chunks/chunk-RHIXHBS5.js +1 -0
  76. package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
  77. package/dist/public/chunks/chunk-SDKV3OID.js +1 -0
  78. package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
  79. package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
  80. package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
  81. package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
  82. package/dist/public/chunks/chunk-UAH6DWNS.js +1 -0
  83. package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
  84. package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
  85. package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
  86. package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
  87. package/dist/public/chunks/chunk-V5UDSIRA.js +63 -0
  88. package/dist/public/chunks/chunk-VC6IS564.js +1 -0
  89. package/dist/public/chunks/chunk-VFEBNOPX.js +1 -0
  90. package/dist/public/chunks/chunk-VXT3FIVL.js +55 -0
  91. package/dist/public/chunks/chunk-WJ3OXMVX.js +1 -0
  92. package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
  93. package/dist/public/chunks/chunk-WZVO3HG4.js +1 -0
  94. package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
  95. package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
  96. package/dist/public/chunks/chunk-YYNMZ4WC.js +1 -0
  97. package/dist/public/chunks/chunk-Z2JPRGFT.js +1 -0
  98. package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
  99. package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
  100. package/dist/public/client-TYC7JKRE.js +91 -0
  101. package/dist/tailwind.css +1 -1
  102. package/package.json +5 -4
  103. package/src/api/schema.ts +1 -1
  104. package/src/assets.ts +4 -1
  105. package/src/client/App.tsx +100 -23
  106. package/src/client/TracedRoutes.tsx +8 -4
  107. package/src/client/code-highlighter.ts +26 -0
  108. package/src/client/code.tsx +2 -22
  109. package/src/client/components/Drawer.tsx +12 -71
  110. package/src/client/components/DrawerContent.tsx +76 -0
  111. package/src/client/components/ProfileMenu.tsx +12 -3
  112. package/src/client/components/UserInputForm.tsx +28 -5
  113. package/src/client/conversations/ConversationCatchUp.tsx +107 -0
  114. package/src/client/conversations/ConversationContextTags.tsx +110 -0
  115. package/src/client/conversations/ConversationEventLog.tsx +213 -71
  116. package/src/client/conversations/ConversationHeader.tsx +35 -41
  117. package/src/client/conversations/ConversationHeaderActions.tsx +34 -38
  118. package/src/client/conversations/ConversationHomeList.tsx +41 -3
  119. package/src/client/conversations/ConversationMeta.tsx +6 -12
  120. package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
  121. package/src/client/conversations/ConversationPage.tsx +188 -150
  122. package/src/client/conversations/ShareConversationButton.tsx +5 -50
  123. package/src/client/conversations/SlackMark.tsx +16 -4
  124. package/src/client/conversations/TranscriptRows.tsx +24 -11
  125. package/src/client/conversations/TranscriptView.tsx +21 -13
  126. package/src/client/conversations/conversationArchive.ts +22 -0
  127. package/src/client/conversations/conversationCatchUpState.ts +96 -0
  128. package/src/client/conversations/conversationMemory.ts +8 -1
  129. package/src/client/conversations/queries.ts +11 -3
  130. package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
  131. package/src/client/conversations/transcriptBottomPinning.ts +27 -3
  132. package/src/client/conversations/transcriptReadPosition.ts +24 -2
  133. package/src/client/conversations/turnContext.ts +1 -1
  134. package/src/client/pages/admin/AdminPage.tsx +1 -1
  135. package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
  136. package/src/client/pages/memory/memoryDashboard.ts +1 -1
  137. package/src/client/pages/memory/memoryRecord.ts +1 -1
  138. package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
  139. package/src/client/pages/user/PluginUserPage.tsx +0 -6
  140. package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
  141. package/src/client/primaryNav.ts +1 -1
  142. package/src/client/review-desk/reviewItemFormat.ts +1 -1
  143. package/src/mock-reporting/fixtures.ts +43 -0
  144. package/src/mock-reporting/routes.ts +40 -3
  145. package/src/public-assets.ts +28 -20
  146. package/src/shell.ts +12 -2
  147. package/dist/public/client-ZCZXZBTL.js +0 -460
@@ -3,6 +3,7 @@ import { Menu, PanelRight, ScrollText, X } from "lucide-react";
3
3
 
4
4
  import { SearchInput } from "../components/SearchInput";
5
5
  import { Button } from "../components/ui/button";
6
+ import { Spinner } from "../components/ui/spinner";
6
7
  import { cn } from "../styles";
7
8
  import {
8
9
  MobileHeaderActionsPortal,
@@ -17,32 +18,29 @@ import {
17
18
  type ConversationPinAction,
18
19
  } from "./ConversationHeaderActions";
19
20
  import { ConversationOverflowMenu } from "./ConversationOverflowMenu";
20
- import { ShareConversationButton } from "./ShareConversationButton";
21
21
  import type { TranscriptViewMode } from "./transcriptRenderModel";
22
22
 
23
23
  /**
24
- * Show the conversation title bar: where it runs, the title, its cost, and
25
- * actions. Other run facts, linked work, and the Brief live in Conversation
26
- * details.
24
+ * Keep the conversation title and frequently used controls in the header.
25
+ * Run facts and the Brief live in Conversation details.
27
26
  */
28
27
  export function ConversationHeader(props: {
29
28
  /** Parent chain of a child Conversation. Root Conversations omit it. */
30
29
  agentTree?: ReactNode;
30
+ contextTags?: ReactNode;
31
31
  /** A `menu` layout copy action for the `⋯` menu. */
32
32
  copyAction?: ReactNode;
33
- /** Estimated cost. Desktop only; mobile shows it in Conversation details. */
34
- cost?: ReactNode;
35
33
  archive: ConversationArchiveAction;
36
34
  conversationId: string;
35
+ /** Estimated cost. Desktop only; mobile shows it in Conversation details. */
36
+ cost?: ReactNode;
37
37
  detailsOpen: boolean;
38
38
  onDetailsClick(): void;
39
39
  /** Links to the fork source and to forks of this conversation. */
40
40
  forks?: ReactNode;
41
- /** Mobile only. Desktop shows linked work in Conversation details. */
41
+ /** Linked work links below the title on mobile. */
42
42
  linkedWork?: ReactNode;
43
43
  live: boolean;
44
- /** Where the conversation runs, such as a Slack channel. */
45
- location?: ReactNode;
46
44
  onSearchChange(value: string): void;
47
45
  onViewChange(value: TranscriptViewMode): void;
48
46
  pin: ConversationPinAction;
@@ -121,16 +119,18 @@ export function ConversationHeader(props: {
121
119
  searchOpenVisible ||
122
120
  props.archive.error ||
123
121
  props.pin.error ||
122
+ Boolean(props.contextTags) ||
124
123
  Boolean(props.linkedWork) ||
125
124
  Boolean(props.forks) ||
126
125
  Boolean(props.agentTree);
127
126
 
128
127
  const liveIndicator = props.live ? (
129
- <span
130
- aria-label="Conversation is live"
131
- className="inline-flex size-2 shrink-0 rounded-full bg-emerald-300"
132
- title="Live"
133
- />
128
+ <span className="inline-flex shrink-0" title="Working">
129
+ <Spinner
130
+ aria-label="Working"
131
+ className="size-3.5 text-dashboard-text-muted motion-safe:[animation-duration:1.25s]"
132
+ />
133
+ </span>
134
134
  ) : null;
135
135
 
136
136
  return (
@@ -166,28 +166,27 @@ export function ConversationHeader(props: {
166
166
  !showMobileHeader && "hidden md:block",
167
167
  )}
168
168
  >
169
- <div className="hidden h-13 min-w-0 items-center justify-between gap-4 px-4 md:flex">
170
- <div className="flex min-w-0 items-center gap-2">
169
+ <div className="hidden h-13 min-w-0 items-center justify-between gap-1.5 px-1 md:flex">
170
+ <div className="flex min-w-0 items-center gap-1.5">
171
171
  {props.sidebarToggle ? (
172
172
  <div className="shrink-0">{props.sidebarToggle}</div>
173
173
  ) : null}
174
- {props.agentTree ? (
175
- <div className="min-w-0 shrink-[2]">{props.agentTree}</div>
176
- ) : props.location ? (
177
- <span className="shrink-0 text-sm text-dashboard-text-muted">
178
- {props.location}
179
- </span>
180
- ) : null}
181
- {props.agentTree || props.location ? (
182
- <span aria-hidden="true" className="text-dashboard-text-faint">
183
- ›
184
- </span>
185
- ) : null}
186
- <h2 className="m-0 line-clamp-1 min-w-0 font-display text-base font-medium leading-tight tracking-tight">
174
+ <h2 className="m-0 min-w-0 truncate font-display text-base font-medium leading-6 tracking-tight">
187
175
  {props.title}
188
176
  </h2>
189
177
  {liveIndicator}
190
- <span className="shrink-0">{props.privacy}</span>
178
+ {props.privacy ? (
179
+ <span className="shrink-0">{props.privacy}</span>
180
+ ) : null}
181
+ {props.agentTree ? (
182
+ <div className="min-w-0 max-w-1/3 shrink-[2] border-l border-dashboard-border-subtle pl-2 text-xs text-dashboard-text-muted [&_a]:text-dashboard-text-muted [&_a:hover]:text-dashboard-text">
183
+ {props.agentTree}
184
+ </div>
185
+ ) : props.contextTags ? (
186
+ <div className="flex h-6 min-w-0 max-w-[min(19rem,60%)] shrink-[2] items-center">
187
+ {props.contextTags}
188
+ </div>
189
+ ) : null}
191
190
  </div>
192
191
  <div className="flex shrink-0 items-center gap-0.5">
193
192
  {props.cost ? (
@@ -214,11 +213,6 @@ export function ConversationHeader(props: {
214
213
  open={searchOpenVisible}
215
214
  />
216
215
  )}
217
- <ShareConversationButton
218
- key={props.conversationId}
219
- conversationId={props.conversationId}
220
- layout="icon"
221
- />
222
216
  <ConversationOverflowMenu>
223
217
  {(close) => menuActions("desktop", close)}
224
218
  </ConversationOverflowMenu>
@@ -241,12 +235,12 @@ export function ConversationHeader(props: {
241
235
  {props.forks ? (
242
236
  <div className="px-4 py-2 md:hidden">{props.forks}</div>
243
237
  ) : null}
244
- {props.linkedWork ? (
245
- <div
246
- aria-label="Linked work"
247
- className="border-t border-dashboard-border-subtle px-4 py-2.5 md:hidden"
248
- >
249
- {props.linkedWork}
238
+ {props.contextTags || props.linkedWork ? (
239
+ <div className="grid min-w-0 gap-2 px-3 pb-2 pt-1.5 md:hidden">
240
+ {props.contextTags}
241
+ {props.linkedWork ? (
242
+ <div aria-label="Linked work">{props.linkedWork}</div>
243
+ ) : null}
250
244
  </div>
251
245
  ) : null}
252
246
  {searchOpenVisible ? (
@@ -12,6 +12,7 @@ import {
12
12
  } from "lucide-react";
13
13
 
14
14
  import { Button, buttonVariants } from "../components/ui/button";
15
+ import { Separator } from "../components/ui/separator";
15
16
  import {
16
17
  Tooltip,
17
18
  TooltipContent,
@@ -38,8 +39,8 @@ export type ConversationPinAction = {
38
39
  };
39
40
 
40
41
  /**
41
- * Show conversation actions in the `⋯` menu. On desktop the title bar shows
42
- * search, share, and details, so the menu leaves them out.
42
+ * Keep secondary conversation actions in the menu, with search and details
43
+ * available there on mobile too.
43
44
  */
44
45
  export function ConversationHeaderActions(props: {
45
46
  archive: ConversationArchiveAction;
@@ -56,9 +57,10 @@ export function ConversationHeaderActions(props: {
56
57
  view: TranscriptViewMode;
57
58
  }) {
58
59
  const mobile = props.placement === "mobile";
60
+ const eventLogOpen = props.view === "raw";
59
61
  return (
60
62
  <div className="grid gap-0.5">
61
- {mobile && props.view === "rich" ? (
63
+ {mobile && !eventLogOpen ? (
62
64
  <MenuActionButton
63
65
  label={props.searchOpen ? "Hide search" : "Search transcript"}
64
66
  onClick={props.onSearchClick}
@@ -71,40 +73,30 @@ export function ConversationHeaderActions(props: {
71
73
  )}
72
74
  </MenuActionButton>
73
75
  ) : null}
74
- <MenuActionButton
75
- label="Conversation"
76
- onClick={() => props.onViewChange("rich")}
77
- pressed={props.view === "rich"}
78
- >
79
- <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
80
- </MenuActionButton>
81
- <MenuActionButton
82
- label="Event log"
83
- onClick={() => props.onViewChange("raw")}
84
- pressed={props.view === "raw"}
85
- >
86
- <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
87
- </MenuActionButton>
88
76
  {mobile ? (
89
- <ShareConversationButton
90
- key={props.conversationId}
91
- conversationId={props.conversationId}
92
- layout="menu"
93
- />
77
+ <MenuActionButton
78
+ label="Conversation details"
79
+ onClick={props.onDetailsClick}
80
+ pressed={props.detailsOpen}
81
+ >
82
+ <Info aria-hidden="true" size={16} strokeWidth={2} />
83
+ </MenuActionButton>
94
84
  ) : null}
85
+ <ShareConversationButton
86
+ key={props.conversationId}
87
+ conversationId={props.conversationId}
88
+ />
95
89
  {props.copyAction}
96
90
  <MenuActionButton
97
- disabled={props.pin.disabled}
98
- label={props.pin.pinned ? "Unpin" : "Pin"}
99
- onClick={props.pin.onClick}
91
+ label={eventLogOpen ? "Back to conversation" : "Open event log"}
92
+ onClick={() => props.onViewChange(eventLogOpen ? "rich" : "raw")}
100
93
  >
101
- {props.pin.pinned ? (
102
- <PinOff aria-hidden="true" size={16} strokeWidth={2} />
94
+ {eventLogOpen ? (
95
+ <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
103
96
  ) : (
104
- <Pin aria-hidden="true" size={16} strokeWidth={2} />
97
+ <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
105
98
  )}
106
99
  </MenuActionButton>
107
- <ArchiveConversationButton {...props.archive} />
108
100
  {props.sentryConversationUrl ? (
109
101
  <a
110
102
  className={buttonVariants({ size: "menu" })}
@@ -116,15 +108,19 @@ export function ConversationHeaderActions(props: {
116
108
  Open in Sentry
117
109
  </a>
118
110
  ) : null}
119
- {mobile ? (
120
- <MenuActionButton
121
- label="Conversation details"
122
- onClick={props.onDetailsClick}
123
- pressed={props.detailsOpen}
124
- >
125
- <Info aria-hidden="true" size={16} strokeWidth={2} />
126
- </MenuActionButton>
127
- ) : null}
111
+ <Separator className="my-1 bg-dashboard-border-subtle" />
112
+ <MenuActionButton
113
+ disabled={props.pin.disabled}
114
+ label={props.pin.pinned ? "Unpin" : "Pin"}
115
+ onClick={props.pin.onClick}
116
+ >
117
+ {props.pin.pinned ? (
118
+ <PinOff aria-hidden="true" size={16} strokeWidth={2} />
119
+ ) : (
120
+ <Pin aria-hidden="true" size={16} strokeWidth={2} />
121
+ )}
122
+ </MenuActionButton>
123
+ <ArchiveConversationButton {...props.archive} />
128
124
  </div>
129
125
  );
130
126
  }
@@ -1,6 +1,6 @@
1
- import { useCallback, useState } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
  import { Archive, ArchiveRestore } from "lucide-react";
3
- import { Link } from "react-router";
3
+ import { Link, useLocation, useNavigate } from "react-router";
4
4
 
5
5
  import {
6
6
  conversationDisplayTitle,
@@ -20,7 +20,11 @@ import { ConversationSidebarAnnotations } from "./ConversationMeta";
20
20
  import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
21
21
  import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
22
22
  import { conversationPath } from "./conversationRoutes";
23
- import { useArchiveConversation } from "./conversationArchive";
23
+ import {
24
+ useArchiveConversation,
25
+ useLatestArchiveStatus,
26
+ type ArchivedFromConversationState,
27
+ } from "./conversationArchive";
24
28
  import { DisplayText } from "../display-text/DisplayText";
25
29
  import {
26
30
  attentionConversationIds,
@@ -60,6 +64,7 @@ export function ConversationHomeList(props: {
60
64
  );
61
65
  setArchivedConversation(conversation);
62
66
  }, []);
67
+ useConversationPageArchiveResult(handleArchived, handleArchiveError);
63
68
  const notices = (
64
69
  <ConversationArchiveNotices
65
70
  archivedConversation={archivedConversation}
@@ -116,6 +121,39 @@ export function ConversationHomeList(props: {
116
121
  );
117
122
  }
118
123
 
124
+ /**
125
+ * Report an archive started on the conversation page, which navigated home
126
+ * before the request finished.
127
+ */
128
+ function useConversationPageArchiveResult(
129
+ onArchived: (conversation: Conversation) => void,
130
+ onArchiveError: (conversation: Conversation, wasArchiving: boolean) => void,
131
+ ) {
132
+ const location = useLocation();
133
+ const navigate = useNavigate();
134
+ const conversation = (location.state as ArchivedFromConversationState | null)
135
+ ?.archivedConversation;
136
+ const status = useLatestArchiveStatus(conversation?.id);
137
+ useEffect(() => {
138
+ if (!conversation || (status !== "success" && status !== "error")) return;
139
+ if (status === "success") onArchived(conversation);
140
+ else onArchiveError(conversation, true);
141
+ // Clear the state so a reload or a later restore does not report again.
142
+ navigate(
143
+ { hash: location.hash, search: location.search },
144
+ { replace: true, state: null },
145
+ );
146
+ }, [
147
+ conversation,
148
+ location.hash,
149
+ location.search,
150
+ navigate,
151
+ onArchiveError,
152
+ onArchived,
153
+ status,
154
+ ]);
155
+ }
156
+
119
157
  /** Match the grouped card list while its first feed request is pending. */
120
158
  export function ConversationHomeListLoading(props: { label?: string }) {
121
159
  return (
@@ -42,7 +42,10 @@ import {
42
42
  type SidebarAnnotation,
43
43
  type SidebarAnnotationBadgeGroup,
44
44
  } from "./sidebarAnnotationBadges";
45
- import { conversationLinks, linkStatusLabel } from "./ConversationLinkedWork";
45
+ import {
46
+ linkStatusLabel,
47
+ type ConversationLink,
48
+ } from "./ConversationLinkedWork";
46
49
 
47
50
  const MOBILE_MEDIA_QUERY = "(max-width: 767px)";
48
51
 
@@ -93,13 +96,6 @@ export function ConversationPrivacyChip(props: {
93
96
  );
94
97
  }
95
98
 
96
- /** True when the conversation has at least one resource-link annotation. */
97
- export function hasConversationAnnotations(
98
- annotations: ConversationDetailReport["annotations"] | undefined,
99
- ): boolean {
100
- return Boolean(annotations?.some((annotation) => Boolean(annotation.url)));
101
- }
102
-
103
99
  /** Render plugin annotations as a newest-first stack in a conversation row. */
104
100
  export function ConversationSidebarAnnotations(props: {
105
101
  annotations: ConversationDetailReport["sidebarAnnotations"] | undefined;
@@ -401,13 +397,11 @@ function SidebarAnnotationIcon(props: {
401
397
 
402
398
  /** Render linked work as a compact strip under the conversation title. */
403
399
  export function ConversationAnnotations(props: {
404
- detail: ConversationDetailReport | undefined;
400
+ links: readonly ConversationLink[];
405
401
  }) {
406
- const links = conversationLinks(props.detail);
407
- if (links.length === 0) return null;
408
402
  return (
409
403
  <div className="flex gap-x-4 gap-y-2 overflow-x-auto">
410
- {links.map((link) => (
404
+ {props.links.map((link) => (
411
405
  <a
412
406
  className="inline-flex min-w-0 max-w-full shrink-0 items-center gap-2 rounded-md px-1 py-0.5 font-sans text-xs leading-snug text-dashboard-text no-underline transition-colors hover:text-dashboard-accent-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
413
407
  href={link.url}
@@ -1,5 +1,11 @@
1
1
  import { Ellipsis } from "lucide-react";
2
- import { useEffect, useRef, useState, type ReactNode, type Ref } from "react";
2
+ import {
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ type RefObject,
8
+ } from "react";
3
9
 
4
10
  import { HeaderIconButton } from "./ConversationHeaderActions";
5
11
 
@@ -9,11 +15,13 @@ import { HeaderIconButton } from "./ConversationHeaderActions";
9
15
  * menu before it acts.
10
16
  */
11
17
  export function ConversationOverflowMenu(props: {
12
- buttonRef?: Ref<HTMLButtonElement>;
18
+ buttonRef?: RefObject<HTMLButtonElement | null>;
13
19
  children(close: () => void): ReactNode;
14
20
  }) {
15
21
  const [open, setOpen] = useState(false);
16
22
  const menuRef = useRef<HTMLDivElement>(null);
23
+ const localButtonRef = useRef<HTMLButtonElement>(null);
24
+ const buttonRef = props.buttonRef ?? localButtonRef;
17
25
 
18
26
  useEffect(() => {
19
27
  if (!open) return;
@@ -28,6 +36,9 @@ export function ConversationOverflowMenu(props: {
28
36
  if (event.key !== "Escape" || event.defaultPrevented) return;
29
37
  event.preventDefault();
30
38
  setOpen(false);
39
+ if (menuRef.current?.contains(document.activeElement)) {
40
+ buttonRef.current?.focus({ preventScroll: true });
41
+ }
31
42
  }
32
43
 
33
44
  document.addEventListener("pointerdown", closeOnOutsideClick);
@@ -36,12 +47,12 @@ export function ConversationOverflowMenu(props: {
36
47
  document.removeEventListener("pointerdown", closeOnOutsideClick);
37
48
  document.removeEventListener("keydown", closeOnEscape, true);
38
49
  };
39
- }, [open]);
50
+ }, [buttonRef, open]);
40
51
 
41
52
  return (
42
53
  <div className="relative" ref={menuRef}>
43
54
  <HeaderIconButton
44
- ref={props.buttonRef}
55
+ ref={buttonRef}
45
56
  label={open ? "Close conversation menu" : "Conversation menu"}
46
57
  onClick={() => setOpen((value) => !value)}
47
58
  pressed={open}