@sentry/junior-dashboard 0.279.0 → 0.281.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 (164) hide show
  1. package/dist/app.js +105 -43
  2. package/dist/client-manifest.json +1 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +105 -43
  5. package/dist/public/chunks/AdminPage-M5RBVZFN.js +1 -0
  6. package/dist/public/chunks/AutomationPage-WALCREDT.js +3 -0
  7. package/dist/public/chunks/AutomationRunsPage-CV47E4UX.js +1 -0
  8. package/dist/public/chunks/AutomationsPage-57L3AQVJ.js +1 -0
  9. package/dist/public/chunks/CodePage-XJUYMFJH.js +1 -0
  10. package/dist/public/chunks/ConnectionsPage-3QQAGYWF.js +1 -0
  11. package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
  12. package/dist/public/chunks/LocationDetailPage-4IL5HQML.js +1 -0
  13. package/dist/public/chunks/LocationsPage-D2DUQAHJ.js +1 -0
  14. package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
  15. package/dist/public/chunks/PeoplePage-6F5GPQ7C.js +1 -0
  16. package/dist/public/chunks/PersonProfilePage-SPAYQPTD.js +1 -0
  17. package/dist/public/chunks/PersonalTokensPage-SHAUJ74M.js +1 -0
  18. package/dist/public/chunks/PluginUserPage-64FIWD7M.js +1 -0
  19. package/dist/public/chunks/RepositoryPage-L7IHOXSN.js +1 -0
  20. package/dist/public/chunks/SettingsPage-U7WG4FHE.js +1 -0
  21. package/dist/public/chunks/SystemPage-ZBC43OJH.js +1 -0
  22. package/dist/public/chunks/WorkspaceFormPage-3NVIIZ4P.js +2 -0
  23. package/dist/public/chunks/WorkspacesPage-G73HPBDD.js +1 -0
  24. package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
  25. package/dist/public/chunks/chunk-2J3LGF5Q.js +1 -0
  26. package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
  27. package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
  28. package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
  29. package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
  30. package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
  31. package/dist/public/chunks/chunk-564V7YHG.js +1 -0
  32. package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
  33. package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
  34. package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
  35. package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
  36. package/dist/public/chunks/chunk-6FHY65EQ.js +1 -0
  37. package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
  38. package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
  39. package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -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-DEEO4D4N.js +1 -0
  44. package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
  45. package/dist/public/chunks/chunk-FE522UEA.js +1 -0
  46. package/dist/public/chunks/chunk-FSGVY7HB.js +1 -0
  47. package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
  48. package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
  49. package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
  50. package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
  51. package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
  52. package/dist/public/chunks/chunk-GZBMHYYU.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-II4NXLSQ.js +2 -0
  57. package/dist/public/chunks/chunk-ILH47V7O.js +1 -0
  58. package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
  59. package/dist/public/chunks/chunk-J37MXUED.js +1 -0
  60. package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
  61. package/dist/public/chunks/chunk-JPL2NRWK.js +1 -0
  62. package/dist/public/chunks/chunk-JVJ5UWIK.js +1 -0
  63. package/dist/public/chunks/chunk-K4TYMAA3.js +63 -0
  64. package/dist/public/chunks/chunk-KDUTK357.js +1 -0
  65. package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
  66. package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
  67. package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
  68. package/dist/public/chunks/chunk-LZYAHJVQ.js +1 -0
  69. package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
  70. package/dist/public/chunks/chunk-MKKFXRYO.js +1 -0
  71. package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
  72. package/dist/public/chunks/chunk-NGQRNS3C.js +1 -0
  73. package/dist/public/chunks/chunk-ONDKE72P.js +1 -0
  74. package/dist/public/chunks/chunk-PG7M7L24.js +1 -0
  75. package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
  76. package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
  77. package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
  78. package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
  79. package/dist/public/chunks/chunk-QQUFROWT.js +1 -0
  80. package/dist/public/chunks/chunk-RB5FBDEY.js +1 -0
  81. package/dist/public/chunks/chunk-RGXXD4DM.js +41 -0
  82. package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
  83. package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
  84. package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
  85. package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
  86. package/dist/public/chunks/chunk-TC7TU4GK.js +1 -0
  87. package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
  88. package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
  89. package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
  90. package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
  91. package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
  92. package/dist/public/chunks/chunk-VC6IS564.js +1 -0
  93. package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
  94. package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
  95. package/dist/public/chunks/chunk-XDUSY26N.js +55 -0
  96. package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
  97. package/dist/public/chunks/chunk-YPMUHXYT.js +147 -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-4OAAOHF4.js +68 -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/BottomSheet.tsx +1 -1
  110. package/src/client/components/DetailsPanel.tsx +2 -1
  111. package/src/client/components/Drawer.tsx +12 -71
  112. package/src/client/components/DrawerContent.tsx +76 -0
  113. package/src/client/components/ProfileMenu.tsx +12 -3
  114. package/src/client/components/UserInputForm.tsx +28 -5
  115. package/src/client/conversations/ConversationBrief.tsx +129 -65
  116. package/src/client/conversations/ConversationCatchUp.tsx +107 -0
  117. package/src/client/conversations/ConversationContextTags.tsx +110 -0
  118. package/src/client/conversations/ConversationDetails.tsx +13 -14
  119. package/src/client/conversations/ConversationEventLog.tsx +135 -70
  120. package/src/client/conversations/ConversationHeader.tsx +74 -65
  121. package/src/client/conversations/ConversationHeaderActions.tsx +36 -38
  122. package/src/client/conversations/ConversationHomeList.tsx +41 -3
  123. package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
  124. package/src/client/conversations/ConversationMeta.tsx +21 -27
  125. package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
  126. package/src/client/conversations/ConversationPage.tsx +219 -157
  127. package/src/client/conversations/ConversationTranscript.tsx +234 -186
  128. package/src/client/conversations/ConversationUsageBreakdown.tsx +142 -64
  129. package/src/client/conversations/DetailsList.tsx +7 -1
  130. package/src/client/conversations/PendingMailboxStack.tsx +88 -44
  131. package/src/client/conversations/ShareConversationButton.tsx +5 -50
  132. package/src/client/conversations/SlackMark.tsx +16 -4
  133. package/src/client/conversations/TranscriptActivityGroup.tsx +7 -14
  134. package/src/client/conversations/TranscriptEventView.tsx +7 -3
  135. package/src/client/conversations/TranscriptReasoningView.tsx +4 -2
  136. package/src/client/conversations/TranscriptRows.tsx +61 -16
  137. package/src/client/conversations/TranscriptView.tsx +25 -13
  138. package/src/client/conversations/conversationArchive.ts +22 -0
  139. package/src/client/conversations/conversationCatchUpState.ts +96 -0
  140. package/src/client/conversations/conversationMemory.ts +8 -1
  141. package/src/client/conversations/queries.ts +85 -46
  142. package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
  143. package/src/client/conversations/transcriptBottomPinning.ts +40 -8
  144. package/src/client/conversations/transcriptListRows.ts +215 -0
  145. package/src/client/conversations/transcriptReadPosition.ts +24 -2
  146. package/src/client/conversations/turnContext.ts +1 -1
  147. package/src/client/conversations/virtualRows.ts +168 -0
  148. package/src/client/http.ts +6 -5
  149. package/src/client/pages/admin/AdminPage.tsx +1 -1
  150. package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
  151. package/src/client/pages/memory/memoryDashboard.ts +1 -1
  152. package/src/client/pages/memory/memoryRecord.ts +1 -1
  153. package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
  154. package/src/client/pages/user/PluginUserPage.tsx +0 -6
  155. package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
  156. package/src/client/primaryNav.ts +1 -1
  157. package/src/client/review-desk/reviewItemFormat.ts +1 -1
  158. package/src/client/sentry.ts +53 -3
  159. package/src/conversation-cache.ts +8 -10
  160. package/src/mock-reporting/fixtures.ts +43 -8
  161. package/src/mock-reporting/routes.ts +40 -3
  162. package/src/public-assets.ts +28 -20
  163. package/src/shell.ts +12 -2
  164. package/dist/public/client-ZCZXZBTL.js +0 -460
