@sentry/junior-dashboard 0.263.0 → 0.264.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 +2 -2
- package/dist/client.js +62 -58
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +7 -3
- package/src/client/components/BottomSheet.tsx +16 -18
- package/src/client/components/Drawer.tsx +22 -12
- package/src/client/components/README.md +37 -9
- package/src/client/components/ui/sheet.tsx +141 -0
- package/src/client/components/ui/useSheet.ts +74 -0
- package/src/client/conversations/ConversationChildAgents.tsx +87 -58
- package/src/client/conversations/ConversationDetails.tsx +85 -67
- package/src/client/conversations/ConversationDetailsFrame.tsx +47 -19
- package/src/client/conversations/ConversationMeta.tsx +56 -4
- package/src/client/conversations/ConversationPage.tsx +28 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
- package/src/client/conversations/README.md +6 -5
- package/src/client/conversations/ShareConversationButton.tsx +15 -2
- package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
- package/src/client/lib/utils.ts +1 -0
- package/src/client/pages/dev/ComponentsPage.tsx +1 -1
- package/src/tailwind.css +77 -0
- package/src/client/components/Sheet.tsx +0 -141
- /package/src/client/components/{Sheet.LICENSE → ui/sheet.LICENSE} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ExternalLink } from "lucide-react";
|
|
2
1
|
import { useState, type ReactNode } from "react";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { IconButtonTooltip } from "../components/Tooltip";
|
|
4
|
+
import {
|
|
5
|
+
ConversationDetailsFrame,
|
|
6
|
+
useConversationDetailsPanel,
|
|
7
|
+
} from "./ConversationDetailsFrame";
|
|
7
8
|
import { SegmentedTabs } from "../components/SegmentedTabs";
|
|
8
9
|
import {
|
|
9
10
|
ConversationChildAgents,
|
|
@@ -18,20 +19,25 @@ import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
|
18
19
|
|
|
19
20
|
type DetailsTab = "agents" | "summary" | "memories";
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Show conversation details: the brief and facts first, then linked work,
|
|
24
|
+
* participants, and the usage breakdown. Share and Sentry links stay in the
|
|
25
|
+
* header so every tab can reach them.
|
|
26
|
+
*/
|
|
22
27
|
export function ConversationDetails(props: {
|
|
23
28
|
annotations: ReactNode;
|
|
24
29
|
brief: ReactNode;
|
|
25
30
|
conversationId: string;
|
|
26
31
|
identity: ReactNode;
|
|
27
|
-
lastActivityAt?: string;
|
|
28
32
|
onClose(): void;
|
|
29
|
-
/** Open one child agent transcript.
|
|
33
|
+
/** Open one child agent transcript. A details sheet closes first. */
|
|
30
34
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
35
|
+
overview: ReactNode;
|
|
31
36
|
sentryConversationUrl?: string;
|
|
32
|
-
|
|
37
|
+
usage: ReactNode;
|
|
33
38
|
}) {
|
|
34
39
|
const [tab, setTab] = useState<DetailsTab>("summary");
|
|
40
|
+
const panel = useConversationDetailsPanel();
|
|
35
41
|
// Load both lists with details so each tab can show its count.
|
|
36
42
|
const memories = useConversationMemories(props.conversationId);
|
|
37
43
|
const agents = useConversationChildAgents(props.conversationId);
|
|
@@ -46,14 +52,38 @@ export function ConversationDetails(props: {
|
|
|
46
52
|
] as const;
|
|
47
53
|
|
|
48
54
|
const sections = [
|
|
49
|
-
{ content: props.
|
|
50
|
-
{ content: props.
|
|
51
|
-
{ content: props.
|
|
52
|
-
{ content: props.
|
|
55
|
+
{ content: props.overview, key: "overview", title: undefined },
|
|
56
|
+
{ content: props.identity, key: "participants", title: "Participants" },
|
|
57
|
+
{ content: props.annotations, key: "linked-work", title: "Linked work" },
|
|
58
|
+
{ content: props.usage, key: "usage", title: "Usage" },
|
|
53
59
|
].filter((section) => section.content != null);
|
|
54
60
|
|
|
55
61
|
return (
|
|
56
|
-
<ConversationDetailsFrame
|
|
62
|
+
<ConversationDetailsFrame
|
|
63
|
+
actions={
|
|
64
|
+
<>
|
|
65
|
+
<ShareConversationButton
|
|
66
|
+
key={props.conversationId}
|
|
67
|
+
conversationId={props.conversationId}
|
|
68
|
+
layout="icon"
|
|
69
|
+
/>
|
|
70
|
+
{props.sentryConversationUrl ? (
|
|
71
|
+
<IconButtonTooltip label="Open in Sentry" placement="below">
|
|
72
|
+
<a
|
|
73
|
+
aria-label="Open in Sentry"
|
|
74
|
+
className="grid size-9 place-items-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
|
|
75
|
+
href={props.sentryConversationUrl}
|
|
76
|
+
rel="noopener noreferrer"
|
|
77
|
+
target="_blank"
|
|
78
|
+
>
|
|
79
|
+
<SentryGlyph />
|
|
80
|
+
</a>
|
|
81
|
+
</IconButtonTooltip>
|
|
82
|
+
) : null}
|
|
83
|
+
</>
|
|
84
|
+
}
|
|
85
|
+
onClose={props.onClose}
|
|
86
|
+
>
|
|
57
87
|
<SegmentedTabs
|
|
58
88
|
scroll
|
|
59
89
|
items={tabs}
|
|
@@ -67,26 +97,22 @@ export function ConversationDetails(props: {
|
|
|
67
97
|
<ConversationChildAgents
|
|
68
98
|
conversationId={props.conversationId}
|
|
69
99
|
onOpenAgent={(target) => {
|
|
70
|
-
|
|
100
|
+
// The panel stays beside the drawer. A modal sheet cannot.
|
|
101
|
+
if (!panel) props.onClose();
|
|
71
102
|
props.onOpenSubagent(target);
|
|
72
103
|
}}
|
|
73
104
|
/>
|
|
74
|
-
) : sections.length > 0 ? (
|
|
75
|
-
<div className="grid min-w-0 gap-
|
|
105
|
+
) : props.brief != null || sections.length > 0 ? (
|
|
106
|
+
<div className="grid min-w-0 gap-3">
|
|
107
|
+
{props.brief != null ? (
|
|
108
|
+
<div className="min-w-0 break-words px-1 pb-1 font-sans text-sm leading-relaxed text-dashboard-text-muted">
|
|
109
|
+
{props.brief}
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
76
112
|
{sections.map((section) => (
|
|
77
|
-
<section
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
>
|
|
81
|
-
{section.title ? (
|
|
82
|
-
<h3 className="m-0 text-sm font-semibold text-dashboard-text">
|
|
83
|
-
{section.title}
|
|
84
|
-
</h3>
|
|
85
|
-
) : null}
|
|
86
|
-
<div className="min-w-0 break-words font-sans text-sm leading-relaxed text-dashboard-text-muted">
|
|
87
|
-
{section.content}
|
|
88
|
-
</div>
|
|
89
|
-
</section>
|
|
113
|
+
<DetailsSection key={section.key} title={section.title}>
|
|
114
|
+
{section.content}
|
|
115
|
+
</DetailsSection>
|
|
90
116
|
))}
|
|
91
117
|
</div>
|
|
92
118
|
) : (
|
|
@@ -94,45 +120,37 @@ export function ConversationDetails(props: {
|
|
|
94
120
|
No additional conversation details.
|
|
95
121
|
</p>
|
|
96
122
|
)}
|
|
97
|
-
{tab === "summary" ? (
|
|
98
|
-
<footer className="mt-5 grid gap-6 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
|
|
99
|
-
{props.lastActivityAt ? (
|
|
100
|
-
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
101
|
-
<span>Last activity</span>
|
|
102
|
-
<time
|
|
103
|
-
dateTime={props.lastActivityAt}
|
|
104
|
-
title={formatTime(props.lastActivityAt, {
|
|
105
|
-
dateStyle: "medium",
|
|
106
|
-
timeStyle: "short",
|
|
107
|
-
})}
|
|
108
|
-
>
|
|
109
|
-
{formatRelativeTime(props.lastActivityAt)}
|
|
110
|
-
</time>
|
|
111
|
-
</div>
|
|
112
|
-
) : null}
|
|
113
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
114
|
-
<ShareConversationButton
|
|
115
|
-
key={props.conversationId}
|
|
116
|
-
conversationId={props.conversationId}
|
|
117
|
-
/>
|
|
118
|
-
{props.sentryConversationUrl ? (
|
|
119
|
-
<ButtonLink
|
|
120
|
-
className="w-fit focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
121
|
-
to={props.sentryConversationUrl}
|
|
122
|
-
rel="noopener noreferrer"
|
|
123
|
-
target="_blank"
|
|
124
|
-
>
|
|
125
|
-
Open in Sentry
|
|
126
|
-
<ExternalLink
|
|
127
|
-
aria-hidden="true"
|
|
128
|
-
className="size-3.5 shrink-0"
|
|
129
|
-
/>
|
|
130
|
-
</ButtonLink>
|
|
131
|
-
) : null}
|
|
132
|
-
</div>
|
|
133
|
-
</footer>
|
|
134
|
-
) : null}
|
|
135
123
|
</SegmentedTabs>
|
|
136
124
|
</ConversationDetailsFrame>
|
|
137
125
|
);
|
|
138
126
|
}
|
|
127
|
+
|
|
128
|
+
/** Group one kind of conversation detail in a compact card. */
|
|
129
|
+
function DetailsSection(props: { children: ReactNode; title?: string }) {
|
|
130
|
+
return (
|
|
131
|
+
<section className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3">
|
|
132
|
+
{props.title ? (
|
|
133
|
+
<h3 className="m-0 font-sans text-xs font-medium text-dashboard-text-muted">
|
|
134
|
+
{props.title}
|
|
135
|
+
</h3>
|
|
136
|
+
) : null}
|
|
137
|
+
<div className="min-w-0 break-words font-sans text-xs leading-relaxed text-dashboard-text-muted">
|
|
138
|
+
{props.children}
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Draw the Sentry glyph in the current text color, sized like a header icon. */
|
|
145
|
+
function SentryGlyph() {
|
|
146
|
+
return (
|
|
147
|
+
<svg
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
className="size-4"
|
|
150
|
+
fill="currentColor"
|
|
151
|
+
viewBox="0 0 24 24"
|
|
152
|
+
>
|
|
153
|
+
<path d="M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z" />
|
|
154
|
+
</svg>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -11,28 +11,42 @@ import { Button } from "../components/Button";
|
|
|
11
11
|
import { BottomSheet } from "../components/BottomSheet";
|
|
12
12
|
|
|
13
13
|
const panelMediaQuery = "(min-width: 1280px)";
|
|
14
|
+
// Open details by default only when the chat column stays wide beside them.
|
|
15
|
+
const defaultOpenMediaQuery = "(min-width: 1440px)";
|
|
14
16
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
function useMediaQuery(query: string): boolean {
|
|
18
|
+
return useSyncExternalStore(
|
|
19
|
+
(onChange) => {
|
|
20
|
+
const media = window.matchMedia(query);
|
|
21
|
+
media.addEventListener("change", onChange);
|
|
22
|
+
return () => media.removeEventListener("change", onChange);
|
|
23
|
+
},
|
|
24
|
+
() => window.matchMedia(query).matches,
|
|
25
|
+
() => false,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Report whether the viewport has room to show details as a panel beside the
|
|
31
|
+
* chat. Narrower viewports show details in a bottom sheet.
|
|
32
|
+
*/
|
|
33
|
+
export function useConversationDetailsPanel(): boolean {
|
|
34
|
+
return useMediaQuery(panelMediaQuery);
|
|
19
35
|
}
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
/** Report whether details should open beside the chat without a request. */
|
|
38
|
+
export function useConversationDetailsDefaultOpen(): boolean {
|
|
39
|
+
return useMediaQuery(defaultOpenMediaQuery);
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
/** Keep details beside the chat when space permits, or in a modal bottom sheet. */
|
|
26
43
|
export function ConversationDetailsFrame(props: {
|
|
44
|
+
actions?: ReactNode;
|
|
27
45
|
children: ReactNode;
|
|
28
46
|
onClose(): void;
|
|
29
47
|
}) {
|
|
30
48
|
const titleId = useId();
|
|
31
|
-
const panel =
|
|
32
|
-
subscribeToPanelLayout,
|
|
33
|
-
hasPanelSpace,
|
|
34
|
-
() => false,
|
|
35
|
-
);
|
|
49
|
+
const panel = useConversationDetailsPanel();
|
|
36
50
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
37
51
|
const wasPanel = useRef(panel);
|
|
38
52
|
useEffect(() => {
|
|
@@ -49,6 +63,12 @@ export function ConversationDetailsFrame(props: {
|
|
|
49
63
|
return (
|
|
50
64
|
<BottomSheet label="Conversation details" onClose={props.onClose}>
|
|
51
65
|
{props.children}
|
|
66
|
+
{/* After the tabs, so the sheet still opens with the first tab focused. */}
|
|
67
|
+
{props.actions ? (
|
|
68
|
+
<div className="flex justify-end gap-1 border-t border-dashboard-border px-4 py-2">
|
|
69
|
+
{props.actions}
|
|
70
|
+
</div>
|
|
71
|
+
) : null}
|
|
52
72
|
</BottomSheet>
|
|
53
73
|
);
|
|
54
74
|
}
|
|
@@ -68,14 +88,22 @@ export function ConversationDetailsFrame(props: {
|
|
|
68
88
|
}
|
|
69
89
|
}}
|
|
70
90
|
>
|
|
71
|
-
<header className="flex items-center justify-between gap-3 border-b border-dashboard-border
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
<header className="flex items-center justify-between gap-3 border-b border-dashboard-border py-2 pl-5 pr-3">
|
|
92
|
+
{/* Keep actions beside the title. The panel opens under the pointer
|
|
93
|
+
that clicked the chat's Details button, so that corner holds only
|
|
94
|
+
Close. */}
|
|
95
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
96
|
+
<h2
|
|
97
|
+
aria-label="Conversation details"
|
|
98
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
99
|
+
id={titleId}
|
|
100
|
+
>
|
|
101
|
+
Details
|
|
102
|
+
</h2>
|
|
103
|
+
{props.actions ? (
|
|
104
|
+
<div className="flex items-center gap-0.5">{props.actions}</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
79
107
|
<Button
|
|
80
108
|
ref={closeRef}
|
|
81
109
|
aria-label="Close conversation details"
|
|
@@ -16,6 +16,8 @@ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
formatConversationDuration,
|
|
19
|
+
formatRelativeTime,
|
|
20
|
+
formatTime,
|
|
19
21
|
slackLocationLabel,
|
|
20
22
|
summarizeCost,
|
|
21
23
|
summarizeModelUsage,
|
|
@@ -547,7 +549,13 @@ export function hasConversationStats(props: {
|
|
|
547
549
|
return conversationStatItems(props).length > 0;
|
|
548
550
|
}
|
|
549
551
|
|
|
550
|
-
|
|
552
|
+
type ConversationStatItem = MetricListItem & { label: string };
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Render runtime, source, token, and cost metadata. The compact variant is one
|
|
556
|
+
* line under the conversation title. The full variant is a labeled list for
|
|
557
|
+
* conversation details.
|
|
558
|
+
*/
|
|
551
559
|
export function ConversationStats(props: {
|
|
552
560
|
conversation: Conversation | undefined;
|
|
553
561
|
detail?: ConversationDetailReport;
|
|
@@ -556,6 +564,24 @@ export function ConversationStats(props: {
|
|
|
556
564
|
const stats = conversationStatItems(props);
|
|
557
565
|
if (stats.length === 0) return null;
|
|
558
566
|
|
|
567
|
+
if ((props.variant ?? "full") === "full") {
|
|
568
|
+
return (
|
|
569
|
+
<dl className="m-0 grid min-w-0 gap-2 text-xs leading-snug">
|
|
570
|
+
{stats.map((item) => (
|
|
571
|
+
<div
|
|
572
|
+
className="flex min-w-0 items-baseline justify-between gap-4"
|
|
573
|
+
key={item.key}
|
|
574
|
+
>
|
|
575
|
+
<dt className="shrink-0 text-dashboard-text-muted">{item.label}</dt>
|
|
576
|
+
<dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
|
|
577
|
+
{item.content}
|
|
578
|
+
</dd>
|
|
579
|
+
</div>
|
|
580
|
+
))}
|
|
581
|
+
</dl>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
559
585
|
return (
|
|
560
586
|
<MetricList
|
|
561
587
|
className="break-words text-xs leading-[1.45] text-dashboard-text-muted"
|
|
@@ -568,7 +594,7 @@ function conversationStatItems(props: {
|
|
|
568
594
|
conversation: Conversation | undefined;
|
|
569
595
|
detail?: ConversationDetailReport;
|
|
570
596
|
variant?: "compact" | "full";
|
|
571
|
-
}):
|
|
597
|
+
}): ConversationStatItem[] {
|
|
572
598
|
if (!props.conversation) return [];
|
|
573
599
|
const variant = props.variant ?? "full";
|
|
574
600
|
const completeDetail = props.detail?.previousCursor
|
|
@@ -590,17 +616,37 @@ function conversationStatItems(props: {
|
|
|
590
616
|
const sourceTask = props.detail?.sourceTask;
|
|
591
617
|
const childAgentTotals =
|
|
592
618
|
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
|
|
593
|
-
const
|
|
619
|
+
const lastActivityAt = props.conversation.lastSeenAt;
|
|
620
|
+
const rawStats: Array<ConversationStatItem | undefined> = [
|
|
594
621
|
location
|
|
595
622
|
? {
|
|
596
623
|
content: <LocationLink label={location} locationUrl={locationUrl} />,
|
|
597
624
|
key: "location",
|
|
625
|
+
label: "Location",
|
|
598
626
|
}
|
|
599
627
|
: undefined,
|
|
600
628
|
variant === "full" && sourceTask
|
|
601
629
|
? {
|
|
602
630
|
content: <SourceTask sourceTask={sourceTask} />,
|
|
603
631
|
key: "source-task",
|
|
632
|
+
label: "Source",
|
|
633
|
+
}
|
|
634
|
+
: undefined,
|
|
635
|
+
variant === "full" && lastActivityAt
|
|
636
|
+
? {
|
|
637
|
+
content: (
|
|
638
|
+
<time
|
|
639
|
+
dateTime={lastActivityAt}
|
|
640
|
+
title={formatTime(lastActivityAt, {
|
|
641
|
+
dateStyle: "medium",
|
|
642
|
+
timeStyle: "short",
|
|
643
|
+
})}
|
|
644
|
+
>
|
|
645
|
+
{formatRelativeTime(lastActivityAt)}
|
|
646
|
+
</time>
|
|
647
|
+
),
|
|
648
|
+
key: "last-activity",
|
|
649
|
+
label: "Last activity",
|
|
604
650
|
}
|
|
605
651
|
: undefined,
|
|
606
652
|
durationLabel !== "none"
|
|
@@ -615,6 +661,7 @@ function conversationStatItems(props: {
|
|
|
615
661
|
/>
|
|
616
662
|
),
|
|
617
663
|
key: "duration",
|
|
664
|
+
label: "Runtime",
|
|
618
665
|
}
|
|
619
666
|
: undefined,
|
|
620
667
|
tokenSummary
|
|
@@ -637,6 +684,7 @@ function conversationStatItems(props: {
|
|
|
637
684
|
/>
|
|
638
685
|
),
|
|
639
686
|
key: "tokens",
|
|
687
|
+
label: "Tokens",
|
|
640
688
|
}
|
|
641
689
|
: undefined,
|
|
642
690
|
costSummary ||
|
|
@@ -659,14 +707,18 @@ function conversationStatItems(props: {
|
|
|
659
707
|
/>
|
|
660
708
|
),
|
|
661
709
|
key: "cost",
|
|
710
|
+
label: "Cost",
|
|
662
711
|
}
|
|
663
712
|
: undefined,
|
|
664
713
|
childAgentTotals
|
|
665
714
|
? {
|
|
666
715
|
content: <SubagentsMetric childAgentTotals={childAgentTotals} />,
|
|
667
716
|
key: "subagents",
|
|
717
|
+
label: "Subagents",
|
|
668
718
|
}
|
|
669
719
|
: undefined,
|
|
670
720
|
];
|
|
671
|
-
return rawStats.filter(
|
|
721
|
+
return rawStats.filter(
|
|
722
|
+
(item): item is ConversationStatItem => item !== undefined,
|
|
723
|
+
);
|
|
672
724
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { ConversationComposer } from "./ConversationComposer";
|
|
28
28
|
import { ConversationHeader } from "./ConversationHeader";
|
|
29
29
|
import { ConversationDetails } from "./ConversationDetails";
|
|
30
|
+
import { useConversationDetailsDefaultOpen } from "./ConversationDetailsFrame";
|
|
30
31
|
import { cn } from "../styles";
|
|
31
32
|
import { ConversationForkLinks } from "./ConversationForkLinks";
|
|
32
33
|
import { ConversationAgentTree } from "./ConversationAgentTree";
|
|
@@ -39,7 +40,10 @@ import {
|
|
|
39
40
|
hasConversationStats,
|
|
40
41
|
PendingAuthorization,
|
|
41
42
|
} from "./ConversationMeta";
|
|
42
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
ConversationUsageBreakdown,
|
|
45
|
+
hasConversationUsageBreakdown,
|
|
46
|
+
} from "./ConversationUsageBreakdown";
|
|
43
47
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
44
48
|
import {
|
|
45
49
|
buildConversations,
|
|
@@ -78,7 +82,11 @@ export function ConversationPage(props: {
|
|
|
78
82
|
onRead?(conversationId: string, lastReadAt: string): void;
|
|
79
83
|
pendingArchiveUpdate?: PendingArchiveConversationUpdate;
|
|
80
84
|
}) {
|
|
81
|
-
|
|
85
|
+
// Large viewports show details beside the chat until the reader closes
|
|
86
|
+
// them. Smaller viewports open details only on request.
|
|
87
|
+
const detailsDefaultOpen = useConversationDetailsDefaultOpen();
|
|
88
|
+
const [detailsChoice, setDetailsChoice] = useState<boolean>();
|
|
89
|
+
const detailsOpen = detailsChoice ?? detailsDefaultOpen;
|
|
82
90
|
const [subagentTarget, setSubagentTarget] =
|
|
83
91
|
useState<SubagentTranscriptTarget>();
|
|
84
92
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -217,7 +225,7 @@ export function ConversationPage(props: {
|
|
|
217
225
|
}
|
|
218
226
|
conversationId={conversationId}
|
|
219
227
|
detailsOpen={detailsOpen}
|
|
220
|
-
onDetailsClick={() =>
|
|
228
|
+
onDetailsClick={() => setDetailsChoice(!detailsOpen)}
|
|
221
229
|
forks={
|
|
222
230
|
detail.data?.forkedFromConversationId ||
|
|
223
231
|
detail.data?.forks?.length ? (
|
|
@@ -362,9 +370,8 @@ export function ConversationPage(props: {
|
|
|
362
370
|
<ConversationDetails
|
|
363
371
|
conversationId={conversationId}
|
|
364
372
|
identity={identity}
|
|
365
|
-
lastActivityAt={conversation?.lastSeenAt}
|
|
366
373
|
sentryConversationUrl={detail.data?.sentryConversationUrl}
|
|
367
|
-
onClose={() =>
|
|
374
|
+
onClose={() => setDetailsChoice(false)}
|
|
368
375
|
onOpenSubagent={setSubagentTarget}
|
|
369
376
|
annotations={
|
|
370
377
|
hasConversationAnnotations(detail.data?.annotations) ? (
|
|
@@ -376,21 +383,26 @@ export function ConversationPage(props: {
|
|
|
376
383
|
<ConversationBrief brief={detail.data.brief} variant="summary" />
|
|
377
384
|
) : null
|
|
378
385
|
}
|
|
379
|
-
|
|
386
|
+
overview={
|
|
380
387
|
hasConversationStats({
|
|
381
388
|
conversation,
|
|
382
389
|
detail: detail.data,
|
|
383
390
|
}) ? (
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
<ConversationStats
|
|
392
|
+
conversation={conversation}
|
|
393
|
+
detail={detail.data}
|
|
394
|
+
/>
|
|
395
|
+
) : null
|
|
396
|
+
}
|
|
397
|
+
usage={
|
|
398
|
+
hasConversationUsageBreakdown({
|
|
399
|
+
conversation,
|
|
400
|
+
detail: detail.data,
|
|
401
|
+
}) ? (
|
|
402
|
+
<ConversationUsageBreakdown
|
|
403
|
+
conversation={conversation}
|
|
404
|
+
detail={detail.data}
|
|
405
|
+
/>
|
|
394
406
|
) : null
|
|
395
407
|
}
|
|
396
408
|
/>
|
|
@@ -167,6 +167,19 @@ function BreakdownTable(props: {
|
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** True when the Conversation has model or auxiliary usage to break down. */
|
|
171
|
+
export function hasConversationUsageBreakdown(props: {
|
|
172
|
+
conversation: Conversation | undefined;
|
|
173
|
+
detail?: ConversationDetailReport;
|
|
174
|
+
}): boolean {
|
|
175
|
+
if (!props.conversation) return false;
|
|
176
|
+
const auxiliaryCosts =
|
|
177
|
+
props.detail?.auxiliaryCosts ?? props.conversation.auxiliaryCosts;
|
|
178
|
+
return Boolean(
|
|
179
|
+
props.detail?.modelUsage?.length || auxiliaryCosts?.operations?.length,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
/**
|
|
171
184
|
* Break a Conversation's usage down by agent, model, and auxiliary
|
|
172
185
|
* operation. The header tooltips show only the largest items; this lists
|
|
@@ -177,7 +190,9 @@ export function ConversationUsageBreakdown(props: {
|
|
|
177
190
|
conversation: Conversation | undefined;
|
|
178
191
|
detail?: ConversationDetailReport;
|
|
179
192
|
}) {
|
|
180
|
-
if (!props.conversation)
|
|
193
|
+
if (!hasConversationUsageBreakdown(props) || !props.conversation) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
181
196
|
const modelUsage = props.detail?.modelUsage ?? [];
|
|
182
197
|
const auxiliaryCosts =
|
|
183
198
|
props.detail?.auxiliaryCosts ?? props.conversation.auxiliaryCosts;
|
|
@@ -193,8 +208,6 @@ export function ConversationUsageBreakdown(props: {
|
|
|
193
208
|
const operations = [...(auxiliaryCosts?.operations ?? [])].sort(
|
|
194
209
|
(a, b) => b.costUsd - a.costUsd,
|
|
195
210
|
);
|
|
196
|
-
if (!modelUsage.length && !operations.length) return null;
|
|
197
|
-
|
|
198
211
|
const formatCost = costFormatter(cost ?? 0);
|
|
199
212
|
const formatTokens = (value: number | undefined) =>
|
|
200
213
|
value === undefined ? NOT_RECORDED : formatCompactNumber(value);
|
|
@@ -57,9 +57,10 @@ dismissal.
|
|
|
57
57
|
|
|
58
58
|
`ConversationDetails` owns one set of tabs and content. `ConversationDetailsFrame`
|
|
59
59
|
uses a 400px aside at 1280px and above. The chat and aside scroll separately.
|
|
60
|
-
Smaller windows use `BottomSheet` with a
|
|
61
|
-
side drawers
|
|
62
|
-
scroll lock. A resize changes the frame and keeps the selected tab. Tabs stay
|
|
63
|
-
layouts.
|
|
64
|
-
backdrop, or press Escape to dismiss it. Base UI owns the
|
|
60
|
+
Smaller windows use `BottomSheet` with a visible title and close control. The sheet and
|
|
61
|
+
side drawers use the shadcn Base UI `Sheet` and share the app's document
|
|
62
|
+
scroll lock. A resize changes the frame and keeps the selected tab. Tabs stay
|
|
63
|
+
fixed above the scrolling content in both layouts. The sheet title and close control sit above the tabs. Use the close
|
|
64
|
+
control, tap the backdrop, or press Escape to dismiss it. Base UI owns the
|
|
65
|
+
transition lifecycle.
|
|
65
66
|
A pending exit cannot close a new panel or a different Conversation.
|
|
@@ -3,12 +3,13 @@ import { useHref } from "react-router";
|
|
|
3
3
|
import { Check, Share2 } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { Button } from "../components/Button";
|
|
6
|
+
import { IconButtonTooltip } from "../components/Tooltip";
|
|
6
7
|
import { conversationPath } from "./conversationRoutes";
|
|
7
8
|
|
|
8
9
|
/** Share a conversation link without changing who can access it. */
|
|
9
10
|
export function ShareConversationButton(props: {
|
|
10
11
|
conversationId: string;
|
|
11
|
-
layout?: "button" | "menu";
|
|
12
|
+
layout?: "button" | "icon" | "menu";
|
|
12
13
|
}) {
|
|
13
14
|
const href = useHref(conversationPath(props.conversationId));
|
|
14
15
|
const [status, setStatus] = useState<
|
|
@@ -70,7 +71,19 @@ export function ShareConversationButton(props: {
|
|
|
70
71
|
</>
|
|
71
72
|
);
|
|
72
73
|
const button =
|
|
73
|
-
props.layout === "
|
|
74
|
+
props.layout === "icon" ? (
|
|
75
|
+
<IconButtonTooltip label={label} placement="below">
|
|
76
|
+
<Button
|
|
77
|
+
aria-label={label}
|
|
78
|
+
className="text-dashboard-text-muted"
|
|
79
|
+
disabled={status === "sharing"}
|
|
80
|
+
onClick={() => void shareLink()}
|
|
81
|
+
size="icon"
|
|
82
|
+
>
|
|
83
|
+
<Icon aria-hidden="true" size={15} />
|
|
84
|
+
</Button>
|
|
85
|
+
</IconButtonTooltip>
|
|
86
|
+
) : props.layout === "menu" ? (
|
|
74
87
|
<button
|
|
75
88
|
aria-label={label}
|
|
76
89
|
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:bg-dashboard-fill-hover focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
@@ -8,9 +8,9 @@ import { subagentStatusTone } from "./ConversationChildAgents";
|
|
|
8
8
|
import { HighlightText } from "./transcriptSearch";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Render a child agent as a compact card in the transcript stream
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Render a child agent as a compact card in the transcript stream: name,
|
|
12
|
+
* status, and run time first, then the task title. It opens the child
|
|
13
|
+
* transcript.
|
|
14
14
|
*/
|
|
15
15
|
export function TranscriptSubagentView(props: {
|
|
16
16
|
onOpenTranscript?: (part: TranscriptViewSubagentPart) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from "cn";
|
|
@@ -313,7 +313,7 @@ function FoundationsGalleryPage() {
|
|
|
313
313
|
onClose={() => setSheetOpen(false)}
|
|
314
314
|
>
|
|
315
315
|
<p className="m-0 p-4 pb-[max(1rem,env(safe-area-inset-bottom))] text-sm text-dashboard-text-muted">
|
|
316
|
-
|
|
316
|
+
Use the close button, tap outside, or press Escape to return to
|
|
317
317
|
the page.
|
|
318
318
|
</p>
|
|
319
319
|
</BottomSheet>
|