@sentry/junior-dashboard 0.274.0 → 0.275.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 +72 -72
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/client/App.tsx +11 -0
- package/src/client/components/Metric.tsx +0 -32
- package/src/client/components/ProfileMenu.tsx +56 -4
- package/src/client/components/layout/DashboardChrome.tsx +3 -3
- package/src/client/components/layout/DashboardHeader.tsx +41 -4
- package/src/client/components/layout/NavRail.tsx +43 -40
- package/src/client/conversations/ConversationDetails.tsx +117 -162
- package/src/client/conversations/ConversationHeader.tsx +119 -133
- package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
- package/src/client/conversations/ConversationMeta.tsx +49 -44
- package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
- package/src/client/conversations/ConversationPage.tsx +33 -368
- package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
- package/src/client/conversations/ConversationSidebar.tsx +5 -7
- package/src/client/conversations/TelemetryMetrics.tsx +0 -14
- package/src/client/conversations/TranscriptView.tsx +15 -32
- package/src/client/conversations/UsageCard.tsx +1 -15
- package/src/client/conversations/conversationSections.ts +18 -14
- package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
- package/src/tailwind.css +4 -0
- package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -85
|
@@ -1,17 +1,12 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
1
2
|
import { useState, type ReactNode } from "react";
|
|
3
|
+
import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
2
4
|
|
|
3
5
|
import { DetailsPanel, useDetailsPanel } from "../components/DetailsPanel";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "../components/ui/tooltip";
|
|
9
|
-
import {
|
|
10
|
-
Tabs,
|
|
11
|
-
TabsContent,
|
|
12
|
-
TabsList,
|
|
13
|
-
TabsTrigger,
|
|
14
|
-
} from "../components/ui/tabs";
|
|
6
|
+
import { formatRelativeTime, formatTime } from "../format";
|
|
7
|
+
import { ConversationReviewItems } from "../review-desk/ConversationReviewItems";
|
|
8
|
+
import { dashboardDisclosureChevronClass } from "../styles";
|
|
9
|
+
import { ConversationBrief } from "./ConversationBrief";
|
|
15
10
|
import {
|
|
16
11
|
ConversationChildAgents,
|
|
17
12
|
useConversationChildAgents,
|
|
@@ -29,62 +24,33 @@ import {
|
|
|
29
24
|
useConversationWatches,
|
|
30
25
|
} from "./ConversationWatches";
|
|
31
26
|
import { DetailsCard } from "./DetailsList";
|
|
32
|
-
import { ShareConversationButton } from "./ShareConversationButton";
|
|
33
27
|
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
34
28
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
/** Keep the tabs fixed while each panel scrolls to the frame edge. */
|
|
38
|
-
const detailsPanelClass =
|
|
39
|
-
"min-h-0 overflow-y-auto overscroll-contain rounded-none px-4 pb-[max(1rem,env(safe-area-inset-bottom))] focus-visible:-outline-offset-2";
|
|
29
|
+
type Brief = NonNullable<ConversationDetailReport["brief"]>;
|
|
40
30
|
|
|
41
31
|
/**
|
|
42
|
-
* Show conversation details
|
|
43
|
-
*
|
|
44
|
-
*
|
|
32
|
+
* Show conversation details in the order a reader asks about them: what the
|
|
33
|
+
* work is (the Brief), what needs them, what it changed or waits on, who did
|
|
34
|
+
* it, and what it cost. Each fact shows here only, not in the title bar.
|
|
45
35
|
*/
|
|
46
36
|
export function ConversationDetails(props: {
|
|
47
|
-
brief
|
|
37
|
+
brief?: Brief;
|
|
48
38
|
conversationId: string;
|
|
39
|
+
/** True when a sidebar review chip opened details. */
|
|
40
|
+
highlightReview: boolean;
|
|
49
41
|
identity: ReactNode;
|
|
50
42
|
links: ConversationLink[];
|
|
51
43
|
onClose(): void;
|
|
52
44
|
/** Open one child agent transcript. A details sheet closes first. */
|
|
53
45
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
54
46
|
overview: ReactNode;
|
|
55
|
-
sentryConversationUrl?: string;
|
|
56
47
|
usage: ReactNode;
|
|
57
48
|
}) {
|
|
58
|
-
const [tab, setTab] = useState<DetailsTab>("summary");
|
|
59
49
|
const panel = useDetailsPanel();
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// People first, then run facts. Watches come before linked work: they say
|
|
66
|
-
// what can wake this agent later, so readers see them without another tab.
|
|
67
|
-
const sections = [
|
|
68
|
-
{ content: props.identity, key: "participants", title: "Participants" },
|
|
69
|
-
{ content: props.overview, key: "overview", title: undefined },
|
|
70
|
-
{
|
|
71
|
-
content: watches?.length ? (
|
|
72
|
-
<ConversationWatches watches={watches} />
|
|
73
|
-
) : null,
|
|
74
|
-
count: watches?.length,
|
|
75
|
-
key: "watching",
|
|
76
|
-
title: "Watching",
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
content: props.links.length ? (
|
|
80
|
-
<ConversationLinkedWork links={props.links} />
|
|
81
|
-
) : null,
|
|
82
|
-
count: props.links.length,
|
|
83
|
-
key: "linked-work",
|
|
84
|
-
title: "Linked work",
|
|
85
|
-
},
|
|
86
|
-
{ content: props.usage, key: "usage", title: "Usage" },
|
|
87
|
-
].filter((section) => section.content != null);
|
|
50
|
+
const memories = useConversationMemories(props.conversationId).data?.memories;
|
|
51
|
+
const agents = useConversationChildAgents(props.conversationId).data?.agents;
|
|
52
|
+
const watches =
|
|
53
|
+
useConversationWatches(props.conversationId).data?.watches ?? [];
|
|
88
54
|
|
|
89
55
|
return (
|
|
90
56
|
<DetailsPanel
|
|
@@ -92,126 +58,115 @@ export function ConversationDetails(props: {
|
|
|
92
58
|
title={
|
|
93
59
|
<h2
|
|
94
60
|
aria-label="Conversation details"
|
|
95
|
-
className="m-0
|
|
61
|
+
className="m-0 flex min-w-0 items-center gap-1.5 font-sans text-sm"
|
|
96
62
|
>
|
|
97
|
-
|
|
63
|
+
{props.brief ? (
|
|
64
|
+
<>
|
|
65
|
+
<span className="font-semibold text-dashboard-text">Brief</span>
|
|
66
|
+
<span aria-hidden="true" className="text-dashboard-text-faint">
|
|
67
|
+
·
|
|
68
|
+
</span>
|
|
69
|
+
<time
|
|
70
|
+
className="truncate font-normal text-dashboard-text-muted"
|
|
71
|
+
dateTime={props.brief.updatedAt}
|
|
72
|
+
title={formatTime(props.brief.updatedAt, {
|
|
73
|
+
dateStyle: "medium",
|
|
74
|
+
timeStyle: "short",
|
|
75
|
+
})}
|
|
76
|
+
>
|
|
77
|
+
Updated {formatRelativeTime(props.brief.updatedAt)}
|
|
78
|
+
</time>
|
|
79
|
+
</>
|
|
80
|
+
) : (
|
|
81
|
+
<span className="font-semibold text-dashboard-text">Details</span>
|
|
82
|
+
)}
|
|
98
83
|
</h2>
|
|
99
84
|
}
|
|
100
|
-
actions={
|
|
101
|
-
<>
|
|
102
|
-
<ShareConversationButton
|
|
103
|
-
key={props.conversationId}
|
|
104
|
-
conversationId={props.conversationId}
|
|
105
|
-
layout="icon"
|
|
106
|
-
/>
|
|
107
|
-
{props.sentryConversationUrl ? (
|
|
108
|
-
<Tooltip>
|
|
109
|
-
<TooltipTrigger
|
|
110
|
-
render={
|
|
111
|
-
<a
|
|
112
|
-
aria-label="Open in Sentry"
|
|
113
|
-
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"
|
|
114
|
-
href={props.sentryConversationUrl}
|
|
115
|
-
rel="noopener noreferrer"
|
|
116
|
-
target="_blank"
|
|
117
|
-
>
|
|
118
|
-
<SentryGlyph />
|
|
119
|
-
</a>
|
|
120
|
-
}
|
|
121
|
-
/>
|
|
122
|
-
<TooltipContent size="compact" side="bottom">
|
|
123
|
-
Open in Sentry
|
|
124
|
-
</TooltipContent>
|
|
125
|
-
</Tooltip>
|
|
126
|
-
) : null}
|
|
127
|
-
</>
|
|
128
|
-
}
|
|
129
85
|
onClose={props.onClose}
|
|
130
86
|
>
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
</Tabs>
|
|
87
|
+
<div className="grid min-w-0 content-start gap-3 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
|
|
88
|
+
{props.brief ? (
|
|
89
|
+
<div className="min-w-0 break-words px-1 pb-1 font-sans text-sm leading-relaxed text-dashboard-text-muted">
|
|
90
|
+
<ConversationBrief brief={props.brief} variant="summary" />
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
<ConversationReviewItems
|
|
94
|
+
conversationId={props.conversationId}
|
|
95
|
+
highlight={props.highlightReview}
|
|
96
|
+
/>
|
|
97
|
+
{props.links.length > 0 || watches.length > 0 ? (
|
|
98
|
+
// A Watch waits on something. It shows with linked work, so the
|
|
99
|
+
// work and what Junior waits on read as one list.
|
|
100
|
+
<DetailsCard
|
|
101
|
+
count={props.links.length || watches.length}
|
|
102
|
+
title={props.links.length > 0 ? "Linked work" : "Waiting on"}
|
|
103
|
+
>
|
|
104
|
+
{props.links.length > 0 ? (
|
|
105
|
+
<ConversationLinkedWork links={props.links} />
|
|
106
|
+
) : null}
|
|
107
|
+
{props.links.length > 0 && watches.length > 0 ? (
|
|
108
|
+
<h4 className="m-0 mb-1.5 mt-3 font-sans text-xs font-medium text-dashboard-text-faint">
|
|
109
|
+
Also waiting on
|
|
110
|
+
</h4>
|
|
111
|
+
) : null}
|
|
112
|
+
{watches.length > 0 ? (
|
|
113
|
+
<ConversationWatches watches={watches} />
|
|
114
|
+
) : null}
|
|
115
|
+
</DetailsCard>
|
|
116
|
+
) : null}
|
|
117
|
+
{agents?.length ? (
|
|
118
|
+
<DetailsCard count={agents.length} title="Subagents">
|
|
119
|
+
<ConversationChildAgents
|
|
120
|
+
conversationId={props.conversationId}
|
|
121
|
+
onOpenAgent={(target) => {
|
|
122
|
+
// The panel stays beside the drawer. A modal sheet cannot.
|
|
123
|
+
if (!panel) props.onClose();
|
|
124
|
+
props.onOpenSubagent(target);
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</DetailsCard>
|
|
128
|
+
) : null}
|
|
129
|
+
{props.overview != null ? (
|
|
130
|
+
<DetailsCard>{props.overview}</DetailsCard>
|
|
131
|
+
) : null}
|
|
132
|
+
{props.usage != null ? (
|
|
133
|
+
<DetailsCard title="Usage">{props.usage}</DetailsCard>
|
|
134
|
+
) : null}
|
|
135
|
+
{props.identity != null ? (
|
|
136
|
+
<DetailsCard title="Participants">{props.identity}</DetailsCard>
|
|
137
|
+
) : null}
|
|
138
|
+
{memories?.length ? (
|
|
139
|
+
<DetailsCard count={memories.length} title="Memories">
|
|
140
|
+
<Disclosure label="memories">
|
|
141
|
+
<ConversationMemories conversationId={props.conversationId} />
|
|
142
|
+
</Disclosure>
|
|
143
|
+
</DetailsCard>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
191
146
|
</DetailsPanel>
|
|
192
147
|
);
|
|
193
148
|
}
|
|
194
149
|
|
|
195
|
-
/**
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
return (
|
|
199
|
-
<span className="min-w-5 shrink-0 rounded-full bg-dashboard-fill-strong px-1.5 text-center text-xs font-semibold leading-5 tabular-nums">
|
|
200
|
-
{props.count.toLocaleString("en-US")}
|
|
201
|
-
</span>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Draw the Sentry glyph in the current text color, sized like a header icon. */
|
|
206
|
-
function SentryGlyph() {
|
|
150
|
+
/** Hide long, rarely read content behind one show/hide row. */
|
|
151
|
+
function Disclosure(props: { children: ReactNode; label: string }) {
|
|
152
|
+
const [open, setOpen] = useState(false);
|
|
207
153
|
return (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
154
|
+
<div className="grid min-w-0 gap-2">
|
|
155
|
+
<button
|
|
156
|
+
aria-expanded={open}
|
|
157
|
+
className="group -mx-1.5 -my-1 flex min-h-8 items-center gap-2.5 rounded-md px-1.5 py-1.5 text-left text-xs font-medium leading-4 text-dashboard-text-muted transition-colors hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
158
|
+
onClick={() => setOpen((value) => !value)}
|
|
159
|
+
type="button"
|
|
160
|
+
>
|
|
161
|
+
<span className="grid size-5 shrink-0 place-items-center">
|
|
162
|
+
<ChevronRight
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
className={dashboardDisclosureChevronClass}
|
|
165
|
+
/>
|
|
166
|
+
</span>
|
|
167
|
+
{open ? `Hide ${props.label}` : `Show ${props.label}`}
|
|
168
|
+
</button>
|
|
169
|
+
{open ? props.children : null}
|
|
170
|
+
</div>
|
|
216
171
|
);
|
|
217
172
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
isValidElement,
|
|
4
|
-
useEffect,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type ReactElement,
|
|
8
|
-
type ReactNode,
|
|
9
|
-
} from "react";
|
|
10
|
-
import { Ellipsis, Menu } from "lucide-react";
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { Menu, PanelRight, ScrollText, X } from "lucide-react";
|
|
11
3
|
|
|
12
4
|
import { SearchInput } from "../components/SearchInput";
|
|
13
5
|
import { Button } from "../components/ui/button";
|
|
@@ -20,15 +12,21 @@ import {
|
|
|
20
12
|
import {
|
|
21
13
|
ConversationHeaderActions,
|
|
22
14
|
HeaderIconButton,
|
|
23
|
-
|
|
15
|
+
TranscriptSearchToggle,
|
|
24
16
|
type ConversationArchiveAction,
|
|
25
17
|
} from "./ConversationHeaderActions";
|
|
18
|
+
import { ConversationOverflowMenu } from "./ConversationOverflowMenu";
|
|
19
|
+
import { ShareConversationButton } from "./ShareConversationButton";
|
|
26
20
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
27
21
|
|
|
28
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Show the conversation title bar: where it runs, the title, and actions.
|
|
24
|
+
* Run facts, linked work, and the Brief live in Conversation details.
|
|
25
|
+
*/
|
|
29
26
|
export function ConversationHeader(props: {
|
|
30
27
|
/** Parent chain of a child Conversation. Root Conversations omit it. */
|
|
31
28
|
agentTree?: ReactNode;
|
|
29
|
+
/** A `menu` layout copy action for the `⋯` menu. */
|
|
32
30
|
copyAction?: ReactNode;
|
|
33
31
|
archive: ConversationArchiveAction;
|
|
34
32
|
conversationId: string;
|
|
@@ -36,13 +34,16 @@ export function ConversationHeader(props: {
|
|
|
36
34
|
onDetailsClick(): void;
|
|
37
35
|
/** Links to the fork source and to forks of this conversation. */
|
|
38
36
|
forks?: ReactNode;
|
|
37
|
+
/** Mobile only. Desktop shows linked work in Conversation details. */
|
|
39
38
|
linkedWork?: ReactNode;
|
|
40
39
|
live: boolean;
|
|
41
|
-
|
|
40
|
+
/** Where the conversation runs, such as a Slack channel. */
|
|
41
|
+
location?: ReactNode;
|
|
42
42
|
onSearchChange(value: string): void;
|
|
43
43
|
onViewChange(value: TranscriptViewMode): void;
|
|
44
44
|
privacy: ReactNode;
|
|
45
45
|
search: string;
|
|
46
|
+
sentryConversationUrl?: string;
|
|
46
47
|
title: string;
|
|
47
48
|
view: TranscriptViewMode;
|
|
48
49
|
}) {
|
|
@@ -51,9 +52,7 @@ export function ConversationHeader(props: {
|
|
|
51
52
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
52
53
|
const wasDetailsOpen = useRef(detailsOpen);
|
|
53
54
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
54
|
-
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
55
55
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
56
|
-
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
|
57
56
|
const searchOpenVisible =
|
|
58
57
|
props.view === "rich" && (searchOpen || props.search.length > 0);
|
|
59
58
|
const openMobileNavigation = useOpenMobileNavigation();
|
|
@@ -77,27 +76,6 @@ export function ConversationHeader(props: {
|
|
|
77
76
|
searchInputRef.current?.focus();
|
|
78
77
|
}, [searchOpenVisible]);
|
|
79
78
|
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (!mobileMenuOpen) return;
|
|
82
|
-
|
|
83
|
-
function closeOnOutsideClick(event: PointerEvent) {
|
|
84
|
-
if (!mobileMenuRef.current?.contains(event.target as Node)) {
|
|
85
|
-
setMobileMenuOpen(false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function closeOnEscape(event: KeyboardEvent) {
|
|
90
|
-
if (event.key === "Escape") setMobileMenuOpen(false);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
document.addEventListener("pointerdown", closeOnOutsideClick);
|
|
94
|
-
document.addEventListener("keydown", closeOnEscape);
|
|
95
|
-
return () => {
|
|
96
|
-
document.removeEventListener("pointerdown", closeOnOutsideClick);
|
|
97
|
-
document.removeEventListener("keydown", closeOnEscape);
|
|
98
|
-
};
|
|
99
|
-
}, [mobileMenuOpen]);
|
|
100
|
-
|
|
101
79
|
const toggleSearch = () => {
|
|
102
80
|
if (searchOpenVisible) {
|
|
103
81
|
setSearchOpen(false);
|
|
@@ -107,64 +85,29 @@ export function ConversationHeader(props: {
|
|
|
107
85
|
setSearchOpen(true);
|
|
108
86
|
};
|
|
109
87
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
conversationId={props.conversationId}
|
|
134
|
-
copyAction={menuCopyAction}
|
|
135
|
-
detailsOpen={detailsOpen}
|
|
136
|
-
layout="menu"
|
|
137
|
-
onDetailsClick={() => {
|
|
138
|
-
setMobileMenuOpen(false);
|
|
139
|
-
props.onDetailsClick();
|
|
140
|
-
}}
|
|
141
|
-
onSearchClick={() => {
|
|
142
|
-
setMobileMenuOpen(false);
|
|
143
|
-
toggleSearch();
|
|
144
|
-
}}
|
|
145
|
-
onViewChange={(value) => {
|
|
146
|
-
setMobileMenuOpen(false);
|
|
147
|
-
props.onViewChange(value);
|
|
148
|
-
}}
|
|
149
|
-
searchOpen={searchOpenVisible}
|
|
150
|
-
view={props.view}
|
|
151
|
-
/>
|
|
152
|
-
{openMobileNavigation ? (
|
|
153
|
-
<Button
|
|
154
|
-
className="mt-0.5"
|
|
155
|
-
onClick={() => {
|
|
156
|
-
setMobileMenuOpen(false);
|
|
157
|
-
openMobileNavigation();
|
|
158
|
-
}}
|
|
159
|
-
size="menu"
|
|
160
|
-
>
|
|
161
|
-
<Menu aria-hidden="true" size={16} strokeWidth={2} />
|
|
162
|
-
App menu
|
|
163
|
-
</Button>
|
|
164
|
-
) : null}
|
|
165
|
-
</div>
|
|
166
|
-
) : null}
|
|
167
|
-
</div>
|
|
88
|
+
const menuActions = (placement: "desktop" | "mobile", close: () => void) => (
|
|
89
|
+
<ConversationHeaderActions
|
|
90
|
+
archive={props.archive}
|
|
91
|
+
conversationId={props.conversationId}
|
|
92
|
+
copyAction={props.copyAction}
|
|
93
|
+
detailsOpen={detailsOpen}
|
|
94
|
+
onDetailsClick={() => {
|
|
95
|
+
close();
|
|
96
|
+
props.onDetailsClick();
|
|
97
|
+
}}
|
|
98
|
+
onSearchClick={() => {
|
|
99
|
+
close();
|
|
100
|
+
toggleSearch();
|
|
101
|
+
}}
|
|
102
|
+
onViewChange={(value) => {
|
|
103
|
+
close();
|
|
104
|
+
props.onViewChange(value);
|
|
105
|
+
}}
|
|
106
|
+
placement={placement}
|
|
107
|
+
searchOpen={searchOpenVisible}
|
|
108
|
+
sentryConversationUrl={props.sentryConversationUrl}
|
|
109
|
+
view={props.view}
|
|
110
|
+
/>
|
|
168
111
|
);
|
|
169
112
|
|
|
170
113
|
const showMobileHeader =
|
|
@@ -184,48 +127,98 @@ export function ConversationHeader(props: {
|
|
|
184
127
|
|
|
185
128
|
return (
|
|
186
129
|
<>
|
|
187
|
-
<MobileHeaderActionsPortal>
|
|
130
|
+
<MobileHeaderActionsPortal>
|
|
131
|
+
<ConversationOverflowMenu buttonRef={menuButtonRef}>
|
|
132
|
+
{(close) => (
|
|
133
|
+
<>
|
|
134
|
+
{menuActions("mobile", close)}
|
|
135
|
+
{openMobileNavigation ? (
|
|
136
|
+
<Button
|
|
137
|
+
className="mt-0.5"
|
|
138
|
+
onClick={() => {
|
|
139
|
+
close();
|
|
140
|
+
openMobileNavigation();
|
|
141
|
+
}}
|
|
142
|
+
size="menu"
|
|
143
|
+
>
|
|
144
|
+
<Menu aria-hidden="true" size={16} strokeWidth={2} />
|
|
145
|
+
App menu
|
|
146
|
+
</Button>
|
|
147
|
+
) : null}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</ConversationOverflowMenu>
|
|
151
|
+
</MobileHeaderActionsPortal>
|
|
188
152
|
<MobileHeaderLivePortal>{liveIndicator}</MobileHeaderLivePortal>
|
|
153
|
+
{/* Desktop: above the event log's sticky toolbar, so the `⋯` menu
|
|
154
|
+
covers it. Mobile: below the app header, which holds the menu. */}
|
|
189
155
|
<header
|
|
190
156
|
className={cn(
|
|
191
|
-
"relative z-10 border-b border-dashboard-border bg-dashboard-bg",
|
|
157
|
+
"relative z-10 border-b md:z-30 border-dashboard-border bg-dashboard-bg",
|
|
192
158
|
!showMobileHeader && "hidden md:block",
|
|
193
159
|
)}
|
|
194
160
|
>
|
|
195
|
-
<div className="hidden min-w-0 items-
|
|
196
|
-
<div className="min-w-0">
|
|
161
|
+
<div className="hidden h-12 min-w-0 items-center justify-between gap-4 pl-7 pr-4 md:flex">
|
|
162
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
197
163
|
{props.agentTree ? (
|
|
198
|
-
<div className="
|
|
164
|
+
<div className="min-w-0 shrink-[2]">{props.agentTree}</div>
|
|
165
|
+
) : props.location ? (
|
|
166
|
+
<span className="shrink-0 text-sm text-dashboard-text-muted">
|
|
167
|
+
{props.location}
|
|
168
|
+
</span>
|
|
199
169
|
) : null}
|
|
200
|
-
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
</
|
|
204
|
-
{liveIndicator}
|
|
205
|
-
<span className="shrink-0">{props.privacy}</span>
|
|
206
|
-
</div>
|
|
207
|
-
{props.meta ? (
|
|
208
|
-
<div className="mt-2 min-w-0 text-xs leading-snug text-dashboard-text-muted">
|
|
209
|
-
{props.meta}
|
|
210
|
-
</div>
|
|
211
|
-
) : null}
|
|
212
|
-
{props.forks ? (
|
|
213
|
-
<div className="mt-1.5 min-w-0">{props.forks}</div>
|
|
170
|
+
{props.agentTree || props.location ? (
|
|
171
|
+
<span aria-hidden="true" className="text-dashboard-text-faint">
|
|
172
|
+
›
|
|
173
|
+
</span>
|
|
214
174
|
) : null}
|
|
175
|
+
<h2 className="m-0 line-clamp-1 min-w-0 font-display text-base font-medium leading-tight tracking-tight">
|
|
176
|
+
{props.title}
|
|
177
|
+
</h2>
|
|
178
|
+
{liveIndicator}
|
|
179
|
+
<span className="shrink-0">{props.privacy}</span>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
182
|
+
{props.view === "raw" ? (
|
|
183
|
+
// Event log is a debug view reached from the menu. Say which
|
|
184
|
+
// view this is, and give one click back.
|
|
185
|
+
<Button
|
|
186
|
+
aria-label="Close event log"
|
|
187
|
+
className="mr-1 h-8 px-2.5"
|
|
188
|
+
onClick={() => props.onViewChange("rich")}
|
|
189
|
+
variant="quiet"
|
|
190
|
+
>
|
|
191
|
+
<ScrollText aria-hidden="true" size={14} strokeWidth={2} />
|
|
192
|
+
Event log
|
|
193
|
+
<X aria-hidden="true" size={14} strokeWidth={2} />
|
|
194
|
+
</Button>
|
|
195
|
+
) : (
|
|
196
|
+
<TranscriptSearchToggle
|
|
197
|
+
onClick={toggleSearch}
|
|
198
|
+
open={searchOpenVisible}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
<ShareConversationButton
|
|
202
|
+
key={props.conversationId}
|
|
203
|
+
conversationId={props.conversationId}
|
|
204
|
+
layout="icon"
|
|
205
|
+
/>
|
|
206
|
+
<ConversationOverflowMenu>
|
|
207
|
+
{(close) => menuActions("desktop", close)}
|
|
208
|
+
</ConversationOverflowMenu>
|
|
209
|
+
<HeaderIconButton
|
|
210
|
+
ref={detailsButtonRef}
|
|
211
|
+
label="Conversation details"
|
|
212
|
+
onClick={props.onDetailsClick}
|
|
213
|
+
pressed={detailsOpen}
|
|
214
|
+
>
|
|
215
|
+
<PanelRight aria-hidden="true" size={15} strokeWidth={2} />
|
|
216
|
+
</HeaderIconButton>
|
|
215
217
|
</div>
|
|
216
|
-
<ConversationHeaderActions
|
|
217
|
-
archive={props.archive}
|
|
218
|
-
conversationId={props.conversationId}
|
|
219
|
-
copyAction={props.copyAction}
|
|
220
|
-
detailsOpen={detailsOpen}
|
|
221
|
-
detailsButtonRef={detailsButtonRef}
|
|
222
|
-
onDetailsClick={props.onDetailsClick}
|
|
223
|
-
onSearchClick={toggleSearch}
|
|
224
|
-
onViewChange={props.onViewChange}
|
|
225
|
-
searchOpen={searchOpenVisible}
|
|
226
|
-
view={props.view}
|
|
227
|
-
/>
|
|
228
218
|
</div>
|
|
219
|
+
{props.forks ? (
|
|
220
|
+
<div className="hidden px-7 pb-2.5 md:block">{props.forks}</div>
|
|
221
|
+
) : null}
|
|
229
222
|
{props.agentTree ? (
|
|
230
223
|
<div className="px-4 py-2 md:hidden">{props.agentTree}</div>
|
|
231
224
|
) : null}
|
|
@@ -235,18 +228,11 @@ export function ConversationHeader(props: {
|
|
|
235
228
|
{props.linkedWork ? (
|
|
236
229
|
<div
|
|
237
230
|
aria-label="Linked work"
|
|
238
|
-
className="border-t border-dashboard-border-subtle px-4 py-2.5 md:
|
|
231
|
+
className="border-t border-dashboard-border-subtle px-4 py-2.5 md:hidden"
|
|
239
232
|
>
|
|
240
233
|
{props.linkedWork}
|
|
241
234
|
</div>
|
|
242
235
|
) : null}
|
|
243
|
-
<div className="hidden border-t border-dashboard-border-subtle px-7 md:flex">
|
|
244
|
-
<TranscriptViewToggle
|
|
245
|
-
labelled
|
|
246
|
-
onChange={props.onViewChange}
|
|
247
|
-
value={props.view}
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
236
|
{searchOpenVisible ? (
|
|
251
237
|
<div className="px-4 py-3 md:px-7">
|
|
252
238
|
<SearchInput
|