@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
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
ConversationModelUsage,
|
|
4
|
-
} from "@sentry/junior/api/schema";
|
|
1
|
+
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
5
3
|
import type { ReactNode } from "react";
|
|
6
4
|
|
|
7
5
|
import {
|
|
8
6
|
formatCompactNumber,
|
|
7
|
+
formatCostSummary,
|
|
9
8
|
summarizeCost,
|
|
10
9
|
summarizeModelCost,
|
|
11
10
|
summarizeModelUsage,
|
|
12
11
|
summarizeUsage,
|
|
13
12
|
totalConversationCost,
|
|
13
|
+
type CostUsageSummary,
|
|
14
|
+
type TokenUsageSummary,
|
|
14
15
|
} from "../format";
|
|
15
16
|
import { ShimmerText } from "../components/ShimmerText";
|
|
17
|
+
import {
|
|
18
|
+
Collapsible,
|
|
19
|
+
CollapsibleContent,
|
|
20
|
+
CollapsibleTrigger,
|
|
21
|
+
} from "../components/ui/collapsible";
|
|
16
22
|
import {
|
|
17
23
|
Table,
|
|
18
24
|
TableBody,
|
|
@@ -22,7 +28,7 @@ import {
|
|
|
22
28
|
TableHeader,
|
|
23
29
|
TableRow,
|
|
24
30
|
} from "../components/ui/table";
|
|
25
|
-
import { cn } from "../styles";
|
|
31
|
+
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
26
32
|
import type { Conversation } from "../types";
|
|
27
33
|
import { liveModelId } from "./ConversationMeta";
|
|
28
34
|
import {
|
|
@@ -31,7 +37,6 @@ import {
|
|
|
31
37
|
modelLabel,
|
|
32
38
|
subagentShare,
|
|
33
39
|
subagentUsage,
|
|
34
|
-
tokenTypeRows,
|
|
35
40
|
} from "./TelemetryMetrics";
|
|
36
41
|
|
|
37
42
|
/** One token type of a model, shown as an indented row under the model. */
|
|
@@ -52,31 +57,33 @@ type BreakdownRow = {
|
|
|
52
57
|
const NOT_RECORDED = "—";
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* output tokens, so they have no cost of their own.
|
|
60
|
+
* Show token types with a recorded count or cost. Reasoning tokens are part
|
|
61
|
+
* of output tokens and have no cost of their own.
|
|
58
62
|
*/
|
|
59
63
|
function tokenTypeParts(
|
|
60
|
-
|
|
64
|
+
tokens: TokenUsageSummary | undefined,
|
|
65
|
+
costs: CostUsageSummary | undefined,
|
|
61
66
|
formatCost: (value: number) => string,
|
|
62
67
|
): BreakdownPart[] | undefined {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
const types = [
|
|
69
|
+
["input", tokens?.inputTokens, costs?.input],
|
|
70
|
+
["output", tokens?.outputTokens, costs?.output],
|
|
71
|
+
["cached", tokens?.cachedInputTokens, costs?.cacheRead],
|
|
72
|
+
["cache write", tokens?.cacheCreationTokens, costs?.cacheWrite],
|
|
73
|
+
["reasoning (in output)", tokens?.reasoningTokens, undefined],
|
|
74
|
+
] as const;
|
|
75
|
+
const parts = types.flatMap(([label, count, cost]) =>
|
|
76
|
+
count || cost !== undefined
|
|
77
|
+
? [
|
|
78
|
+
{
|
|
79
|
+
cost: cost === undefined ? undefined : formatCost(cost),
|
|
80
|
+
count:
|
|
81
|
+
count === undefined ? NOT_RECORDED : formatCompactNumber(count),
|
|
82
|
+
label,
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
: [],
|
|
86
|
+
);
|
|
80
87
|
return parts.length ? parts : undefined;
|
|
81
88
|
}
|
|
82
89
|
|
|
@@ -224,10 +231,8 @@ export function hasConversationUsageBreakdown(props: {
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
/**
|
|
227
|
-
*
|
|
228
|
-
* operation.
|
|
229
|
-
* every one with its token types. Render nothing without model or auxiliary
|
|
230
|
-
* usage.
|
|
234
|
+
* Summarize usage and let the reader inspect the breakdown by agent, model,
|
|
235
|
+
* and auxiliary operation.
|
|
231
236
|
*/
|
|
232
237
|
export function ConversationUsageBreakdown(props: {
|
|
233
238
|
conversation: Conversation | undefined;
|
|
@@ -243,11 +248,16 @@ export function ConversationUsageBreakdown(props: {
|
|
|
243
248
|
props.detail?.cumulativeUsage ?? props.conversation.cumulativeUsage;
|
|
244
249
|
const childAgentTotals =
|
|
245
250
|
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const modelCost = summarizeModelCost(modelUsage)
|
|
250
|
-
const
|
|
251
|
+
const modelTokens = summarizeModelUsage(modelUsage);
|
|
252
|
+
const savedTokens = summarizeUsage(usage);
|
|
253
|
+
const tokens = (modelTokens ?? savedTokens)?.totalTokens;
|
|
254
|
+
const modelCost = summarizeModelCost(modelUsage);
|
|
255
|
+
const savedCost = summarizeCost(usage);
|
|
256
|
+
const totalCost = totalConversationCost(
|
|
257
|
+
modelCost ?? savedCost,
|
|
258
|
+
auxiliaryCosts,
|
|
259
|
+
);
|
|
260
|
+
const cost = totalCost?.total;
|
|
251
261
|
const operations = [...(auxiliaryCosts?.operations ?? [])].sort(
|
|
252
262
|
(a, b) => b.costUsd - a.costUsd,
|
|
253
263
|
);
|
|
@@ -257,17 +267,38 @@ export function ConversationUsageBreakdown(props: {
|
|
|
257
267
|
const formatMaybeCost = (value: number | undefined) =>
|
|
258
268
|
value === undefined ? NOT_RECORDED : formatCost(value);
|
|
259
269
|
const activeModelId = liveModelId(props.detail);
|
|
270
|
+
const live = (props.detail?.status ?? props.conversation.status) === "active";
|
|
271
|
+
const aggregateTokenTypes = tokenTypeParts(
|
|
272
|
+
modelTokens ? undefined : savedTokens,
|
|
273
|
+
modelCost ? undefined : savedCost,
|
|
274
|
+
formatCost,
|
|
275
|
+
);
|
|
276
|
+
const compactions = props.detail?.previousCursor
|
|
277
|
+
? undefined
|
|
278
|
+
: props.detail?.events.filter((event) => event.data.type === "compaction")
|
|
279
|
+
.length;
|
|
260
280
|
|
|
261
281
|
const models = modelUsage
|
|
262
282
|
.map((item) => ({
|
|
263
|
-
cost: summarizeCost(item.usage)
|
|
264
|
-
item,
|
|
265
|
-
tokens: summarizeUsage(item.usage)
|
|
283
|
+
cost: summarizeCost(item.usage),
|
|
284
|
+
modelId: item.modelId,
|
|
285
|
+
tokens: summarizeUsage(item.usage),
|
|
266
286
|
}))
|
|
267
287
|
.sort(
|
|
268
288
|
(a, b) =>
|
|
269
|
-
(b.cost ?? 0) - (a.cost ?? 0) ||
|
|
289
|
+
(b.cost?.total ?? 0) - (a.cost?.total ?? 0) ||
|
|
290
|
+
(b.tokens?.totalTokens ?? 0) - (a.tokens?.totalTokens ?? 0),
|
|
270
291
|
);
|
|
292
|
+
if (
|
|
293
|
+
activeModelId &&
|
|
294
|
+
!models.some((model) => model.modelId === activeModelId)
|
|
295
|
+
) {
|
|
296
|
+
models.unshift({
|
|
297
|
+
cost: undefined,
|
|
298
|
+
modelId: activeModelId,
|
|
299
|
+
tokens: undefined,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
271
302
|
|
|
272
303
|
const subagents = subagentUsage(childAgentTotals);
|
|
273
304
|
const subagentCost = subagentShare(subagents.cost, cost ?? subagents.cost);
|
|
@@ -276,8 +307,8 @@ export function ConversationUsageBreakdown(props: {
|
|
|
276
307
|
tokens ?? subagents.tokens,
|
|
277
308
|
);
|
|
278
309
|
|
|
279
|
-
|
|
280
|
-
<div className="grid min-w-0 gap-5">
|
|
310
|
+
const breakdown = (
|
|
311
|
+
<div className="grid min-w-0 gap-5 px-1 pb-2.5">
|
|
281
312
|
{childAgentTotals ? (
|
|
282
313
|
<BreakdownTable
|
|
283
314
|
countLabel="Tokens"
|
|
@@ -311,38 +342,51 @@ export function ConversationUsageBreakdown(props: {
|
|
|
311
342
|
<BreakdownTable
|
|
312
343
|
countLabel="Tokens"
|
|
313
344
|
label="Model"
|
|
314
|
-
rows={models.map(({ cost
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
</span>
|
|
326
|
-
{item.modelId === activeModelId ? (
|
|
327
|
-
<span className="shrink-0 text-dashboard-text-muted">
|
|
328
|
-
<ShimmerText active>in progress</ShimmerText>
|
|
345
|
+
rows={models.map(({ cost, modelId, tokens }) => {
|
|
346
|
+
const active = modelId === activeModelId;
|
|
347
|
+
return {
|
|
348
|
+
cost: active && !cost ? "…" : formatMaybeCost(cost?.total),
|
|
349
|
+
count:
|
|
350
|
+
active && !tokens ? "…" : formatTokens(tokens?.totalTokens),
|
|
351
|
+
key: modelId,
|
|
352
|
+
label: (
|
|
353
|
+
<span className="flex min-w-0 items-baseline gap-2">
|
|
354
|
+
<span className="min-w-0 truncate font-mono" title={modelId}>
|
|
355
|
+
{modelLabel(modelId)}
|
|
329
356
|
</span>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
357
|
+
{active ? (
|
|
358
|
+
<span className="shrink-0 text-dashboard-text-muted">
|
|
359
|
+
<ShimmerText active>in progress</ShimmerText>
|
|
360
|
+
</span>
|
|
361
|
+
) : null}
|
|
362
|
+
</span>
|
|
363
|
+
),
|
|
364
|
+
parts: tokenTypeParts(tokens, cost, formatCost),
|
|
365
|
+
};
|
|
366
|
+
})}
|
|
335
367
|
total={
|
|
336
|
-
|
|
368
|
+
modelUsage.length > 1
|
|
337
369
|
? {
|
|
338
370
|
cost: formatMaybeCost(modelCost?.total),
|
|
339
|
-
count: formatTokens(
|
|
371
|
+
count: formatTokens(modelTokens?.totalTokens),
|
|
340
372
|
label: "All models",
|
|
341
373
|
}
|
|
342
374
|
: undefined
|
|
343
375
|
}
|
|
344
376
|
/>
|
|
345
377
|
) : null}
|
|
378
|
+
{aggregateTokenTypes ? (
|
|
379
|
+
<BreakdownTable
|
|
380
|
+
countLabel="Tokens"
|
|
381
|
+
label="Token type"
|
|
382
|
+
rows={aggregateTokenTypes.map((part) => ({
|
|
383
|
+
cost: part.cost ?? NOT_RECORDED,
|
|
384
|
+
count: part.count,
|
|
385
|
+
key: part.label,
|
|
386
|
+
label: part.label,
|
|
387
|
+
}))}
|
|
388
|
+
/>
|
|
389
|
+
) : null}
|
|
346
390
|
{auxiliaryCosts && operations.length ? (
|
|
347
391
|
<BreakdownTable
|
|
348
392
|
countLabel="Calls"
|
|
@@ -369,7 +413,41 @@ export function ConversationUsageBreakdown(props: {
|
|
|
369
413
|
<p className="m-0 text-xs leading-relaxed text-dashboard-text-muted">
|
|
370
414
|
Estimated costs.
|
|
371
415
|
{childAgentTotals ? " Totals include subagents at any depth." : null}
|
|
416
|
+
{compactions
|
|
417
|
+
? ` ${formatCompactNumber(compactions)} compaction${compactions === 1 ? "" : "s"}.`
|
|
418
|
+
: null}
|
|
372
419
|
</p>
|
|
373
420
|
</div>
|
|
374
421
|
);
|
|
422
|
+
|
|
423
|
+
const summary = [
|
|
424
|
+
tokens === undefined ? undefined : `${formatTokens(tokens)} tokens`,
|
|
425
|
+
totalCost
|
|
426
|
+
? `${formatCostSummary(totalCost)}${live ? "+" : ""}`
|
|
427
|
+
: live
|
|
428
|
+
? "$…"
|
|
429
|
+
: undefined,
|
|
430
|
+
]
|
|
431
|
+
.filter(Boolean)
|
|
432
|
+
.join(" · ");
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<Collapsible className="min-w-0 border-t border-dashboard-border-subtle">
|
|
436
|
+
<h3 className="m-0">
|
|
437
|
+
<CollapsibleTrigger className="group flex min-h-9 w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-md px-1 py-2 text-left font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus">
|
|
438
|
+
<ChevronRight
|
|
439
|
+
aria-hidden="true"
|
|
440
|
+
className={dashboardDisclosureChevronClass}
|
|
441
|
+
/>
|
|
442
|
+
Usage
|
|
443
|
+
{summary ? (
|
|
444
|
+
<span className="ml-auto font-normal tabular-nums">
|
|
445
|
+
<ShimmerText active={live}>{summary}</ShimmerText>
|
|
446
|
+
</span>
|
|
447
|
+
) : null}
|
|
448
|
+
</CollapsibleTrigger>
|
|
449
|
+
</h3>
|
|
450
|
+
<CollapsibleContent hiddenUntilFound>{breakdown}</CollapsibleContent>
|
|
451
|
+
</Collapsible>
|
|
452
|
+
);
|
|
375
453
|
}
|
|
@@ -12,13 +12,19 @@ import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
|
12
12
|
export function DetailsCard(props: {
|
|
13
13
|
children: ReactNode;
|
|
14
14
|
count?: number;
|
|
15
|
+
flat?: boolean;
|
|
15
16
|
label?: string;
|
|
16
17
|
title?: string;
|
|
17
18
|
}) {
|
|
18
19
|
return (
|
|
19
20
|
<section
|
|
20
21
|
aria-label={props.label}
|
|
21
|
-
className=
|
|
22
|
+
className={cn(
|
|
23
|
+
"grid min-w-0 gap-2.5 border-dashboard-border-subtle",
|
|
24
|
+
props.flat
|
|
25
|
+
? "border-t px-1 py-2.5"
|
|
26
|
+
: "rounded-lg border bg-dashboard-surface-panel p-3",
|
|
27
|
+
)}
|
|
22
28
|
>
|
|
23
29
|
{props.title ? (
|
|
24
30
|
<h3 className="m-0 flex min-w-0 items-center gap-1.5 break-words font-sans text-xs font-medium text-dashboard-text-muted">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
memo,
|
|
3
|
+
useId,
|
|
3
4
|
useLayoutEffect,
|
|
4
5
|
useRef,
|
|
5
6
|
useState,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "@sentry/junior/api/schema";
|
|
13
14
|
import {
|
|
14
15
|
AlertCircle,
|
|
16
|
+
ChevronRight,
|
|
15
17
|
Clock3,
|
|
16
18
|
FileText,
|
|
17
19
|
Pencil,
|
|
@@ -44,6 +46,7 @@ import { SlackMark } from "./SlackMark";
|
|
|
44
46
|
import { DisplayText } from "../display-text/DisplayText";
|
|
45
47
|
import { ReviewItemChangeLabel } from "../review-desk/ReviewItemChangeRow";
|
|
46
48
|
import { displayPlainText } from "../display-text/plainText";
|
|
49
|
+
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
47
50
|
|
|
48
51
|
// Rows are one line each, so the stack can show five before it collapses.
|
|
49
52
|
const MAX_EXPANDED_PENDING_ROWS = 5;
|
|
@@ -306,6 +309,7 @@ function PendingRow(props: {
|
|
|
306
309
|
cancelPending: boolean;
|
|
307
310
|
canEdit: boolean;
|
|
308
311
|
canSteer: boolean;
|
|
312
|
+
className: string;
|
|
309
313
|
conversationId?: string;
|
|
310
314
|
editPending: boolean;
|
|
311
315
|
message: ConversationMailboxMessage;
|
|
@@ -319,13 +323,14 @@ function PendingRow(props: {
|
|
|
319
323
|
const mentions = props.message.mentions ?? [];
|
|
320
324
|
const redacted = Boolean(props.message.redacted);
|
|
321
325
|
const showSlack = props.message.source === "slack";
|
|
322
|
-
const canRetry =
|
|
323
|
-
props.message.clientStatus === "failed" &&
|
|
324
|
-
Boolean(props.message.idempotencyKey) &&
|
|
325
|
-
Boolean(props.onRetry);
|
|
326
326
|
|
|
327
327
|
return (
|
|
328
|
-
<article
|
|
328
|
+
<article
|
|
329
|
+
className={cn(
|
|
330
|
+
"min-w-0 grid-cols-[minmax(0,1fr)] gap-1 px-3 py-1.5 text-dashboard-text md:px-3.5",
|
|
331
|
+
props.className,
|
|
332
|
+
)}
|
|
333
|
+
>
|
|
329
334
|
<TranscriptHeadingRow
|
|
330
335
|
className="items-center"
|
|
331
336
|
left={
|
|
@@ -390,7 +395,7 @@ function PendingRow(props: {
|
|
|
390
395
|
/>
|
|
391
396
|
}
|
|
392
397
|
/>
|
|
393
|
-
{
|
|
398
|
+
{props.onRetry ? (
|
|
394
399
|
<div className="flex min-w-0 items-center gap-2">
|
|
395
400
|
<p className="m-0 font-sans text-xs text-red-300/80">
|
|
396
401
|
Could not send.
|
|
@@ -414,36 +419,42 @@ function PendingRow(props: {
|
|
|
414
419
|
}
|
|
415
420
|
|
|
416
421
|
function ExpandQueuedMessagesButton(props: {
|
|
422
|
+
className?: string;
|
|
423
|
+
controls: string;
|
|
417
424
|
expanded: boolean;
|
|
418
|
-
hiddenCount: number;
|
|
419
425
|
onClick(): void;
|
|
426
|
+
tallHiddenCount: number;
|
|
420
427
|
totalCount: number;
|
|
421
428
|
}) {
|
|
422
|
-
const totalLabel =
|
|
423
|
-
props.totalCount === 1
|
|
424
|
-
? "1 queued message"
|
|
425
|
-
: `${props.totalCount} queued messages`;
|
|
426
|
-
const moreLabel =
|
|
427
|
-
props.hiddenCount > 0
|
|
428
|
-
? `${props.hiddenCount} more queued messages`
|
|
429
|
-
: totalLabel;
|
|
430
|
-
// Mobile collapses previews and uses the total count as the expand control.
|
|
431
|
-
const mobileCollapsedLabel = totalLabel;
|
|
432
|
-
const label = props.expanded ? "Show fewer queued messages" : moreLabel;
|
|
433
|
-
|
|
434
429
|
return (
|
|
435
430
|
<button
|
|
431
|
+
aria-controls={props.controls}
|
|
436
432
|
aria-expanded={props.expanded}
|
|
437
|
-
className=
|
|
433
|
+
className={cn(
|
|
434
|
+
"group flex w-full cursor-pointer items-center gap-1.5 border-0 bg-transparent px-3 py-2 text-left font-sans text-xs font-medium text-amber-100/80 transition-colors hover:text-amber-50 focus-visible:outline focus-visible:outline-1 focus-visible:outline-amber-200/55 md:px-3.5",
|
|
435
|
+
props.className,
|
|
436
|
+
)}
|
|
438
437
|
onClick={props.onClick}
|
|
439
438
|
type="button"
|
|
440
439
|
>
|
|
440
|
+
<ChevronRight
|
|
441
|
+
aria-hidden="true"
|
|
442
|
+
className={dashboardDisclosureChevronClass}
|
|
443
|
+
/>
|
|
441
444
|
{props.expanded ? (
|
|
442
|
-
|
|
445
|
+
"Show fewer queued messages"
|
|
443
446
|
) : (
|
|
444
447
|
<>
|
|
445
|
-
<span className="md:hidden">
|
|
446
|
-
|
|
448
|
+
<span className="md:tall:hidden">
|
|
449
|
+
{props.totalCount} queued messages
|
|
450
|
+
</span>
|
|
451
|
+
{props.tallHiddenCount > 0 ? (
|
|
452
|
+
<span className="hidden md:tall:inline">
|
|
453
|
+
{props.tallHiddenCount === 1
|
|
454
|
+
? "1 more queued message"
|
|
455
|
+
: `${props.tallHiddenCount} more queued messages`}
|
|
456
|
+
</span>
|
|
457
|
+
) : null}
|
|
447
458
|
</>
|
|
448
459
|
)}
|
|
449
460
|
</button>
|
|
@@ -484,6 +495,7 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
484
495
|
viewerEmail?: string;
|
|
485
496
|
}): ReactNode {
|
|
486
497
|
const [expanded, setExpanded] = useState(false);
|
|
498
|
+
const rowsId = useId();
|
|
487
499
|
const onLayoutChangeRef = useRef(props.onLayoutChange);
|
|
488
500
|
onLayoutChangeRef.current = props.onLayoutChange;
|
|
489
501
|
const hasMountedExpandedRef = useRef(false);
|
|
@@ -500,24 +512,46 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
500
512
|
// Preserve mailbox order and clientStatus from the merged source rows.
|
|
501
513
|
// History wins on messageId so a send does not appear twice once workers
|
|
502
514
|
// persist the user message.
|
|
503
|
-
const rows = props.messages
|
|
504
|
-
(message) => !committedIds.has(message.messageId)
|
|
505
|
-
|
|
515
|
+
const rows = props.messages
|
|
516
|
+
.filter((message) => !committedIds.has(message.messageId))
|
|
517
|
+
.map((message) => {
|
|
518
|
+
const cancelError = Boolean(
|
|
519
|
+
props.cancelError &&
|
|
520
|
+
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
521
|
+
);
|
|
522
|
+
const retry =
|
|
523
|
+
message.clientStatus === "failed" && message.idempotencyKey
|
|
524
|
+
? props.onRetry
|
|
525
|
+
: undefined;
|
|
526
|
+
return {
|
|
527
|
+
cancelError,
|
|
528
|
+
message,
|
|
529
|
+
retry,
|
|
530
|
+
needsAttention: cancelError || !!retry,
|
|
531
|
+
};
|
|
532
|
+
});
|
|
506
533
|
if (rows.length === 0) return null;
|
|
507
534
|
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
535
|
+
const longQueue = rows.length > MAX_EXPANDED_PENDING_ROWS;
|
|
536
|
+
const canCollapse =
|
|
537
|
+
rows.length > 1 && rows.some((row) => !row.needsAttention);
|
|
538
|
+
const tallHiddenCount = longQueue
|
|
539
|
+
? rows
|
|
540
|
+
.slice(COLLAPSED_PENDING_ROW_COUNT)
|
|
541
|
+
.filter((row) => !row.needsAttention).length
|
|
542
|
+
: 0;
|
|
513
543
|
const toggleExpanded = () => setExpanded((value) => !value);
|
|
514
544
|
const actionsDisabled =
|
|
515
545
|
Boolean(props.cancelPending) ||
|
|
516
546
|
Boolean(props.editTargetInboundMessageId) ||
|
|
517
547
|
Boolean(props.steerTargetInboundMessageId);
|
|
518
|
-
const renderRow = (
|
|
519
|
-
const
|
|
520
|
-
|
|
548
|
+
const renderRow = (row: (typeof rows)[number], index: number) => {
|
|
549
|
+
const { cancelError, message, needsAttention, retry } = row;
|
|
550
|
+
const collapsed = canCollapse && !expanded && !needsAttention;
|
|
551
|
+
if (collapsed && longQueue && index >= COLLAPSED_PENDING_ROW_COUNT) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
521
555
|
// Only accepted server rows can leave the queue. Local rows are still sending.
|
|
522
556
|
const accepted = message.clientStatus === undefined;
|
|
523
557
|
const ownWebRow =
|
|
@@ -529,10 +563,20 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
529
563
|
<PendingRow
|
|
530
564
|
actionsDisabled={actionsDisabled}
|
|
531
565
|
authors={props.authors}
|
|
532
|
-
cancelError={
|
|
533
|
-
cancelPending={Boolean(
|
|
566
|
+
cancelError={cancelError}
|
|
567
|
+
cancelPending={Boolean(
|
|
568
|
+
props.cancelPending &&
|
|
569
|
+
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
570
|
+
)}
|
|
534
571
|
canEdit={ownWebRow}
|
|
535
572
|
canSteer={ownWebRow && message.delivery === "defer"}
|
|
573
|
+
className={
|
|
574
|
+
collapsed
|
|
575
|
+
? longQueue
|
|
576
|
+
? "hidden md:tall:grid"
|
|
577
|
+
: "grid md:hidden md:tall:grid"
|
|
578
|
+
: "grid"
|
|
579
|
+
}
|
|
536
580
|
conversationId={props.conversationId}
|
|
537
581
|
editPending={
|
|
538
582
|
props.editTargetInboundMessageId === message.inboundMessageId
|
|
@@ -541,7 +585,7 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
541
585
|
message={message}
|
|
542
586
|
onCancel={accepted ? props.onCancelMessage : undefined}
|
|
543
587
|
onEdit={props.onEditMessage}
|
|
544
|
-
onRetry={
|
|
588
|
+
onRetry={retry}
|
|
545
589
|
onSteer={props.onSteerMessage}
|
|
546
590
|
steerPending={
|
|
547
591
|
props.steerTargetInboundMessageId === message.inboundMessageId
|
|
@@ -555,17 +599,17 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
555
599
|
aria-label="Pending messages"
|
|
556
600
|
className="mx-2 overflow-hidden rounded-t-lg bg-amber-300/[0.055] md:mx-3"
|
|
557
601
|
>
|
|
558
|
-
{
|
|
559
|
-
// Desktop keeps a short preview; mobile collapses to the control only.
|
|
560
|
-
<div className="hidden md:block">{previewRows.map(renderRow)}</div>
|
|
561
|
-
) : (
|
|
562
|
-
visibleRows.map(renderRow)
|
|
563
|
-
)}
|
|
602
|
+
<div id={rowsId}>{rows.map(renderRow)}</div>
|
|
564
603
|
{canCollapse ? (
|
|
565
604
|
<ExpandQueuedMessagesButton
|
|
605
|
+
className={cn(
|
|
606
|
+
!longQueue && "hidden md:flex",
|
|
607
|
+
tallHiddenCount === 0 && "md:tall:hidden",
|
|
608
|
+
)}
|
|
609
|
+
controls={rowsId}
|
|
566
610
|
expanded={expanded}
|
|
567
|
-
hiddenCount={hiddenCount}
|
|
568
611
|
onClick={toggleExpanded}
|
|
612
|
+
tallHiddenCount={tallHiddenCount}
|
|
569
613
|
totalCount={rows.length}
|
|
570
614
|
/>
|
|
571
615
|
) : null}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment,
|
|
1
|
+
import { Fragment, type ReactNode } from "react";
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import { ShimmerText } from "../components/ShimmerText";
|
|
@@ -15,6 +15,7 @@ import type { RenderedTranscriptEntry } from "./transcriptRenderModel";
|
|
|
15
15
|
import { cn } from "../styles";
|
|
16
16
|
import { useTranscriptSearch } from "./transcriptSearch";
|
|
17
17
|
import { TranscriptActivityNesting } from "./TranscriptRailEvent";
|
|
18
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
18
19
|
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
19
20
|
|
|
20
21
|
function countLabel(count: number, singular: string, plural: string): string {
|
|
@@ -175,18 +176,16 @@ function hasLiveActivity(entries: RenderedTranscriptEntry[]): boolean {
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
/** Keep activity collapsed until the reader explicitly opens it. */
|
|
179
|
-
export function activityGroupOpen(userOpen: boolean | null): boolean {
|
|
180
|
-
return userOpen ?? false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
179
|
/** Collapse completed non-message activity so chat messages stay primary. */
|
|
184
180
|
export function TranscriptActivityGroup(props: {
|
|
185
181
|
entries: RenderedTranscriptEntry[];
|
|
182
|
+
/** Keeps the group open while it unmounts outside the viewport. */
|
|
183
|
+
openKey?: string;
|
|
186
184
|
renderEntry: (entry: RenderedTranscriptEntry) => ReactNode;
|
|
187
185
|
}) {
|
|
188
186
|
const { active: searchActive } = useTranscriptSearch();
|
|
189
|
-
|
|
187
|
+
// Activity stays collapsed until the reader opens it.
|
|
188
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
190
189
|
const rows = props.entries.map((entry) => (
|
|
191
190
|
<Fragment key={entry.key}>{props.renderEntry(entry)}</Fragment>
|
|
192
191
|
));
|
|
@@ -202,14 +201,8 @@ export function TranscriptActivityGroup(props: {
|
|
|
202
201
|
);
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
const open = activityGroupOpen(userOpen);
|
|
206
|
-
|
|
207
204
|
return (
|
|
208
|
-
<Collapsible
|
|
209
|
-
className="min-w-0 py-0.5"
|
|
210
|
-
onOpenChange={setUserOpen}
|
|
211
|
-
open={open}
|
|
212
|
-
>
|
|
205
|
+
<Collapsible className="min-w-0 py-0.5" onOpenChange={setOpen} open={open}>
|
|
213
206
|
<TranscriptSummaryTrigger
|
|
214
207
|
flush
|
|
215
208
|
className={cn(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import { useState } from "react";
|
|
3
2
|
|
|
4
3
|
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
5
4
|
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
@@ -9,6 +8,7 @@ import { messageRawText } from "./transcriptRenderModel";
|
|
|
9
8
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
10
9
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
11
10
|
import type { TranscriptViewMessage } from "../types";
|
|
11
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
12
12
|
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
13
13
|
|
|
14
14
|
function EventTypeChip(props: { eventType: string }) {
|
|
@@ -28,9 +28,13 @@ function batchSummary(events: readonly { label?: string }[]): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Render an Event Message whose heading expands its full text. */
|
|
31
|
-
export function TranscriptEventView(props: {
|
|
31
|
+
export function TranscriptEventView(props: {
|
|
32
|
+
message: TranscriptViewMessage;
|
|
33
|
+
/** Keeps the text open while the row unmounts outside the viewport. */
|
|
34
|
+
openKey?: string;
|
|
35
|
+
}) {
|
|
32
36
|
const { active: searchActive } = useTranscriptSearch();
|
|
33
|
-
const [open, setOpen] =
|
|
37
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
34
38
|
const text = messageRawText(props.message);
|
|
35
39
|
const redacted = props.message.parts.some(
|
|
36
40
|
(part) => part.type === "text" && part.redacted,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
1
|
import { Lightbulb } from "lucide-react";
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -13,15 +12,18 @@ import {
|
|
|
13
12
|
TranscriptHeadingMeta,
|
|
14
13
|
TranscriptHeadingRow,
|
|
15
14
|
} from "./TranscriptHeadingRow";
|
|
15
|
+
import { useTranscriptRowOpen } from "./TranscriptRows";
|
|
16
16
|
import { useTranscriptSearch } from "./transcriptSearch";
|
|
17
17
|
|
|
18
18
|
/** Render reasoning collapsed with a short preview until expanded or searched. */
|
|
19
19
|
export function TranscriptReasoningView(props: {
|
|
20
|
+
/** Keeps the entry open while it unmounts outside the viewport. */
|
|
21
|
+
openKey?: string;
|
|
20
22
|
part: TranscriptViewReasoningPart;
|
|
21
23
|
timestamp?: number;
|
|
22
24
|
}) {
|
|
23
25
|
const { active: searchActive } = useTranscriptSearch();
|
|
24
|
-
const [open, setOpen] =
|
|
26
|
+
const [open, setOpen] = useTranscriptRowOpen(props.openKey);
|
|
25
27
|
const expanded = searchActive || open;
|
|
26
28
|
const rendered = props.part.redacted ? "Reasoning redacted" : props.part.text;
|
|
27
29
|
const timestamp = formatMessageTimestamp(props.timestamp);
|