@sentry/junior-dashboard 0.280.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 +23 -30
- package/dist/client-manifest.json +1 -1
- package/dist/index.js +23 -30
- package/dist/public/chunks/{AdminPage-YEKZOKH6.js → AdminPage-M5RBVZFN.js} +1 -1
- package/dist/public/chunks/{AutomationPage-5GS3IUAK.js → AutomationPage-WALCREDT.js} +1 -1
- package/dist/public/chunks/{AutomationRunsPage-IPYEZU4L.js → AutomationRunsPage-CV47E4UX.js} +1 -1
- package/dist/public/chunks/{AutomationsPage-HSAYPC3C.js → AutomationsPage-57L3AQVJ.js} +1 -1
- package/dist/public/chunks/{CodePage-IPFKXWFB.js → CodePage-XJUYMFJH.js} +1 -1
- package/dist/public/chunks/{ConnectionsPage-5XUEK6FS.js → ConnectionsPage-3QQAGYWF.js} +1 -1
- package/dist/public/chunks/{LocationDetailPage-27ETUGR6.js → LocationDetailPage-4IL5HQML.js} +1 -1
- package/dist/public/chunks/{LocationsPage-WOKMQAGM.js → LocationsPage-D2DUQAHJ.js} +1 -1
- package/dist/public/chunks/{PeoplePage-6YQLDOUL.js → PeoplePage-6F5GPQ7C.js} +1 -1
- package/dist/public/chunks/{PersonProfilePage-GYMAZFIK.js → PersonProfilePage-SPAYQPTD.js} +1 -1
- package/dist/public/chunks/{PersonalTokensPage-4BCR744B.js → PersonalTokensPage-SHAUJ74M.js} +1 -1
- package/dist/public/chunks/{PluginUserPage-IP6WL7MD.js → PluginUserPage-64FIWD7M.js} +1 -1
- package/dist/public/chunks/{RepositoryPage-TRD44LQ3.js → RepositoryPage-L7IHOXSN.js} +1 -1
- package/dist/public/chunks/{SettingsPage-4E3SLQ36.js → SettingsPage-U7WG4FHE.js} +1 -1
- package/dist/public/chunks/{SystemPage-C3XBKXKW.js → SystemPage-ZBC43OJH.js} +1 -1
- package/dist/public/chunks/{WorkspaceFormPage-2LS2X67Q.js → WorkspaceFormPage-3NVIIZ4P.js} +1 -1
- package/dist/public/chunks/{WorkspacesPage-5WB3CUTX.js → WorkspacesPage-G73HPBDD.js} +1 -1
- package/dist/public/chunks/{chunk-VFEBNOPX.js → chunk-2J3LGF5Q.js} +1 -1
- package/dist/public/chunks/chunk-2NNKHSZE.js +1 -0
- package/dist/public/chunks/chunk-4TQSTCIN.js +13 -0
- package/dist/public/chunks/chunk-64GPEGGE.js +1 -0
- package/dist/public/chunks/{chunk-WZVO3HG4.js → chunk-6FHY65EQ.js} +1 -1
- package/dist/public/chunks/{chunk-YYNMZ4WC.js → chunk-DEEO4D4N.js} +1 -1
- package/dist/public/chunks/{chunk-SDKV3OID.js → chunk-FSGVY7HB.js} +1 -1
- package/dist/public/chunks/chunk-GZBMHYYU.js +1 -0
- package/dist/public/chunks/{chunk-NQMDED2R.js → chunk-II4NXLSQ.js} +1 -1
- package/dist/public/chunks/{chunk-B6HGRBBX.js → chunk-ILH47V7O.js} +1 -1
- package/dist/public/chunks/{chunk-GRMX2HX6.js → chunk-JPL2NRWK.js} +1 -1
- package/dist/public/chunks/{chunk-NQOJAZFQ.js → chunk-JVJ5UWIK.js} +1 -1
- package/dist/public/chunks/{chunk-V5UDSIRA.js → chunk-K4TYMAA3.js} +1 -1
- package/dist/public/chunks/{chunk-EFEREKX4.js → chunk-KDUTK357.js} +1 -1
- package/dist/public/chunks/{chunk-RHIXHBS5.js → chunk-LZYAHJVQ.js} +1 -1
- package/dist/public/chunks/{chunk-5GZDBGAR.js → chunk-MKKFXRYO.js} +1 -1
- package/dist/public/chunks/{chunk-PEQNT5MO.js → chunk-NGQRNS3C.js} +1 -1
- package/dist/public/chunks/{chunk-IA6BJDKN.js → chunk-ONDKE72P.js} +1 -1
- package/dist/public/chunks/{chunk-WJ3OXMVX.js → chunk-PG7M7L24.js} +1 -1
- package/dist/public/chunks/{chunk-6GQL3EVJ.js → chunk-QQUFROWT.js} +1 -1
- package/dist/public/chunks/{chunk-UAH6DWNS.js → chunk-RB5FBDEY.js} +1 -1
- package/dist/public/chunks/{chunk-A2QWWA7O.js → chunk-RGXXD4DM.js} +1 -1
- package/dist/public/chunks/{chunk-AYXSBMWA.js → chunk-TC7TU4GK.js} +1 -1
- package/dist/public/chunks/chunk-WLIOZO3W.js +1 -0
- package/dist/public/chunks/{chunk-VXT3FIVL.js → chunk-XDUSY26N.js} +11 -11
- package/dist/public/client-4OAAOHF4.js +68 -0
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/client/components/BottomSheet.tsx +1 -1
- package/src/client/components/DetailsPanel.tsx +2 -1
- package/src/client/conversations/ConversationBrief.tsx +129 -65
- package/src/client/conversations/ConversationDetails.tsx +13 -14
- package/src/client/conversations/ConversationEventLog.tsx +20 -97
- package/src/client/conversations/ConversationHeader.tsx +41 -26
- package/src/client/conversations/ConversationHeaderActions.tsx +3 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +30 -10
- package/src/client/conversations/ConversationMeta.tsx +15 -15
- package/src/client/conversations/ConversationPage.tsx +31 -7
- 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/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 +37 -5
- package/src/client/conversations/TranscriptView.tsx +4 -0
- package/src/client/conversations/queries.ts +74 -43
- package/src/client/conversations/transcriptBottomPinning.ts +13 -5
- package/src/client/conversations/transcriptListRows.ts +215 -0
- package/src/client/conversations/virtualRows.ts +168 -0
- package/src/client/http.ts +6 -5
- package/src/client/sentry.ts +53 -3
- package/src/conversation-cache.ts +8 -10
- package/src/mock-reporting/fixtures.ts +0 -8
- package/dist/public/chunks/chunk-GO45RQDL.js +0 -1
- package/dist/public/chunks/chunk-N4AONKD3.js +0 -1
- package/dist/public/chunks/chunk-QNA7F7QB.js +0 -1
- package/dist/public/chunks/chunk-R4ORRE77.js +0 -1
- package/dist/public/chunks/chunk-Z2JPRGFT.js +0 -1
- package/dist/public/client-TYC7JKRE.js +0 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.281.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
43
43
|
"shiki": "4.1.0",
|
|
44
44
|
"zod": "^4.5.4",
|
|
45
|
-
"@sentry/junior-plugin-api": "0.
|
|
45
|
+
"@sentry/junior-plugin-api": "0.281.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@sentry/junior": "0.
|
|
48
|
+
"@sentry/junior": "0.281.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"tw-animate-css": "^1.4.0",
|
|
67
67
|
"typescript": "^7.0.2",
|
|
68
68
|
"vitest": "^4.1.12",
|
|
69
|
-
"@sentry/junior": "0.
|
|
69
|
+
"@sentry/junior": "0.281.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
|
|
@@ -31,7 +31,7 @@ export function BottomSheet(props: {
|
|
|
31
31
|
{props.title ?? props.label}
|
|
32
32
|
</SheetTitle>
|
|
33
33
|
</SheetHeader>
|
|
34
|
-
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
34
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
|
|
35
35
|
{props.children}
|
|
36
36
|
</div>
|
|
37
37
|
</SheetContent>
|
|
@@ -117,6 +117,7 @@ export function DetailsPanel(props: {
|
|
|
117
117
|
<Button
|
|
118
118
|
ref={closeRef}
|
|
119
119
|
aria-label={closeLabel}
|
|
120
|
+
data-slot="details-panel-close"
|
|
120
121
|
onClick={onClose}
|
|
121
122
|
size="icon"
|
|
122
123
|
>
|
|
@@ -124,7 +125,7 @@ export function DetailsPanel(props: {
|
|
|
124
125
|
</Button>
|
|
125
126
|
</div>
|
|
126
127
|
</header>
|
|
127
|
-
<div className="thin-scrollbar min-h-0 overflow-y-auto overscroll-contain">
|
|
128
|
+
<div className="thin-scrollbar min-h-0 overflow-y-auto overscroll-contain [scrollbar-gutter:stable]">
|
|
128
129
|
{props.children}
|
|
129
130
|
</div>
|
|
130
131
|
</aside>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import
|
|
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
|
-
<
|
|
51
|
+
<BriefSummary>
|
|
43
52
|
<DisplayText mentions={mentions} text={content.summary} />
|
|
44
|
-
|
|
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
|
|
81
|
+
{content.decisions.length ? (
|
|
84
82
|
<BriefSection title="Decisions">
|
|
85
|
-
<
|
|
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[];
|
|
@@ -37,9 +37,7 @@ import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
|
37
37
|
type Brief = NonNullable<ConversationDetailReport["brief"]>;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
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-
|
|
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">
|
|
209
|
+
<DetailsCard flat title="Participants">
|
|
210
|
+
{props.identity}
|
|
211
|
+
</DetailsCard>
|
|
213
212
|
) : null}
|
|
214
213
|
{memoryList?.events.length ? (
|
|
215
214
|
<ConversationMemoryCard
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defaultRangeExtractor,
|
|
3
|
-
useVirtualizer,
|
|
4
|
-
type Range,
|
|
5
|
-
} from "@tanstack/react-virtual";
|
|
6
|
-
import {
|
|
7
|
-
memo,
|
|
8
|
-
useCallback,
|
|
9
|
-
useEffect,
|
|
10
|
-
useLayoutEffect,
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
} from "react";
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
15
2
|
import { createPortal } from "react-dom";
|
|
16
3
|
|
|
17
4
|
import { Skeleton } from "../components/ui/skeleton";
|
|
@@ -25,11 +12,10 @@ import {
|
|
|
25
12
|
eventLogRows,
|
|
26
13
|
eventLogTone,
|
|
27
14
|
filterEventLogRows,
|
|
28
|
-
type EventLogRow,
|
|
29
15
|
} from "./eventLog";
|
|
30
16
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
31
|
-
import { scrollRootFor } from "./transcriptBottomPinning";
|
|
32
17
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
18
|
+
import { useVirtualRows } from "./virtualRows";
|
|
33
19
|
|
|
34
20
|
const rowClass =
|
|
35
21
|
"grid grid-cols-[12ch_minmax(0,1fr)] items-baseline gap-x-3 px-4 @min-[32rem]:grid-cols-[12ch_17ch_minmax(0,1fr)]";
|
|
@@ -70,14 +56,25 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
70
56
|
// An arrow key can select a row that mounts only on the next render.
|
|
71
57
|
const focusSeqRef = useRef<number>(undefined);
|
|
72
58
|
const focusedSeqRef = useRef<number>(undefined);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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?.())),
|
|
80
75
|
],
|
|
76
|
+
listRef,
|
|
77
|
+
sizeClass: (index) => (visibleRows[index]?.metadata ? "metadata" : "plain"),
|
|
81
78
|
});
|
|
82
79
|
const virtualRows = virtualizer.getVirtualItems();
|
|
83
80
|
useEffect(() => {
|
|
@@ -268,85 +265,11 @@ export const ConversationEventLog = memo(function ConversationEventLog(props: {
|
|
|
268
265
|
);
|
|
269
266
|
});
|
|
270
267
|
|
|
271
|
-
/** Rows mounted beyond the viewport on each side. */
|
|
272
|
-
const OVERSCAN_ROWS = 10;
|
|
273
|
-
|
|
274
268
|
/** Read the event seq from a `data-scroll-anchor` key. */
|
|
275
269
|
function anchorSeq(key: string | undefined): number | undefined {
|
|
276
270
|
return key?.startsWith("event:") ? Number(key.slice(6)) : undefined;
|
|
277
271
|
}
|
|
278
272
|
|
|
279
|
-
/**
|
|
280
|
-
* Pick and measure the rows near the viewport. The scroll owner holds the
|
|
281
|
-
* reader's place, so this never moves the scroll position. `keptSeqs` rows
|
|
282
|
-
* stay mounted outside the viewport: the open entry, the focused row, and
|
|
283
|
-
* the row that holds the reader's place. Code that finds them in the DOM
|
|
284
|
-
* then keeps working.
|
|
285
|
-
*/
|
|
286
|
-
function useEventLogVirtualizer(input: {
|
|
287
|
-
keptSeqs: () => (number | undefined)[];
|
|
288
|
-
listRef: React.RefObject<HTMLOListElement | null>;
|
|
289
|
-
rows: EventLogRow[];
|
|
290
|
-
}) {
|
|
291
|
-
const { keptSeqs, listRef, rows } = input;
|
|
292
|
-
const [scrollMargin, setScrollMargin] = useState(0);
|
|
293
|
-
const indexBySeq = useMemo(
|
|
294
|
-
() => new Map(rows.map((row, index) => [row.event.seq, index])),
|
|
295
|
-
[rows],
|
|
296
|
-
);
|
|
297
|
-
const kept = keptSeqs();
|
|
298
|
-
const keptKey = kept.join(",");
|
|
299
|
-
const rangeExtractor = useCallback(
|
|
300
|
-
(range: Range) => {
|
|
301
|
-
const indexes = new Set(defaultRangeExtractor(range));
|
|
302
|
-
for (const seq of keptKey.split(",")) {
|
|
303
|
-
const index = seq ? indexBySeq.get(Number(seq)) : undefined;
|
|
304
|
-
if (index !== undefined) indexes.add(index);
|
|
305
|
-
}
|
|
306
|
-
// Rows stay in document order, so the scroll owner can search them.
|
|
307
|
-
return [...indexes].sort((left, right) => left - right);
|
|
308
|
-
},
|
|
309
|
-
[indexBySeq, keptKey],
|
|
310
|
-
);
|
|
311
|
-
// The toolbar and earlier-page control sit above the rows, and the
|
|
312
|
-
// virtualizer counts offsets from the top of the scroll content. The
|
|
313
|
-
// margin only picks which rows mount, so a reading from the last scroll
|
|
314
|
-
// or resize is close enough: the overscan rows cover a small error.
|
|
315
|
-
const measureScrollMargin = useCallback(() => {
|
|
316
|
-
const list = listRef.current;
|
|
317
|
-
const root = scrollRootFor(list);
|
|
318
|
-
if (!list || !(root instanceof Element)) return;
|
|
319
|
-
const margin =
|
|
320
|
-
list.getBoundingClientRect().top -
|
|
321
|
-
root.getBoundingClientRect().top +
|
|
322
|
-
root.scrollTop;
|
|
323
|
-
setScrollMargin((current) =>
|
|
324
|
-
Math.abs(margin - current) >= 1 ? margin : current,
|
|
325
|
-
);
|
|
326
|
-
}, [listRef]);
|
|
327
|
-
useLayoutEffect(measureScrollMargin, [measureScrollMargin]);
|
|
328
|
-
// The dashboard build does not use React Compiler, so nothing memoizes
|
|
329
|
-
// the values that this hook returns.
|
|
330
|
-
// oxlint-disable-next-line react/incompatible-library
|
|
331
|
-
const virtualizer = useVirtualizer<Element, HTMLLIElement>({
|
|
332
|
-
count: rows.length,
|
|
333
|
-
estimateSize: (index) => (rows[index]?.metadata ? 56 : 40),
|
|
334
|
-
getItemKey: (index) => rows[index]?.event.seq ?? index,
|
|
335
|
-
getScrollElement: () => {
|
|
336
|
-
const root = scrollRootFor(listRef.current);
|
|
337
|
-
return root instanceof Element ? root : null;
|
|
338
|
-
},
|
|
339
|
-
onChange: measureScrollMargin,
|
|
340
|
-
overscan: OVERSCAN_ROWS,
|
|
341
|
-
rangeExtractor,
|
|
342
|
-
scrollMargin,
|
|
343
|
-
// The scroll owner moves the scroll position. This only reads it.
|
|
344
|
-
scrollToFn: () => {},
|
|
345
|
-
});
|
|
346
|
-
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false;
|
|
347
|
-
return virtualizer;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
273
|
const loadingRowWidths = ["w-3/5", "w-2/5", "w-4/5", "w-1/2", "w-2/3", "w-1/3"];
|
|
351
274
|
|
|
352
275
|
/** Match the event log toolbar, columns, and rows while the transcript loads. */
|
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -171,9 +171,35 @@ export function ConversationHeader(props: {
|
|
|
171
171
|
{props.sidebarToggle ? (
|
|
172
172
|
<div className="shrink-0">{props.sidebarToggle}</div>
|
|
173
173
|
) : null}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
)}
|
|
177
203
|
{liveIndicator}
|
|
178
204
|
{props.privacy ? (
|
|
179
205
|
<span className="shrink-0">{props.privacy}</span>
|
|
@@ -194,20 +220,7 @@ export function ConversationHeader(props: {
|
|
|
194
220
|
{props.cost}
|
|
195
221
|
</span>
|
|
196
222
|
) : null}
|
|
197
|
-
{props.view === "raw" ? (
|
|
198
|
-
// Event log is a debug view reached from the menu. Say which
|
|
199
|
-
// view this is, and give one click back.
|
|
200
|
-
<Button
|
|
201
|
-
aria-label="Close event log"
|
|
202
|
-
className="mr-1 h-8 px-2.5"
|
|
203
|
-
onClick={() => props.onViewChange("rich")}
|
|
204
|
-
variant="quiet"
|
|
205
|
-
>
|
|
206
|
-
<ScrollText aria-hidden="true" size={14} strokeWidth={2} />
|
|
207
|
-
Event log
|
|
208
|
-
<X aria-hidden="true" size={14} strokeWidth={2} />
|
|
209
|
-
</Button>
|
|
210
|
-
) : (
|
|
223
|
+
{props.view === "raw" ? null : (
|
|
211
224
|
<TranscriptSearchToggle
|
|
212
225
|
onClick={toggleSearch}
|
|
213
226
|
open={searchOpenVisible}
|
|
@@ -216,14 +229,16 @@ export function ConversationHeader(props: {
|
|
|
216
229
|
<ConversationOverflowMenu>
|
|
217
230
|
{(close) => menuActions("desktop", close)}
|
|
218
231
|
</ConversationOverflowMenu>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
)}
|
|
227
242
|
</div>
|
|
228
243
|
</div>
|
|
229
244
|
{props.forks ? (
|
|
@@ -82,8 +82,10 @@ export function ConversationHeaderActions(props: {
|
|
|
82
82
|
<Info aria-hidden="true" size={16} strokeWidth={2} />
|
|
83
83
|
</MenuActionButton>
|
|
84
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. */}
|
|
85
87
|
<ShareConversationButton
|
|
86
|
-
key={props.conversationId}
|
|
88
|
+
key={`share:${props.conversationId}`}
|
|
87
89
|
conversationId={props.conversationId}
|
|
88
90
|
/>
|
|
89
91
|
{props.copyAction}
|
|
@@ -7,6 +7,11 @@ import type {
|
|
|
7
7
|
|
|
8
8
|
import { ObjectIcon } from "../components/ObjectIcon";
|
|
9
9
|
import { Collapsible } from "../components/ui/collapsible";
|
|
10
|
+
import {
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "../components/ui/tooltip";
|
|
10
15
|
import { WatchDetails, WatchDetailsRow } from "./ConversationWatches";
|
|
11
16
|
import { DetailsList, DetailsRow, DetailsRowLine } from "./DetailsList";
|
|
12
17
|
|
|
@@ -88,7 +93,7 @@ export function conversationLinks(
|
|
|
88
93
|
* Show the outside work that a Conversation links to, one row each. A row
|
|
89
94
|
* that Junior also watches shows the Watch details under a chevron, so the
|
|
90
95
|
* same resource does not show twice. When one row has a chevron, the other
|
|
91
|
-
* rows keep the same space, so
|
|
96
|
+
* rows keep the same space, so all rows keep the same width.
|
|
92
97
|
*/
|
|
93
98
|
export function ConversationLinkedWork(props: {
|
|
94
99
|
links: ConversationLink[];
|
|
@@ -133,20 +138,35 @@ function ChevronSpace(props: { children: ReactNode }) {
|
|
|
133
138
|
function LinkRow(props: { link: ConversationLink }) {
|
|
134
139
|
const { link } = props;
|
|
135
140
|
const status = linkStatusLabel(link.status);
|
|
141
|
+
const icon = (
|
|
142
|
+
<ObjectIcon
|
|
143
|
+
decorative
|
|
144
|
+
facts={link.facts}
|
|
145
|
+
objectType={link.objectType}
|
|
146
|
+
size={14}
|
|
147
|
+
status={link.status}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
136
150
|
return (
|
|
137
151
|
<DetailsRow
|
|
138
152
|
href={link.url}
|
|
139
153
|
icon={
|
|
140
|
-
// The
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
// The icon shows the status, so the row does not repeat it in text.
|
|
155
|
+
status ? (
|
|
156
|
+
<Tooltip>
|
|
157
|
+
<TooltipTrigger
|
|
158
|
+
render={
|
|
159
|
+
<span aria-label={status} className="inline-flex" role="img">
|
|
160
|
+
{icon}
|
|
161
|
+
</span>
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
<TooltipContent size="compact">{status}</TooltipContent>
|
|
165
|
+
</Tooltip>
|
|
166
|
+
) : (
|
|
167
|
+
icon
|
|
168
|
+
)
|
|
148
169
|
}
|
|
149
|
-
meta={status}
|
|
150
170
|
title={link.label}
|
|
151
171
|
tooltip={[link.label, link.plugin, status, link.description]
|
|
152
172
|
.filter(Boolean)
|