@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.
- package/dist/app.js +105 -43
- package/dist/client-manifest.json +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +105 -43
- package/dist/public/chunks/AdminPage-M5RBVZFN.js +1 -0
- package/dist/public/chunks/AutomationPage-WALCREDT.js +3 -0
- package/dist/public/chunks/AutomationRunsPage-CV47E4UX.js +1 -0
- package/dist/public/chunks/AutomationsPage-57L3AQVJ.js +1 -0
- package/dist/public/chunks/CodePage-XJUYMFJH.js +1 -0
- package/dist/public/chunks/ConnectionsPage-3QQAGYWF.js +1 -0
- package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
- package/dist/public/chunks/LocationDetailPage-4IL5HQML.js +1 -0
- package/dist/public/chunks/LocationsPage-D2DUQAHJ.js +1 -0
- package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
- package/dist/public/chunks/PeoplePage-6F5GPQ7C.js +1 -0
- package/dist/public/chunks/PersonProfilePage-SPAYQPTD.js +1 -0
- package/dist/public/chunks/PersonalTokensPage-SHAUJ74M.js +1 -0
- package/dist/public/chunks/PluginUserPage-64FIWD7M.js +1 -0
- package/dist/public/chunks/RepositoryPage-L7IHOXSN.js +1 -0
- package/dist/public/chunks/SettingsPage-U7WG4FHE.js +1 -0
- package/dist/public/chunks/SystemPage-ZBC43OJH.js +1 -0
- package/dist/public/chunks/WorkspaceFormPage-3NVIIZ4P.js +2 -0
- package/dist/public/chunks/WorkspacesPage-G73HPBDD.js +1 -0
- package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
- package/dist/public/chunks/chunk-2J3LGF5Q.js +1 -0
- package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
- package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
- package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
- package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
- package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
- package/dist/public/chunks/chunk-564V7YHG.js +1 -0
- package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
- package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
- package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
- package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
- package/dist/public/chunks/chunk-6FHY65EQ.js +1 -0
- package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
- package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
- package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
- package/dist/public/chunks/chunk-BKOACADP.js +1 -0
- package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
- package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
- package/dist/public/chunks/chunk-DEEO4D4N.js +1 -0
- package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
- package/dist/public/chunks/chunk-FE522UEA.js +1 -0
- package/dist/public/chunks/chunk-FSGVY7HB.js +1 -0
- package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
- package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
- package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
- package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
- package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
- package/dist/public/chunks/chunk-GZBMHYYU.js +1 -0
- package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
- package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
- package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
- package/dist/public/chunks/chunk-II4NXLSQ.js +2 -0
- package/dist/public/chunks/chunk-ILH47V7O.js +1 -0
- package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
- package/dist/public/chunks/chunk-J37MXUED.js +1 -0
- package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
- package/dist/public/chunks/chunk-JPL2NRWK.js +1 -0
- package/dist/public/chunks/chunk-JVJ5UWIK.js +1 -0
- package/dist/public/chunks/chunk-K4TYMAA3.js +63 -0
- package/dist/public/chunks/chunk-KDUTK357.js +1 -0
- package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
- package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
- package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
- package/dist/public/chunks/chunk-LZYAHJVQ.js +1 -0
- package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
- package/dist/public/chunks/chunk-MKKFXRYO.js +1 -0
- package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
- package/dist/public/chunks/chunk-NGQRNS3C.js +1 -0
- package/dist/public/chunks/chunk-ONDKE72P.js +1 -0
- package/dist/public/chunks/chunk-PG7M7L24.js +1 -0
- package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
- package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
- package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
- package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
- package/dist/public/chunks/chunk-QQUFROWT.js +1 -0
- package/dist/public/chunks/chunk-RB5FBDEY.js +1 -0
- package/dist/public/chunks/chunk-RGXXD4DM.js +41 -0
- package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
- package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
- package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
- package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
- package/dist/public/chunks/chunk-TC7TU4GK.js +1 -0
- package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
- package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
- package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
- package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
- package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
- package/dist/public/chunks/chunk-VC6IS564.js +1 -0
- package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
- package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
- package/dist/public/chunks/chunk-XDUSY26N.js +55 -0
- package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
- package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
- package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
- package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
- package/dist/public/client-4OAAOHF4.js +68 -0
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +1 -1
- package/src/assets.ts +4 -1
- package/src/client/App.tsx +100 -23
- package/src/client/TracedRoutes.tsx +8 -4
- package/src/client/code-highlighter.ts +26 -0
- package/src/client/code.tsx +2 -22
- package/src/client/components/BottomSheet.tsx +1 -1
- package/src/client/components/DetailsPanel.tsx +2 -1
- package/src/client/components/Drawer.tsx +12 -71
- package/src/client/components/DrawerContent.tsx +76 -0
- package/src/client/components/ProfileMenu.tsx +12 -3
- package/src/client/components/UserInputForm.tsx +28 -5
- package/src/client/conversations/ConversationBrief.tsx +129 -65
- package/src/client/conversations/ConversationCatchUp.tsx +107 -0
- package/src/client/conversations/ConversationContextTags.tsx +110 -0
- package/src/client/conversations/ConversationDetails.tsx +13 -14
- package/src/client/conversations/ConversationEventLog.tsx +135 -70
- package/src/client/conversations/ConversationHeader.tsx +74 -65
- package/src/client/conversations/ConversationHeaderActions.tsx +36 -38
- package/src/client/conversations/ConversationHomeList.tsx +41 -3
- package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
- package/src/client/conversations/ConversationMeta.tsx +21 -27
- package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
- package/src/client/conversations/ConversationPage.tsx +219 -157
- package/src/client/conversations/ConversationTranscript.tsx +234 -186
- package/src/client/conversations/ConversationUsageBreakdown.tsx +142 -64
- package/src/client/conversations/DetailsList.tsx +7 -1
- package/src/client/conversations/PendingMailboxStack.tsx +88 -44
- package/src/client/conversations/ShareConversationButton.tsx +5 -50
- package/src/client/conversations/SlackMark.tsx +16 -4
- package/src/client/conversations/TranscriptActivityGroup.tsx +7 -14
- package/src/client/conversations/TranscriptEventView.tsx +7 -3
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -2
- package/src/client/conversations/TranscriptRows.tsx +61 -16
- package/src/client/conversations/TranscriptView.tsx +25 -13
- package/src/client/conversations/conversationArchive.ts +22 -0
- package/src/client/conversations/conversationCatchUpState.ts +96 -0
- package/src/client/conversations/conversationMemory.ts +8 -1
- package/src/client/conversations/queries.ts +85 -46
- package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
- package/src/client/conversations/transcriptBottomPinning.ts +40 -8
- package/src/client/conversations/transcriptListRows.ts +215 -0
- package/src/client/conversations/transcriptReadPosition.ts +24 -2
- package/src/client/conversations/turnContext.ts +1 -1
- package/src/client/conversations/virtualRows.ts +168 -0
- package/src/client/http.ts +6 -5
- package/src/client/pages/admin/AdminPage.tsx +1 -1
- package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
- package/src/client/pages/memory/memoryDashboard.ts +1 -1
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
- package/src/client/pages/user/PluginUserPage.tsx +0 -6
- package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/reviewItemFormat.ts +1 -1
- package/src/client/sentry.ts +53 -3
- package/src/conversation-cache.ts +8 -10
- package/src/mock-reporting/fixtures.ts +43 -8
- package/src/mock-reporting/routes.ts +40 -3
- package/src/public-assets.ts +28 -20
- package/src/shell.ts +12 -2
- package/dist/public/client-ZCZXZBTL.js +0 -460
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./eventLog";
|
|
16
16
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
17
17
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
18
|
+
import { useVirtualRows } from "./virtualRows";
|
|
18
19
|
|
|
19
20
|
const rowClass =
|
|
20
21
|
"grid grid-cols-[12ch_minmax(0,1fr)] items-baseline gap-x-3 px-4 @min-[32rem]:grid-cols-[12ch_17ch_minmax(0,1fr)]";
|
|
@@ -28,6 +29,8 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
28
29
|
conversation: ConversationTranscript;
|
|
29
30
|
onSearchChange(value: string): void;
|
|
30
31
|
onSelectedSeqChange?(seq: number | undefined): void;
|
|
32
|
+
/** The row that holds the reader's place, from the scroll owner. */
|
|
33
|
+
scrollAnchorKey?: () => string | undefined;
|
|
31
34
|
selectedSeq?: number;
|
|
32
35
|
}) {
|
|
33
36
|
const { conversation } = props;
|
|
@@ -50,17 +53,46 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
50
53
|
);
|
|
51
54
|
// Resolve the selection from current data so polling cannot leave stale details.
|
|
52
55
|
const selected = events.find((event) => event.seq === selectedSeq);
|
|
56
|
+
// An arrow key can select a row that mounts only on the next render.
|
|
57
|
+
const focusSeqRef = useRef<number>(undefined);
|
|
58
|
+
const focusedSeqRef = useRef<number>(undefined);
|
|
59
|
+
const indexBySeq = useMemo(
|
|
60
|
+
() => new Map(visibleRows.map((row, index) => [row.event.seq, index])),
|
|
61
|
+
[visibleRows],
|
|
62
|
+
);
|
|
63
|
+
const indexOf = (seq: number | undefined) =>
|
|
64
|
+
seq === undefined ? undefined : indexBySeq.get(seq);
|
|
65
|
+
// The open entry, the focused row, and the row that holds the reader's
|
|
66
|
+
// place stay mounted.
|
|
67
|
+
const virtualizer = useVirtualRows({
|
|
68
|
+
count: visibleRows.length,
|
|
69
|
+
defaultSize: (sizeClass) => (sizeClass === "metadata" ? 56 : 40),
|
|
70
|
+
getItemKey: (index) => visibleRows[index]?.event.seq ?? index,
|
|
71
|
+
keptIndexes: [
|
|
72
|
+
indexOf(selectedSeq),
|
|
73
|
+
indexOf(focusedSeqRef.current),
|
|
74
|
+
indexOf(anchorSeq(props.scrollAnchorKey?.())),
|
|
75
|
+
],
|
|
76
|
+
listRef,
|
|
77
|
+
sizeClass: (index) => (visibleRows[index]?.metadata ? "metadata" : "plain"),
|
|
78
|
+
});
|
|
79
|
+
const virtualRows = virtualizer.getVirtualItems();
|
|
53
80
|
useEffect(() => {
|
|
54
|
-
if (selectedSeq === undefined
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
81
|
+
if (selectedSeq === undefined) return;
|
|
57
82
|
const row = listRef.current?.querySelector(
|
|
58
83
|
`[data-event-seq="${selectedSeq}"]`,
|
|
59
84
|
);
|
|
60
85
|
if (!row) return;
|
|
86
|
+
if (focusSeqRef.current === selectedSeq) {
|
|
87
|
+
focusSeqRef.current = undefined;
|
|
88
|
+
row
|
|
89
|
+
.querySelector<HTMLButtonElement>("button")
|
|
90
|
+
?.focus({ preventScroll: true });
|
|
91
|
+
}
|
|
92
|
+
if (revealedSeqRef.current === selectedSeq) return;
|
|
61
93
|
revealedSeqRef.current = selectedSeq;
|
|
62
94
|
row.scrollIntoView({ block: "nearest" });
|
|
63
|
-
}, [selectedSeq,
|
|
95
|
+
}, [selectedSeq, virtualRows]);
|
|
64
96
|
|
|
65
97
|
if (conversation.eventHistory.status === "expired") {
|
|
66
98
|
return (
|
|
@@ -101,84 +133,112 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
101
133
|
<EventLogColumns />
|
|
102
134
|
</div>
|
|
103
135
|
<div className="font-mono text-xs">
|
|
136
|
+
{/* Only rows near the viewport mount. Each row is placed at its
|
|
137
|
+
measured offset, so the list keeps its full height. */}
|
|
104
138
|
<ol
|
|
105
139
|
aria-label="Events"
|
|
106
|
-
className="m-0 list-none
|
|
140
|
+
className="relative m-0 list-none p-0"
|
|
107
141
|
ref={listRef}
|
|
142
|
+
style={{ height: virtualizer.getTotalSize() }}
|
|
108
143
|
>
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
key={event.seq}
|
|
115
|
-
>
|
|
116
|
-
<button
|
|
117
|
-
aria-expanded={selectedSeq === event.seq}
|
|
118
|
-
aria-label={`Event ${event.seq}: ${event.data.type}`}
|
|
144
|
+
{virtualRows.map((virtualRow) => {
|
|
145
|
+
const { event, summary, metadata } = visibleRows[virtualRow.index]!;
|
|
146
|
+
const index = virtualRow.index;
|
|
147
|
+
return (
|
|
148
|
+
<li
|
|
119
149
|
className={cn(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
selectedSeq === event.seq &&
|
|
123
|
-
"bg-dashboard-fill-strong before:absolute before:inset-y-0 before:left-0 before:w-0.5 before:bg-dashboard-accent before:content-['']",
|
|
150
|
+
"absolute left-0 top-0 w-full scroll-mt-32 scroll-mb-2",
|
|
151
|
+
index > 0 && "border-t border-dashboard-border-subtle",
|
|
124
152
|
)}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
153
|
+
data-event-seq={event.seq}
|
|
154
|
+
data-index={index}
|
|
155
|
+
data-scroll-anchor={`event:${event.seq}`}
|
|
156
|
+
key={virtualRow.key}
|
|
157
|
+
onBlur={() => {
|
|
158
|
+
if (focusedSeqRef.current === event.seq) {
|
|
159
|
+
focusedSeqRef.current = undefined;
|
|
160
|
+
}
|
|
128
161
|
}}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
(keyEvent.key !== "ArrowUp" && keyEvent.key !== "ArrowDown")
|
|
136
|
-
)
|
|
137
|
-
return;
|
|
138
|
-
keyEvent.preventDefault();
|
|
139
|
-
const next =
|
|
140
|
-
visibleRows[
|
|
141
|
-
index + (keyEvent.key === "ArrowDown" ? 1 : -1)
|
|
142
|
-
];
|
|
143
|
-
if (!next) return;
|
|
144
|
-
setSelectedSeq(next.event.seq);
|
|
145
|
-
listRef.current
|
|
146
|
-
?.querySelector<HTMLButtonElement>(
|
|
147
|
-
`[data-event-seq="${next.event.seq}"] button`,
|
|
148
|
-
)
|
|
149
|
-
?.focus({ preventScroll: true });
|
|
162
|
+
onFocus={() => {
|
|
163
|
+
focusedSeqRef.current = event.seq;
|
|
164
|
+
}}
|
|
165
|
+
ref={virtualizer.measureElement}
|
|
166
|
+
style={{
|
|
167
|
+
transform: `translateY(${virtualRow.start - virtualizer.options.scrollMargin}px)`,
|
|
150
168
|
}}
|
|
151
|
-
type="button"
|
|
152
169
|
>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
<button
|
|
171
|
+
aria-expanded={selectedSeq === event.seq}
|
|
172
|
+
aria-label={`Event ${event.seq}: ${event.data.type}`}
|
|
173
|
+
className={cn(
|
|
174
|
+
rowClass,
|
|
175
|
+
"relative min-h-11 w-full cursor-pointer border-0 bg-transparent py-2 text-left leading-5 hover:bg-dashboard-fill-hover active:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus @min-[32rem]:min-h-9 @min-[32rem]:py-1.5",
|
|
176
|
+
selectedSeq === event.seq &&
|
|
177
|
+
"bg-dashboard-fill-strong before:absolute before:inset-y-0 before:left-0 before:w-0.5 before:bg-dashboard-accent before:content-['']",
|
|
178
|
+
)}
|
|
179
|
+
onClick={() => {
|
|
180
|
+
revealedSeqRef.current = event.seq;
|
|
181
|
+
setSelectedSeq(event.seq);
|
|
182
|
+
}}
|
|
183
|
+
onKeyDown={(keyEvent) => {
|
|
184
|
+
if (
|
|
185
|
+
keyEvent.altKey ||
|
|
186
|
+
keyEvent.ctrlKey ||
|
|
187
|
+
keyEvent.metaKey ||
|
|
188
|
+
keyEvent.shiftKey ||
|
|
189
|
+
(keyEvent.key !== "ArrowUp" &&
|
|
190
|
+
keyEvent.key !== "ArrowDown")
|
|
191
|
+
)
|
|
192
|
+
return;
|
|
193
|
+
keyEvent.preventDefault();
|
|
194
|
+
const next =
|
|
195
|
+
visibleRows[
|
|
196
|
+
index + (keyEvent.key === "ArrowDown" ? 1 : -1)
|
|
197
|
+
];
|
|
198
|
+
if (!next) return;
|
|
199
|
+
setSelectedSeq(next.event.seq);
|
|
200
|
+
// Focus a mounted row now, so the next key press reaches
|
|
201
|
+
// it. A row past the mounted range gets focus after it
|
|
202
|
+
// mounts.
|
|
203
|
+
const nextButton =
|
|
204
|
+
listRef.current?.querySelector<HTMLButtonElement>(
|
|
205
|
+
`[data-event-seq="${next.event.seq}"] button`,
|
|
206
|
+
);
|
|
207
|
+
if (nextButton) nextButton.focus({ preventScroll: true });
|
|
208
|
+
else focusSeqRef.current = next.event.seq;
|
|
209
|
+
}}
|
|
210
|
+
type="button"
|
|
163
211
|
>
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
<time
|
|
213
|
+
className="tabular-nums text-dashboard-text-muted"
|
|
214
|
+
dateTime={event.createdAt}
|
|
215
|
+
title={time.formatTimestamp(event.createdAt)}
|
|
216
|
+
>
|
|
217
|
+
{time.formatClock(event.createdAt)}
|
|
218
|
+
</time>
|
|
219
|
+
<span
|
|
220
|
+
className={cn("truncate", eventLogTone(event.data))}
|
|
221
|
+
title={event.data.type}
|
|
222
|
+
>
|
|
223
|
+
<HighlightText text={event.data.type} />
|
|
169
224
|
</span>
|
|
170
|
-
|
|
171
|
-
<span
|
|
172
|
-
|
|
173
|
-
title={metadata}
|
|
174
|
-
>
|
|
175
|
-
<HighlightText text={metadata} />
|
|
225
|
+
<span className="col-span-2 row-start-2 grid min-w-0 gap-0.5 text-dashboard-text @min-[32rem]:col-span-1 @min-[32rem]:row-auto">
|
|
226
|
+
<span className="truncate" title={summary}>
|
|
227
|
+
<HighlightText text={summary} />
|
|
176
228
|
</span>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
229
|
+
{metadata ? (
|
|
230
|
+
<span
|
|
231
|
+
className="truncate text-dashboard-text-muted"
|
|
232
|
+
title={metadata}
|
|
233
|
+
>
|
|
234
|
+
<HighlightText text={metadata} />
|
|
235
|
+
</span>
|
|
236
|
+
) : null}
|
|
237
|
+
</span>
|
|
238
|
+
</button>
|
|
239
|
+
</li>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
182
242
|
</ol>
|
|
183
243
|
{visibleRows.length === 0 ? (
|
|
184
244
|
<p className="m-0 px-3 py-6 text-sm text-dashboard-text-muted">
|
|
@@ -205,6 +265,11 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
205
265
|
);
|
|
206
266
|
});
|
|
207
267
|
|
|
268
|
+
/** Read the event seq from a `data-scroll-anchor` key. */
|
|
269
|
+
function anchorSeq(key: string | undefined): number | undefined {
|
|
270
|
+
return key?.startsWith("event:") ? Number(key.slice(6)) : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
208
273
|
const loadingRowWidths = ["w-3/5", "w-2/5", "w-4/5", "w-1/2", "w-2/3", "w-1/3"];
|
|
209
274
|
|
|
210
275
|
/** Match the event log toolbar, columns, and rows while the transcript loads. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import { Menu, PanelRight
|
|
2
|
+
import { Menu, PanelRight } 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
|
-
*
|
|
25
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,53 @@ 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-
|
|
170
|
-
<div className="flex min-w-0 items-center gap-
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
{props.view === "raw" ? (
|
|
175
|
+
// Event log is a debug view reached from the `⋯` menu. The
|
|
176
|
+
// title leads back to the Conversation.
|
|
177
|
+
<>
|
|
178
|
+
<h2 className="m-0 min-w-0 font-display text-base font-medium leading-6 tracking-tight">
|
|
179
|
+
<button
|
|
180
|
+
className="block max-w-full cursor-pointer truncate text-left text-dashboard-text-muted hover:text-dashboard-text hover:underline"
|
|
181
|
+
onClick={() => props.onViewChange("rich")}
|
|
182
|
+
title="Back to conversation"
|
|
183
|
+
type="button"
|
|
184
|
+
>
|
|
185
|
+
{props.title}
|
|
186
|
+
</button>
|
|
187
|
+
</h2>
|
|
188
|
+
<span aria-hidden="true" className="text-dashboard-text-faint">
|
|
189
|
+
›
|
|
190
|
+
</span>
|
|
191
|
+
<span
|
|
192
|
+
aria-current="page"
|
|
193
|
+
className="shrink-0 font-display text-base font-medium leading-6 tracking-tight"
|
|
194
|
+
>
|
|
195
|
+
Event log
|
|
196
|
+
</span>
|
|
197
|
+
</>
|
|
198
|
+
) : (
|
|
199
|
+
<h2 className="m-0 min-w-0 truncate font-display text-base font-medium leading-6 tracking-tight">
|
|
200
|
+
{props.title}
|
|
201
|
+
</h2>
|
|
202
|
+
)}
|
|
203
|
+
{liveIndicator}
|
|
204
|
+
{props.privacy ? (
|
|
205
|
+
<span className="shrink-0">{props.privacy}</span>
|
|
180
206
|
) : null}
|
|
181
|
-
{props.agentTree
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
</
|
|
207
|
+
{props.agentTree ? (
|
|
208
|
+
<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">
|
|
209
|
+
{props.agentTree}
|
|
210
|
+
</div>
|
|
211
|
+
) : props.contextTags ? (
|
|
212
|
+
<div className="flex h-6 min-w-0 max-w-[min(19rem,60%)] shrink-[2] items-center">
|
|
213
|
+
{props.contextTags}
|
|
214
|
+
</div>
|
|
185
215
|
) : null}
|
|
186
|
-
<h2 className="m-0 line-clamp-1 min-w-0 font-display text-base font-medium leading-tight tracking-tight">
|
|
187
|
-
{props.title}
|
|
188
|
-
</h2>
|
|
189
|
-
{liveIndicator}
|
|
190
|
-
<span className="shrink-0">{props.privacy}</span>
|
|
191
216
|
</div>
|
|
192
217
|
<div className="flex shrink-0 items-center gap-0.5">
|
|
193
218
|
{props.cost ? (
|
|
@@ -195,41 +220,25 @@ export function ConversationHeader(props: {
|
|
|
195
220
|
{props.cost}
|
|
196
221
|
</span>
|
|
197
222
|
) : null}
|
|
198
|
-
{props.view === "raw" ? (
|
|
199
|
-
// Event log is a debug view reached from the menu. Say which
|
|
200
|
-
// view this is, and give one click back.
|
|
201
|
-
<Button
|
|
202
|
-
aria-label="Close event log"
|
|
203
|
-
className="mr-1 h-8 px-2.5"
|
|
204
|
-
onClick={() => props.onViewChange("rich")}
|
|
205
|
-
variant="quiet"
|
|
206
|
-
>
|
|
207
|
-
<ScrollText aria-hidden="true" size={14} strokeWidth={2} />
|
|
208
|
-
Event log
|
|
209
|
-
<X aria-hidden="true" size={14} strokeWidth={2} />
|
|
210
|
-
</Button>
|
|
211
|
-
) : (
|
|
223
|
+
{props.view === "raw" ? null : (
|
|
212
224
|
<TranscriptSearchToggle
|
|
213
225
|
onClick={toggleSearch}
|
|
214
226
|
open={searchOpenVisible}
|
|
215
227
|
/>
|
|
216
228
|
)}
|
|
217
|
-
<ShareConversationButton
|
|
218
|
-
key={props.conversationId}
|
|
219
|
-
conversationId={props.conversationId}
|
|
220
|
-
layout="icon"
|
|
221
|
-
/>
|
|
222
229
|
<ConversationOverflowMenu>
|
|
223
230
|
{(close) => menuActions("desktop", close)}
|
|
224
231
|
</ConversationOverflowMenu>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
{detailsOpen ? null : (
|
|
233
|
+
<HeaderIconButton
|
|
234
|
+
ref={detailsButtonRef}
|
|
235
|
+
expanded={false}
|
|
236
|
+
label="Conversation details"
|
|
237
|
+
onClick={props.onDetailsClick}
|
|
238
|
+
>
|
|
239
|
+
<PanelRight aria-hidden="true" size={15} strokeWidth={2} />
|
|
240
|
+
</HeaderIconButton>
|
|
241
|
+
)}
|
|
233
242
|
</div>
|
|
234
243
|
</div>
|
|
235
244
|
{props.forks ? (
|
|
@@ -241,12 +250,12 @@ export function ConversationHeader(props: {
|
|
|
241
250
|
{props.forks ? (
|
|
242
251
|
<div className="px-4 py-2 md:hidden">{props.forks}</div>
|
|
243
252
|
) : null}
|
|
244
|
-
{props.linkedWork ? (
|
|
245
|
-
<div
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
{props.contextTags || props.linkedWork ? (
|
|
254
|
+
<div className="grid min-w-0 gap-2 px-3 pb-2 pt-1.5 md:hidden">
|
|
255
|
+
{props.contextTags}
|
|
256
|
+
{props.linkedWork ? (
|
|
257
|
+
<div aria-label="Linked work">{props.linkedWork}</div>
|
|
258
|
+
) : null}
|
|
250
259
|
</div>
|
|
251
260
|
) : null}
|
|
252
261
|
{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
|
-
*
|
|
42
|
-
*
|
|
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 &&
|
|
63
|
+
{mobile && !eventLogOpen ? (
|
|
62
64
|
<MenuActionButton
|
|
63
65
|
label={props.searchOpen ? "Hide search" : "Search transcript"}
|
|
64
66
|
onClick={props.onSearchClick}
|
|
@@ -71,40 +73,32 @@ 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
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
{/* The copy action is a sibling keyed by the same id. Unique keys keep
|
|
86
|
+
React from duplicating menu rows on re-render. */}
|
|
87
|
+
<ShareConversationButton
|
|
88
|
+
key={`share:${props.conversationId}`}
|
|
89
|
+
conversationId={props.conversationId}
|
|
90
|
+
/>
|
|
95
91
|
{props.copyAction}
|
|
96
92
|
<MenuActionButton
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onClick={props.pin.onClick}
|
|
93
|
+
label={eventLogOpen ? "Back to conversation" : "Open event log"}
|
|
94
|
+
onClick={() => props.onViewChange(eventLogOpen ? "rich" : "raw")}
|
|
100
95
|
>
|
|
101
|
-
{
|
|
102
|
-
<
|
|
96
|
+
{eventLogOpen ? (
|
|
97
|
+
<MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
|
|
103
98
|
) : (
|
|
104
|
-
<
|
|
99
|
+
<ScrollText aria-hidden="true" size={16} strokeWidth={2} />
|
|
105
100
|
)}
|
|
106
101
|
</MenuActionButton>
|
|
107
|
-
<ArchiveConversationButton {...props.archive} />
|
|
108
102
|
{props.sentryConversationUrl ? (
|
|
109
103
|
<a
|
|
110
104
|
className={buttonVariants({ size: "menu" })}
|
|
@@ -116,15 +110,19 @@ export function ConversationHeaderActions(props: {
|
|
|
116
110
|
Open in Sentry
|
|
117
111
|
</a>
|
|
118
112
|
) : null}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
113
|
+
<Separator className="my-1 bg-dashboard-border-subtle" />
|
|
114
|
+
<MenuActionButton
|
|
115
|
+
disabled={props.pin.disabled}
|
|
116
|
+
label={props.pin.pinned ? "Unpin" : "Pin"}
|
|
117
|
+
onClick={props.pin.onClick}
|
|
118
|
+
>
|
|
119
|
+
{props.pin.pinned ? (
|
|
120
|
+
<PinOff aria-hidden="true" size={16} strokeWidth={2} />
|
|
121
|
+
) : (
|
|
122
|
+
<Pin aria-hidden="true" size={16} strokeWidth={2} />
|
|
123
|
+
)}
|
|
124
|
+
</MenuActionButton>
|
|
125
|
+
<ArchiveConversationButton {...props.archive} />
|
|
128
126
|
</div>
|
|
129
127
|
);
|
|
130
128
|
}
|
|
@@ -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 {
|
|
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 (
|