@@ -1,5 +1,11 @@
1
- import { ChevronRight } from "lucide-react";
2
- import type { ReactNode } from "react";
1
+ import { ChevronDown, ChevronRight } from "lucide-react";
2
+ import {
3
+ useId,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ type ReactNode,
8
+ } from "react";
3
9
  import type {
4
10
  ActorIdentity,
5
11
  ConversationDetailReport,
@@ -20,6 +26,9 @@ type ConversationBriefReport = NonNullable<ConversationDetailReport["brief"]>;
20
26
 
21
27
  type BriefContent = ConversationBriefReport["content"];
22
28
 
29
+ const SUMMARY_PREVIEW_LINES = 4;
30
+ const SUMMARY_FADE_LINES = 1.65;
31
+
23
32
  const outcomeTones: Record<BriefContent["outcome"]["status"], StatusChipTone> =
24
33
  {
25
34
  abandoned: "neutral",
@@ -39,20 +48,9 @@ export function ConversationBrief(props: {
39
48
  const mentions = props.brief.mentions;
40
49
  if (props.variant === "summary") {
41
50
  return (
42
- <div className="grid min-w-0 gap-4">
51
+ <BriefSummary>
43
52
  <DisplayText mentions={mentions} text={content.summary} />
44
- {content.openDecisions.length > 0 ? (
45
- <div className="grid gap-2">
46
- <h4 className="m-0 text-sm font-medium text-dashboard-text">
47
- Open decisions
48
- </h4>
49
- <BriefOpenDecisions
50
- decisions={content.openDecisions}
51
- mentions={mentions}
52
- />
53
- </div>
54
- ) : null}
55
- </div>
53
+ </BriefSummary>
56
54
  );
57
55
  }
58
56
  return (
@@ -80,26 +78,9 @@ export function ConversationBrief(props: {
80
78
  </Detail>
81
79
  </DetailList>
82
80
  </BriefSection>
83
- {content.decisions.length || content.openDecisions.length ? (
81
+ {content.decisions.length ? (
84
82
  <BriefSection title="Decisions">
85
- <DetailList>
86
- {content.decisions.length ? (
87
- <Detail label="Decisions">
88
- <BriefDecisions
89
- decisions={content.decisions}
90
- mentions={mentions}
91
- />
92
- </Detail>
93
- ) : null}
94
- {content.openDecisions.length ? (
95
- <Detail label="Open decisions">
96
- <BriefOpenDecisions
97
- decisions={content.openDecisions}
98
- mentions={mentions}
99
- />
100
- </Detail>
101
- ) : null}
102
- </DetailList>
83
+ <BriefDecisions decisions={content.decisions} mentions={mentions} />
103
84
  </BriefSection>
104
85
  ) : null}
105
86
  {content.facts.length ||
@@ -162,6 +143,120 @@ export function ConversationBrief(props: {
162
143
  );
163
144
  }
164
145
 
146
+ function BriefSummary(props: { children: ReactNode }) {
147
+ const id = useId();
148
+ const previewRef = useRef<HTMLDivElement>(null);
149
+ const contentRef = useRef<HTMLDivElement>(null);
150
+ const toggleRef = useRef<HTMLButtonElement>(null);
151
+ const [hasMore, setHasMore] = useState(false);
152
+ const [expanded, setExpanded] = useState(false);
153
+ const collapsed = hasMore && !expanded;
154
+
155
+ useLayoutEffect(() => {
156
+ const preview = previewRef.current;
157
+ const content = contentRef.current;
158
+ if (!preview || !content) return;
159
+
160
+ const measure = () => {
161
+ const lineHeight = Number.parseFloat(
162
+ getComputedStyle(preview).lineHeight,
163
+ );
164
+ const height = lineHeight * SUMMARY_PREVIEW_LINES;
165
+ const overflows = content.getBoundingClientRect().height > height + 0.5;
166
+ const focused = document.activeElement;
167
+ if (
168
+ overflows &&
169
+ focused &&
170
+ content.contains(focused) &&
171
+ focused.getBoundingClientRect().bottom >
172
+ preview.getBoundingClientRect().top +
173
+ height -
174
+ lineHeight * SUMMARY_FADE_LINES
175
+ ) {
176
+ setExpanded(true);
177
+ } else if (!overflows && focused && focused === toggleRef.current) {
178
+ preview.focus({ preventScroll: true });
179
+ }
180
+ setHasMore(overflows);
181
+ };
182
+ measure();
183
+ const observer = new ResizeObserver(measure);
184
+ observer.observe(content);
185
+ return () => observer.disconnect();
186
+ }, []);
187
+
188
+ useLayoutEffect(() => {
189
+ const toggle = toggleRef.current;
190
+ if (!expanded && toggle && document.activeElement === toggle) {
191
+ toggle.scrollIntoView({ block: "nearest" });
192
+ }
193
+ }, [expanded]);
194
+
195
+ return (
196
+ <div className="relative min-w-0">
197
+ <div
198
+ className={cn("leading-6 md:leading-7", collapsed && "overflow-clip")}
199
+ id={id}
200
+ onFocusCapture={(event) => {
201
+ if (!collapsed) return;
202
+ // Keep focused links clear of both the clipping and the fade.
203
+ const preview = event.currentTarget;
204
+ const lineHeight = Number.parseFloat(
205
+ getComputedStyle(preview).lineHeight,
206
+ );
207
+ if (
208
+ event.target.getBoundingClientRect().bottom >
209
+ preview.getBoundingClientRect().bottom -
210
+ lineHeight * SUMMARY_FADE_LINES
211
+ ) {
212
+ setExpanded(true);
213
+ }
214
+ }}
215
+ ref={previewRef}
216
+ style={
217
+ collapsed
218
+ ? {
219
+ maskImage: `linear-gradient(black calc(100% - ${SUMMARY_FADE_LINES}lh), transparent)`,
220
+ maxHeight: `${SUMMARY_PREVIEW_LINES}lh`,
221
+ }
222
+ : undefined
223
+ }
224
+ tabIndex={-1}
225
+ >
226
+ <div ref={contentRef}>{props.children}</div>
227
+ </div>
228
+ {hasMore ? (
229
+ <button
230
+ aria-controls={id}
231
+ aria-expanded={expanded}
232
+ aria-label={
233
+ expanded ? "Collapse Brief summary" : "Expand Brief summary"
234
+ }
235
+ className={cn(
236
+ "group flex h-6 w-8 cursor-pointer items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-dashboard-focus",
237
+ collapsed
238
+ ? "absolute bottom-0 left-1/2 -translate-x-1/2"
239
+ : "mx-auto mt-1",
240
+ )}
241
+ onClick={() => setExpanded((open) => !open)}
242
+ ref={toggleRef}
243
+ type="button"
244
+ >
245
+ <span
246
+ aria-hidden="true"
247
+ className="grid size-5 place-items-center rounded-full border border-dashboard-border bg-dashboard-ink text-dashboard-text-solid transition-[filter] group-hover:brightness-95 dark:bg-dashboard-text-solid dark:text-dashboard-text-inverse"
248
+ >
249
+ <ChevronDown
250
+ className="size-4 group-aria-expanded:rotate-180"
251
+ strokeWidth={2.25}
252
+ />
253
+ </span>
254
+ </button>
255
+ ) : null}
256
+ </div>
257
+ );
258
+ }
259
+
165
260
  function BriefSection(props: { children: ReactNode; title: string }) {
166
261
  return (
167
262
  <Collapsible className="min-w-0 border-t border-dashboard-border pt-3">
@@ -221,37 +316,6 @@ function BriefDecisions(props: {
221
316
  );
222
317
  }
223
318
 
224
- function BriefOpenDecisions(props: {
225
- decisions: BriefContent["openDecisions"];
226
- mentions?: readonly ActorIdentity[];
227
- }) {
228
- return (
229
- <ul className="m-0 grid list-none gap-2 p-0">
230
- {props.decisions.map((decision, index) => (
231
- <li key={`${decision.text}:${index}`}>
232
- <div>
233
- <DisplayText
234
- inline
235
- mentions={props.mentions}
236
- text={decision.text}
237
- />
238
- </div>
239
- {decision.owner ? (
240
- <div className="mt-1 text-xs text-dashboard-text-muted">
241
- Owner:{" "}
242
- <DisplayText
243
- inline
244
- mentions={props.mentions}
245
- text={decision.owner}
246
- />
247
- </div>
248
- ) : null}
249
- </li>
250
- ))}
251
- </ul>
252
- );
253
- }
254
-
255
319
  function BriefList(props: {
256
320
  items: string[];
257
321
  mentions?: readonly ActorIdentity[];
@@ -0,0 +1,107 @@
1
+ import { useContext, useEffect, useRef } from "react";
2
+ import { ArrowDown, Sparkles } from "lucide-react";
3
+
4
+ import { Button } from "../components/ui/button";
5
+ import { Skeleton } from "../components/ui/skeleton";
6
+ import { InlineError } from "../components/InlineError";
7
+ import { DisplayText } from "../display-text/DisplayText";
8
+ import { useConversationCatchUpState } from "./conversationCatchUpState";
9
+ import { TranscriptJumpToLatestContext } from "./transcriptBottomPinning";
10
+
11
+ const cardClass =
12
+ "grid gap-2.5 rounded-lg border border-dashboard-accent/20 bg-dashboard-accent/10 px-3.5 py-3 font-sans text-sm leading-relaxed text-dashboard-text";
13
+
14
+ /** Offer the catch-up inside the "New" marker, so it takes no extra row. */
15
+ export function ConversationCatchUpButton() {
16
+ const state = useConversationCatchUpState();
17
+ if (!state || state.data || state.pending) return null;
18
+ return (
19
+ <button
20
+ className="inline-flex h-6 shrink-0 cursor-pointer items-center gap-1 rounded-full border border-dashboard-accent/30 bg-transparent px-2.5 font-sans text-xs font-medium text-dashboard-accent-text transition-colors hover:border-dashboard-accent/60 hover:bg-dashboard-accent/10 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus/55"
21
+ onClick={state.request}
22
+ type="button"
23
+ >
24
+ <Sparkles aria-hidden="true" size={12} strokeWidth={2} />
25
+ Catch me up
26
+ </button>
27
+ );
28
+ }
29
+
30
+ /** Show the loading, loaded, or failed catch-up under the "New" marker. */
31
+ export function ConversationCatchUpCard() {
32
+ const state = useConversationCatchUpState();
33
+ const jumpToLatest = useContext(TranscriptJumpToLatestContext);
34
+ const cardRef = useRef<HTMLDivElement>(null);
35
+ const shown = state?.data ? "data" : state?.pending ? "pending" : undefined;
36
+ // The transcript holds the row below the reader's place, so the card grows
37
+ // up, out of view. Bring it back after that scroll fix runs.
38
+ useEffect(() => {
39
+ if (!shown) return;
40
+ const frame = requestAnimationFrame(() => {
41
+ // Keep the "New" marker above the card in view too.
42
+ const marker = cardRef.current?.closest(
43
+ "[data-transcript-unread-marker]",
44
+ );
45
+ marker?.scrollIntoView({ block: "nearest" });
46
+ });
47
+ return () => cancelAnimationFrame(frame);
48
+ }, [shown]);
49
+
50
+ if (state?.data) {
51
+ return (
52
+ <div
53
+ aria-label="Catch-up"
54
+ className={cardClass}
55
+ ref={cardRef}
56
+ role="group"
57
+ >
58
+ <DisplayText text={state.data.summary} />
59
+ {state.data.needsYou ? (
60
+ <p className="m-0 border-t border-dashboard-accent/15 pt-2.5">
61
+ <span className="font-semibold text-dashboard-accent-text">
62
+ Needs you
63
+ </span>
64
+ <span
65
+ aria-hidden="true"
66
+ className="px-1.5 text-dashboard-text-muted"
67
+ >
68
+ ·
69
+ </span>
70
+ {state.data.needsYou}
71
+ </p>
72
+ ) : null}
73
+ {jumpToLatest ? (
74
+ <Button
75
+ className="-mb-1 h-7 justify-self-center gap-1.5 px-2 text-xs"
76
+ onClick={jumpToLatest}
77
+ variant="quiet"
78
+ >
79
+ <ArrowDown aria-hidden="true" size={14} strokeWidth={2} />
80
+ Jump to latest
81
+ </Button>
82
+ ) : null}
83
+ </div>
84
+ );
85
+ }
86
+ if (state?.error) {
87
+ return (
88
+ <InlineError className="text-center text-xs">
89
+ Catch-up failed. Try again.
90
+ </InlineError>
91
+ );
92
+ }
93
+ if (state?.pending) {
94
+ return (
95
+ <div
96
+ aria-label="Summarizing new messages"
97
+ className={cardClass}
98
+ ref={cardRef}
99
+ role="status"
100
+ >
101
+ <Skeleton className="h-4 w-full" />
102
+ <Skeleton className="h-4 w-2/3" />
103
+ </div>
104
+ );
105
+ }
106
+ return null;
107
+ }
@@ -0,0 +1,110 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { slackLocationLabel } from "../format";
4
+ import { cn } from "../styles";
5
+ import type { Conversation } from "../types";
6
+ import { SlackMark } from "./SlackMark";
7
+
8
+ function githubRepositoryFromUrl(href: string) {
9
+ const url = new URL(href);
10
+ if (url.origin !== "https://github.com") return undefined;
11
+ const [, owner, repo] = url.pathname.split("/");
12
+ if (!owner || !repo) return undefined;
13
+ return {
14
+ label: `${owner}/${repo}`,
15
+ href: `${url.origin}/${owner}/${repo}`,
16
+ };
17
+ }
18
+
19
+ function singleGitHubRepository(annotations: Conversation["annotations"]) {
20
+ let repository: ReturnType<typeof githubRepositoryFromUrl>;
21
+ for (const annotation of annotations ?? []) {
22
+ if (annotation.plugin !== "github" || !annotation.url) continue;
23
+ const candidate = githubRepositoryFromUrl(annotation.url);
24
+ if (!candidate) continue;
25
+ if (
26
+ repository &&
27
+ repository.href.toLowerCase() !== candidate.href.toLowerCase()
28
+ ) {
29
+ return undefined;
30
+ }
31
+ repository ??= candidate;
32
+ }
33
+ return repository;
34
+ }
35
+
36
+ function ContextTag(props: {
37
+ href?: string;
38
+ icon: ReactNode;
39
+ label: string;
40
+ title?: string;
41
+ }) {
42
+ const className =
43
+ "group/context-tag inline-flex h-6 min-w-0 items-center gap-1 rounded-md border border-dashboard-border-subtle px-1.5 font-sans text-xs font-medium text-dashboard-text-muted";
44
+ const content = (
45
+ <>
46
+ {props.icon}
47
+ <span className="truncate">{props.label}</span>
48
+ </>
49
+ );
50
+
51
+ return props.href ? (
52
+ <a
53
+ className={cn(
54
+ className,
55
+ "no-underline transition-colors hover:border-dashboard-border-strong hover:bg-dashboard-fill-soft hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
56
+ )}
57
+ href={props.href}
58
+ rel="noreferrer"
59
+ target="_blank"
60
+ title={props.title ?? props.label}
61
+ >
62
+ {content}
63
+ </a>
64
+ ) : (
65
+ <span className={className} title={props.label}>
66
+ {content}
67
+ </span>
68
+ );
69
+ }
70
+
71
+ function GitHubMark() {
72
+ return (
73
+ <svg
74
+ aria-label="GitHub"
75
+ className="size-3 shrink-0 transition-colors group-hover/context-tag:text-dashboard-text-solid group-focus-visible/context-tag:text-dashboard-text-solid"
76
+ fill="currentColor"
77
+ role="img"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ {/* Keep the geometry from packages/docs/public/integrations/github.svg. */}
81
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ /** Show source context and a repository tag when linked work points to one repository. */
87
+ export function conversationContextTags(
88
+ conversation: Conversation | undefined,
89
+ ) {
90
+ const location =
91
+ conversation && slackLocationLabel(conversation, { includeId: false });
92
+ const repository = singleGitHubRepository(conversation?.annotations);
93
+ if (!location && !repository) return null;
94
+
95
+ return (
96
+ <div className="flex min-w-0 items-center gap-1.5">
97
+ {location ? (
98
+ <ContextTag
99
+ href={conversation?.locationUrl}
100
+ icon={
101
+ <SlackMark className="size-3 shrink-0 group-hover/context-tag:[&>path]:fill-(--slack-mark-color) group-focus-visible/context-tag:[&>path]:fill-(--slack-mark-color)" />
102
+ }
103
+ label={location}
104
+ title={`Open ${location} in Slack`}
105
+ />
106
+ ) : null}
107
+ {repository ? <ContextTag {...repository} icon={<GitHubMark />} /> : null}
108
+ </div>
109
+ );
110
+ }
@@ -37,9 +37,7 @@ import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
37
37
  type Brief = NonNullable<ConversationDetailReport["brief"]>;
38
38
 
39
39
  /**
40
- * Show conversation details in the order a reader asks about them: what the
41
- * work is (the Brief), what needs them, what it changed or waits on, who did
42
- * it, and what it cost. Each fact shows here only, not in the title bar.
40
+ * Surface work that needs the reader, then the Brief and its linked work.
43
41
  */
44
42
  export function ConversationDetails(props: {
45
43
  /** Viewer's auto-archive control. Null for non-participants. */
@@ -153,21 +151,22 @@ export function ConversationDetails(props: {
153
151
  }
154
152
  onClose={props.onClose}
155
153
  >
156
- <div className="grid min-w-0 content-start gap-3 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
154
+ <div className="grid min-w-0 content-start gap-2 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
155
+ <ConversationReviewItems
156
+ conversationId={props.conversationId}
157
+ highlight={props.highlightReview}
158
+ />
157
159
  {props.brief ? (
158
160
  <div className="min-w-0 break-words px-1 pb-1 font-sans text-sm leading-relaxed text-dashboard-text-muted">
159
161
  <ConversationBrief brief={props.brief} variant="summary" />
160
162
  </div>
161
163
  ) : null}
162
- <ConversationReviewItems
163
- conversationId={props.conversationId}
164
- highlight={props.highlightReview}
165
- />
166
164
  {props.links.length > 0 || watches.length > 0 ? (
167
165
  // A Watch waits on something. It shows with linked work, so the
168
166
  // work and what Junior waits on read as one list.
169
167
  <DetailsCard
170
168
  count={props.links.length || watches.length}
169
+ flat
171
170
  title={props.links.length > 0 ? "Linked work" : "Waiting on"}
172
171
  >
173
172
  {props.links.length > 0 ? (
@@ -191,7 +190,7 @@ export function ConversationDetails(props: {
191
190
  <DetailsCard>{props.autoArchive}</DetailsCard>
192
191
  ) : null}
193
192
  {agents?.length ? (
194
- <DetailsCard count={agents.length} title="Subagents">
193
+ <DetailsCard count={agents.length} flat title="Subagents">
195
194
  <ConversationChildAgents
196
195
  conversationId={props.conversationId}
197
196
  onOpenAgent={(target) => {
@@ -203,13 +202,13 @@ export function ConversationDetails(props: {
203
202
  </DetailsCard>
204
203
  ) : null}
205
204
  {props.overview != null ? (
206
- <DetailsCard>{props.overview}</DetailsCard>
207
- ) : null}
208
- {props.usage != null ? (
209
- <DetailsCard title="Usage">{props.usage}</DetailsCard>
205
+ <DetailsCard flat>{props.overview}</DetailsCard>
210
206
  ) : null}
207
+ {props.usage}
211
208
  {props.identity != null ? (
212
- <DetailsCard title="Participants">{props.identity}</DetailsCard>
209
+ <DetailsCard flat title="Participants">
210
+ {props.identity}
211
+ </DetailsCard>
213
212
  ) : null}
214
213
  {memoryList?.events.length ? (
215
214
  <ConversationMemoryCard