@sentry/junior-dashboard 0.260.0 → 0.262.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 +96 -74
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +7 -6
- package/src/client/components/BottomSheet.tsx +32 -0
- package/src/client/components/Button.tsx +2 -1
- package/src/client/components/Drawer.tsx +23 -109
- package/src/client/components/README.md +14 -4
- package/src/client/components/SegmentedTabs.tsx +23 -4
- package/src/client/components/Select.tsx +41 -7
- package/src/client/components/Sheet.LICENSE +21 -0
- package/src/client/components/Sheet.tsx +141 -0
- package/src/client/components/controls/DirectorySortSelect.tsx +6 -4
- package/src/client/conversations/{ConversationDetailsDrawer.tsx → ConversationDetails.tsx} +19 -38
- package/src/client/conversations/ConversationDetailsFrame.tsx +91 -0
- package/src/client/conversations/ConversationEventLog.tsx +0 -1
- package/src/client/conversations/ConversationHeader.tsx +20 -28
- package/src/client/conversations/ConversationHeaderActions.tsx +5 -1
- package/src/client/conversations/ConversationPage.tsx +49 -36
- package/src/client/conversations/ConversationWorkspace.tsx +9 -12
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -4
- package/src/client/conversations/ForkConversationButton.tsx +0 -1
- package/src/client/conversations/README.md +18 -2
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +0 -1
- package/src/client/conversations/TelemetryMetrics.tsx +2 -2
- package/src/client/conversations/TranscriptSubagentView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +0 -1
- package/src/client/debouncedValue.ts +14 -0
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -1
- package/src/client/pages/automations/AutomationFilters.tsx +4 -3
- package/src/client/pages/code/RepositoryTabs.tsx +3 -2
- package/src/client/pages/dev/ComponentsPage.tsx +20 -2
- package/src/client/pages/dev/SegmentedFixtures.tsx +8 -7
- package/src/client/pages/dev/SelectFixtures.tsx +62 -0
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -1
- package/LICENSE +0 -201
|
@@ -3,7 +3,7 @@ import { useState, type ReactNode } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { formatRelativeTime, formatTime } from "../format";
|
|
5
5
|
import { ButtonLink } from "../components/Button";
|
|
6
|
-
import {
|
|
6
|
+
import { ConversationDetailsFrame } from "./ConversationDetailsFrame";
|
|
7
7
|
import { SegmentedTabs } from "../components/SegmentedTabs";
|
|
8
8
|
import {
|
|
9
9
|
ConversationChildAgents,
|
|
@@ -16,29 +16,27 @@ import {
|
|
|
16
16
|
import { ShareConversationButton } from "./ShareConversationButton";
|
|
17
17
|
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
18
18
|
|
|
19
|
-
type DetailsTab = "agents" | "
|
|
19
|
+
type DetailsTab = "agents" | "summary" | "memories";
|
|
20
20
|
|
|
21
21
|
/** Put conversation context before usage and diagnostic links. */
|
|
22
|
-
export function
|
|
22
|
+
export function ConversationDetails(props: {
|
|
23
23
|
annotations: ReactNode;
|
|
24
24
|
brief: ReactNode;
|
|
25
25
|
conversationId: string;
|
|
26
26
|
identity: ReactNode;
|
|
27
27
|
lastActivityAt?: string;
|
|
28
28
|
onClose(): void;
|
|
29
|
-
/** Open one child agent transcript.
|
|
29
|
+
/** Open one child agent transcript. Details closes first. */
|
|
30
30
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
31
31
|
sentryConversationUrl?: string;
|
|
32
|
-
privacy: ReactNode;
|
|
33
32
|
stats: ReactNode;
|
|
34
|
-
title: string;
|
|
35
33
|
}) {
|
|
36
|
-
const [tab, setTab] = useState<DetailsTab>("
|
|
37
|
-
// Load both lists with
|
|
34
|
+
const [tab, setTab] = useState<DetailsTab>("summary");
|
|
35
|
+
// Load both lists with details so each tab can show its count.
|
|
38
36
|
const memories = useConversationMemories(props.conversationId);
|
|
39
37
|
const agents = useConversationChildAgents(props.conversationId);
|
|
40
38
|
const tabs = [
|
|
41
|
-
{ label: "
|
|
39
|
+
{ label: "Summary", value: "summary" },
|
|
42
40
|
{
|
|
43
41
|
count: memories.data?.memories.length,
|
|
44
42
|
label: "Memories",
|
|
@@ -46,37 +44,18 @@ export function ConversationDetailsDrawer(props: {
|
|
|
46
44
|
},
|
|
47
45
|
{ count: agents.data?.agents.length, label: "Subagents", value: "agents" },
|
|
48
46
|
] as const;
|
|
49
|
-
|
|
47
|
+
|
|
50
48
|
const sections = [
|
|
51
|
-
{ content: props.brief, title:
|
|
49
|
+
{ content: props.brief, title: undefined },
|
|
52
50
|
{ content: props.annotations, title: "Linked work" },
|
|
53
51
|
{ content: props.identity, title: "Participants" },
|
|
54
52
|
{ content: props.stats, title: "Usage" },
|
|
55
53
|
].filter((section) => section.content != null);
|
|
56
54
|
|
|
57
55
|
return (
|
|
58
|
-
<
|
|
59
|
-
closeLabel="Close conversation details"
|
|
60
|
-
dismissLabel="Dismiss conversation details"
|
|
61
|
-
header={
|
|
62
|
-
<>
|
|
63
|
-
<h2
|
|
64
|
-
className="m-0 min-w-0 break-words text-lg font-bold leading-tight text-dashboard-text"
|
|
65
|
-
id={titleId}
|
|
66
|
-
>
|
|
67
|
-
{props.title}
|
|
68
|
-
</h2>
|
|
69
|
-
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
70
|
-
{props.privacy}
|
|
71
|
-
</div>
|
|
72
|
-
</>
|
|
73
|
-
}
|
|
74
|
-
onClose={props.onClose}
|
|
75
|
-
openKey={props.conversationId}
|
|
76
|
-
titleId={titleId}
|
|
77
|
-
width="narrow"
|
|
78
|
-
>
|
|
56
|
+
<ConversationDetailsFrame onClose={props.onClose}>
|
|
79
57
|
<SegmentedTabs
|
|
58
|
+
scroll
|
|
80
59
|
items={tabs}
|
|
81
60
|
label="Conversation details"
|
|
82
61
|
onChange={setTab}
|
|
@@ -97,11 +76,13 @@ export function ConversationDetailsDrawer(props: {
|
|
|
97
76
|
{sections.map((section) => (
|
|
98
77
|
<section
|
|
99
78
|
className="grid min-w-0 gap-3 border-b border-dashboard-border pb-5 last:border-0 last:pb-0"
|
|
100
|
-
key={section.title}
|
|
79
|
+
key={section.title ?? "summary"}
|
|
101
80
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
81
|
+
{section.title ? (
|
|
82
|
+
<h3 className="m-0 text-sm font-semibold text-dashboard-text">
|
|
83
|
+
{section.title}
|
|
84
|
+
</h3>
|
|
85
|
+
) : null}
|
|
105
86
|
<div className="min-w-0 break-words font-sans text-sm leading-relaxed text-dashboard-text-muted">
|
|
106
87
|
{section.content}
|
|
107
88
|
</div>
|
|
@@ -113,7 +94,7 @@ export function ConversationDetailsDrawer(props: {
|
|
|
113
94
|
No additional conversation details.
|
|
114
95
|
</p>
|
|
115
96
|
)}
|
|
116
|
-
{tab === "
|
|
97
|
+
{tab === "summary" ? (
|
|
117
98
|
<footer className="mt-5 grid gap-6 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
|
|
118
99
|
{props.lastActivityAt ? (
|
|
119
100
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
@@ -152,6 +133,6 @@ export function ConversationDetailsDrawer(props: {
|
|
|
152
133
|
</footer>
|
|
153
134
|
) : null}
|
|
154
135
|
</SegmentedTabs>
|
|
155
|
-
</
|
|
136
|
+
</ConversationDetailsFrame>
|
|
156
137
|
);
|
|
157
138
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useId,
|
|
5
|
+
useRef,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import { Button } from "../components/Button";
|
|
11
|
+
import { BottomSheet } from "../components/BottomSheet";
|
|
12
|
+
|
|
13
|
+
const panelMediaQuery = "(min-width: 1280px)";
|
|
14
|
+
|
|
15
|
+
function subscribeToPanelLayout(onChange: () => void) {
|
|
16
|
+
const media = window.matchMedia(panelMediaQuery);
|
|
17
|
+
media.addEventListener("change", onChange);
|
|
18
|
+
return () => media.removeEventListener("change", onChange);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasPanelSpace() {
|
|
22
|
+
return window.matchMedia(panelMediaQuery).matches;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Keep details beside the chat when space permits, or in a modal bottom sheet. */
|
|
26
|
+
export function ConversationDetailsFrame(props: {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
onClose(): void;
|
|
29
|
+
}) {
|
|
30
|
+
const titleId = useId();
|
|
31
|
+
const panel = useSyncExternalStore(
|
|
32
|
+
subscribeToPanelLayout,
|
|
33
|
+
hasPanelSpace,
|
|
34
|
+
() => false,
|
|
35
|
+
);
|
|
36
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
37
|
+
const wasPanel = useRef(panel);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (
|
|
40
|
+
panel &&
|
|
41
|
+
!wasPanel.current &&
|
|
42
|
+
!document.querySelector("[data-dashboard-modal]")
|
|
43
|
+
) {
|
|
44
|
+
closeRef.current?.focus({ preventScroll: true });
|
|
45
|
+
}
|
|
46
|
+
wasPanel.current = panel;
|
|
47
|
+
}, [panel]);
|
|
48
|
+
if (!panel) {
|
|
49
|
+
return (
|
|
50
|
+
<BottomSheet label="Conversation details" onClose={props.onClose}>
|
|
51
|
+
{props.children}
|
|
52
|
+
</BottomSheet>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<aside
|
|
58
|
+
aria-labelledby={titleId}
|
|
59
|
+
className="grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] border-l border-dashboard-border bg-dashboard-bg-elevated"
|
|
60
|
+
onKeyDown={(event) => {
|
|
61
|
+
if (
|
|
62
|
+
event.key === "Escape" &&
|
|
63
|
+
!event.defaultPrevented &&
|
|
64
|
+
!document.querySelector("[data-dashboard-modal]")
|
|
65
|
+
) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
props.onClose();
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<header className="flex items-center justify-between gap-3 border-b border-dashboard-border px-5 py-3">
|
|
72
|
+
<h2
|
|
73
|
+
aria-label="Conversation details"
|
|
74
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
75
|
+
id={titleId}
|
|
76
|
+
>
|
|
77
|
+
Details
|
|
78
|
+
</h2>
|
|
79
|
+
<Button
|
|
80
|
+
ref={closeRef}
|
|
81
|
+
aria-label="Close conversation details"
|
|
82
|
+
onClick={props.onClose}
|
|
83
|
+
size="icon"
|
|
84
|
+
>
|
|
85
|
+
<X aria-hidden="true" size={15} />
|
|
86
|
+
</Button>
|
|
87
|
+
</header>
|
|
88
|
+
<div className="min-h-0">{props.children}</div>
|
|
89
|
+
</aside>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -22,37 +22,33 @@ import {
|
|
|
22
22
|
TranscriptViewToggle,
|
|
23
23
|
type ConversationArchiveAction,
|
|
24
24
|
} from "./ConversationHeaderActions";
|
|
25
|
-
import { ConversationDetailsDrawer } from "./ConversationDetailsDrawer";
|
|
26
25
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
27
|
-
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
28
26
|
|
|
29
27
|
/** Show the conversation title, search, and details. */
|
|
30
28
|
export function ConversationHeader(props: {
|
|
31
29
|
/** Parent chain of a child Conversation. Root Conversations omit it. */
|
|
32
30
|
agentTree?: ReactNode;
|
|
33
31
|
copyAction?: ReactNode;
|
|
34
|
-
annotations: ReactNode;
|
|
35
32
|
archive: ConversationArchiveAction;
|
|
36
|
-
brief?: ReactNode;
|
|
37
33
|
conversationId: string;
|
|
34
|
+
detailsOpen: boolean;
|
|
35
|
+
onDetailsClick(): void;
|
|
38
36
|
/** Links to the fork source and to forks of this conversation. */
|
|
39
37
|
forks?: ReactNode;
|
|
40
|
-
identity: ReactNode;
|
|
41
38
|
linkedWork?: ReactNode;
|
|
42
|
-
lastActivityAt?: string;
|
|
43
|
-
sentryConversationUrl?: string;
|
|
44
39
|
live: boolean;
|
|
45
40
|
meta?: ReactNode;
|
|
46
|
-
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
47
41
|
onSearchChange(value: string): void;
|
|
48
42
|
onViewChange(value: TranscriptViewMode): void;
|
|
49
43
|
privacy: ReactNode;
|
|
50
44
|
search: string;
|
|
51
|
-
stats: ReactNode;
|
|
52
45
|
title: string;
|
|
53
46
|
view: TranscriptViewMode;
|
|
54
47
|
}) {
|
|
55
|
-
const
|
|
48
|
+
const detailsOpen = props.detailsOpen;
|
|
49
|
+
const detailsButtonRef = useRef<HTMLButtonElement>(null);
|
|
50
|
+
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
51
|
+
const wasDetailsOpen = useRef(detailsOpen);
|
|
56
52
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
57
53
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
58
54
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -60,6 +56,16 @@ export function ConversationHeader(props: {
|
|
|
60
56
|
const searchOpenVisible = searchOpen || props.search.length > 0;
|
|
61
57
|
const openMobileNavigation = useOpenMobileNavigation();
|
|
62
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (wasDetailsOpen.current && !detailsOpen) {
|
|
61
|
+
const trigger = window.matchMedia("(max-width: 767px)").matches
|
|
62
|
+
? menuButtonRef.current
|
|
63
|
+
: detailsButtonRef.current;
|
|
64
|
+
trigger?.focus({ preventScroll: true });
|
|
65
|
+
}
|
|
66
|
+
wasDetailsOpen.current = detailsOpen;
|
|
67
|
+
}, [detailsOpen]);
|
|
68
|
+
|
|
63
69
|
useEffect(() => {
|
|
64
70
|
if (!searchOpenVisible) return;
|
|
65
71
|
searchInputRef.current?.focus();
|
|
@@ -107,6 +113,7 @@ export function ConversationHeader(props: {
|
|
|
107
113
|
const mobileOverflow = (
|
|
108
114
|
<div className="relative" ref={mobileMenuRef}>
|
|
109
115
|
<HeaderIconButton
|
|
116
|
+
ref={menuButtonRef}
|
|
110
117
|
label={mobileMenuOpen ? "Close conversation menu" : "Conversation menu"}
|
|
111
118
|
onClick={() => setMobileMenuOpen((open) => !open)}
|
|
112
119
|
pressed={mobileMenuOpen}
|
|
@@ -123,7 +130,7 @@ export function ConversationHeader(props: {
|
|
|
123
130
|
layout="menu"
|
|
124
131
|
onDetailsClick={() => {
|
|
125
132
|
setMobileMenuOpen(false);
|
|
126
|
-
|
|
133
|
+
props.onDetailsClick();
|
|
127
134
|
}}
|
|
128
135
|
onSearchClick={() => {
|
|
129
136
|
setMobileMenuOpen(false);
|
|
@@ -205,7 +212,8 @@ export function ConversationHeader(props: {
|
|
|
205
212
|
conversationId={props.conversationId}
|
|
206
213
|
copyAction={props.copyAction}
|
|
207
214
|
detailsOpen={detailsOpen}
|
|
208
|
-
|
|
215
|
+
detailsButtonRef={detailsButtonRef}
|
|
216
|
+
onDetailsClick={props.onDetailsClick}
|
|
209
217
|
onSearchClick={toggleSearch}
|
|
210
218
|
onViewChange={props.onViewChange}
|
|
211
219
|
searchOpen={searchOpenVisible}
|
|
@@ -252,22 +260,6 @@ export function ConversationHeader(props: {
|
|
|
252
260
|
</div>
|
|
253
261
|
) : null}
|
|
254
262
|
</header>
|
|
255
|
-
|
|
256
|
-
{detailsOpen ? (
|
|
257
|
-
<ConversationDetailsDrawer
|
|
258
|
-
annotations={props.annotations}
|
|
259
|
-
brief={props.brief}
|
|
260
|
-
conversationId={props.conversationId}
|
|
261
|
-
identity={props.identity}
|
|
262
|
-
lastActivityAt={props.lastActivityAt}
|
|
263
|
-
sentryConversationUrl={props.sentryConversationUrl}
|
|
264
|
-
onClose={() => setDetailsOpen(false)}
|
|
265
|
-
onOpenSubagent={props.onOpenSubagent}
|
|
266
|
-
privacy={props.privacy}
|
|
267
|
-
stats={props.stats}
|
|
268
|
-
title={props.title}
|
|
269
|
-
/>
|
|
270
|
-
) : null}
|
|
271
263
|
</>
|
|
272
264
|
);
|
|
273
265
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import type { ReactNode, Ref } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Archive,
|
|
4
4
|
ArchiveRestore,
|
|
@@ -30,6 +30,7 @@ export function ConversationHeaderActions(props: {
|
|
|
30
30
|
conversationId: string;
|
|
31
31
|
copyAction?: ReactNode;
|
|
32
32
|
detailsOpen: boolean;
|
|
33
|
+
detailsButtonRef?: Ref<HTMLButtonElement>;
|
|
33
34
|
/** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
|
|
34
35
|
layout?: "bar" | "menu";
|
|
35
36
|
onDetailsClick(): void;
|
|
@@ -94,6 +95,7 @@ export function ConversationHeaderActions(props: {
|
|
|
94
95
|
{props.copyAction}
|
|
95
96
|
<ArchiveConversationButton {...props.archive} layout="bar" />
|
|
96
97
|
<Button
|
|
98
|
+
ref={props.detailsButtonRef}
|
|
97
99
|
aria-label="Conversation details"
|
|
98
100
|
aria-pressed={props.detailsOpen}
|
|
99
101
|
className="ml-2"
|
|
@@ -180,6 +182,7 @@ function ViewModeButton(props: {
|
|
|
180
182
|
/** Shared icon control used by conversation and subagent transcript headers. */
|
|
181
183
|
export function HeaderIconButton(props: {
|
|
182
184
|
children: ReactNode;
|
|
185
|
+
ref?: Ref<HTMLButtonElement>;
|
|
183
186
|
label: string;
|
|
184
187
|
onClick(): void;
|
|
185
188
|
pressed?: boolean;
|
|
@@ -187,6 +190,7 @@ export function HeaderIconButton(props: {
|
|
|
187
190
|
return (
|
|
188
191
|
<IconButtonTooltip label={props.label}>
|
|
189
192
|
<Button
|
|
193
|
+
ref={props.ref}
|
|
190
194
|
aria-label={props.label}
|
|
191
195
|
aria-pressed={props.pressed}
|
|
192
196
|
className={cn(
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
} from "./conversationRoutes";
|
|
27
27
|
import { ConversationComposer } from "./ConversationComposer";
|
|
28
28
|
import { ConversationHeader } from "./ConversationHeader";
|
|
29
|
+
import { ConversationDetails } from "./ConversationDetails";
|
|
30
|
+
import { cn } from "../styles";
|
|
29
31
|
import { ConversationForkLinks } from "./ConversationForkLinks";
|
|
30
32
|
import { ConversationAgentTree } from "./ConversationAgentTree";
|
|
31
33
|
import { ConversationHeaderMeta } from "./ConversationHeaderMeta";
|
|
@@ -76,6 +78,7 @@ export function ConversationPage(props: {
|
|
|
76
78
|
onRead?(conversationId: string, lastReadAt: string): void;
|
|
77
79
|
pendingArchiveUpdate?: PendingArchiveConversationUpdate;
|
|
78
80
|
}) {
|
|
81
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
79
82
|
const [subagentTarget, setSubagentTarget] =
|
|
80
83
|
useState<SubagentTranscriptTarget>();
|
|
81
84
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -196,7 +199,12 @@ export function ConversationPage(props: {
|
|
|
196
199
|
);
|
|
197
200
|
|
|
198
201
|
return (
|
|
199
|
-
|
|
202
|
+
<div
|
|
203
|
+
className={cn(
|
|
204
|
+
"grid h-full min-h-0 min-w-0",
|
|
205
|
+
detailsOpen && "xl:grid-cols-[minmax(0,1fr)_25rem]",
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
200
208
|
<ChatLayout
|
|
201
209
|
scrollMinTall
|
|
202
210
|
scrollAriaLabel="Conversation transcript"
|
|
@@ -208,6 +216,8 @@ export function ConversationPage(props: {
|
|
|
208
216
|
agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
|
|
209
217
|
}
|
|
210
218
|
conversationId={conversationId}
|
|
219
|
+
detailsOpen={detailsOpen}
|
|
220
|
+
onDetailsClick={() => setDetailsOpen((open) => !open)}
|
|
211
221
|
forks={
|
|
212
222
|
detail.data?.forkedFromConversationId ||
|
|
213
223
|
detail.data?.forks?.length ? (
|
|
@@ -220,8 +230,6 @@ export function ConversationPage(props: {
|
|
|
220
230
|
/>
|
|
221
231
|
) : null
|
|
222
232
|
}
|
|
223
|
-
lastActivityAt={conversation?.lastSeenAt}
|
|
224
|
-
sentryConversationUrl={detail.data?.sentryConversationUrl}
|
|
225
233
|
copyAction={
|
|
226
234
|
<CopyMarkdownButton
|
|
227
235
|
key={conversationDetail?.conversationId ?? "loading"}
|
|
@@ -236,11 +244,6 @@ export function ConversationPage(props: {
|
|
|
236
244
|
}
|
|
237
245
|
/>
|
|
238
246
|
}
|
|
239
|
-
annotations={
|
|
240
|
-
hasConversationAnnotations(detail.data?.annotations) ? (
|
|
241
|
-
<ConversationAnnotations detail={detail.data} />
|
|
242
|
-
) : null
|
|
243
|
-
}
|
|
244
247
|
linkedWork={
|
|
245
248
|
hasConversationAnnotations(detail.data?.annotations) ? (
|
|
246
249
|
<ConversationAnnotations
|
|
@@ -260,15 +263,6 @@ export function ConversationPage(props: {
|
|
|
260
263
|
}),
|
|
261
264
|
pending: archive.isPending,
|
|
262
265
|
}}
|
|
263
|
-
brief={
|
|
264
|
-
detail.data?.brief ? (
|
|
265
|
-
<ConversationBrief
|
|
266
|
-
brief={detail.data.brief}
|
|
267
|
-
variant="summary"
|
|
268
|
-
/>
|
|
269
|
-
) : null
|
|
270
|
-
}
|
|
271
|
-
identity={identity}
|
|
272
266
|
live={live}
|
|
273
267
|
meta={
|
|
274
268
|
<ConversationHeaderMeta
|
|
@@ -288,7 +282,6 @@ export function ConversationPage(props: {
|
|
|
288
282
|
}
|
|
289
283
|
/>
|
|
290
284
|
}
|
|
291
|
-
onOpenSubagent={setSubagentTarget}
|
|
292
285
|
onSearchChange={setSearch}
|
|
293
286
|
onViewChange={onViewChange}
|
|
294
287
|
privacy={
|
|
@@ -297,23 +290,6 @@ export function ConversationPage(props: {
|
|
|
297
290
|
/>
|
|
298
291
|
}
|
|
299
292
|
search={search}
|
|
300
|
-
stats={
|
|
301
|
-
hasConversationStats({
|
|
302
|
-
conversation,
|
|
303
|
-
detail: detail.data,
|
|
304
|
-
}) ? (
|
|
305
|
-
<div className="grid min-w-0 gap-5">
|
|
306
|
-
<ConversationStats
|
|
307
|
-
conversation={conversation}
|
|
308
|
-
detail={detail.data}
|
|
309
|
-
/>
|
|
310
|
-
<ConversationUsageBreakdown
|
|
311
|
-
conversation={conversation}
|
|
312
|
-
detail={detail.data}
|
|
313
|
-
/>
|
|
314
|
-
</div>
|
|
315
|
-
) : null
|
|
316
|
-
}
|
|
317
293
|
title={title}
|
|
318
294
|
view={view}
|
|
319
295
|
/>
|
|
@@ -382,11 +358,48 @@ export function ConversationPage(props: {
|
|
|
382
358
|
) : undefined
|
|
383
359
|
}
|
|
384
360
|
/>
|
|
361
|
+
{detailsOpen ? (
|
|
362
|
+
<ConversationDetails
|
|
363
|
+
conversationId={conversationId}
|
|
364
|
+
identity={identity}
|
|
365
|
+
lastActivityAt={conversation?.lastSeenAt}
|
|
366
|
+
sentryConversationUrl={detail.data?.sentryConversationUrl}
|
|
367
|
+
onClose={() => setDetailsOpen(false)}
|
|
368
|
+
onOpenSubagent={setSubagentTarget}
|
|
369
|
+
annotations={
|
|
370
|
+
hasConversationAnnotations(detail.data?.annotations) ? (
|
|
371
|
+
<ConversationAnnotations detail={detail.data} />
|
|
372
|
+
) : null
|
|
373
|
+
}
|
|
374
|
+
brief={
|
|
375
|
+
detail.data?.brief ? (
|
|
376
|
+
<ConversationBrief brief={detail.data.brief} variant="summary" />
|
|
377
|
+
) : null
|
|
378
|
+
}
|
|
379
|
+
stats={
|
|
380
|
+
hasConversationStats({
|
|
381
|
+
conversation,
|
|
382
|
+
detail: detail.data,
|
|
383
|
+
}) ? (
|
|
384
|
+
<div className="grid min-w-0 gap-5">
|
|
385
|
+
<ConversationStats
|
|
386
|
+
conversation={conversation}
|
|
387
|
+
detail={detail.data}
|
|
388
|
+
/>
|
|
389
|
+
<ConversationUsageBreakdown
|
|
390
|
+
conversation={conversation}
|
|
391
|
+
detail={detail.data}
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
) : null
|
|
395
|
+
}
|
|
396
|
+
/>
|
|
397
|
+
) : null}
|
|
385
398
|
<SubagentTranscriptDrawer
|
|
386
399
|
onClose={() => setSubagentTarget(undefined)}
|
|
387
400
|
target={subagentTarget}
|
|
388
401
|
/>
|
|
389
|
-
|
|
402
|
+
</div>
|
|
390
403
|
);
|
|
391
404
|
}
|
|
392
405
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InputImage } from "@sentry/junior/api/schema";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { Globe2, LockKeyhole } from "lucide-react";
|
|
4
4
|
import { useNavigate, useParams } from "react-router";
|
|
5
5
|
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
} from "./queries";
|
|
18
18
|
import { conversationPath, NEW_CONVERSATION_PATH } from "./conversationRoutes";
|
|
19
19
|
import { buildConversations, getDashboardTimeZone } from "../format";
|
|
20
|
+
import { useDebouncedValue } from "../debouncedValue";
|
|
20
21
|
import type { Conversation } from "../types";
|
|
21
|
-
import { cn, dashboardContainerClass } from "../styles";
|
|
22
22
|
import { ConversationPage } from "./ConversationPage";
|
|
23
23
|
import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ const CONVERSATION_PAGE_SIZE = 20;
|
|
|
28
28
|
export function ConversationWorkspace() {
|
|
29
29
|
const [page, setPage] = useState(1);
|
|
30
30
|
const [query, setQuery] = useState("");
|
|
31
|
-
const search =
|
|
31
|
+
const search = useDebouncedValue(query.trim());
|
|
32
32
|
const params = useParams();
|
|
33
33
|
const navigate = useNavigate();
|
|
34
34
|
const selectedId = params.conversationId;
|
|
@@ -90,12 +90,7 @@ export function ConversationWorkspace() {
|
|
|
90
90
|
|
|
91
91
|
if (home) {
|
|
92
92
|
return (
|
|
93
|
-
<main
|
|
94
|
-
className={cn(
|
|
95
|
-
dashboardContainerClass,
|
|
96
|
-
"h-full min-h-0 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6",
|
|
97
|
-
)}
|
|
98
|
-
>
|
|
93
|
+
<main className="h-full min-h-0 w-full overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6">
|
|
99
94
|
<div className="mx-auto grid w-full max-w-6xl gap-8">
|
|
100
95
|
{createView}
|
|
101
96
|
<section aria-label="Conversations" className="grid gap-3">
|
|
@@ -192,25 +187,27 @@ function NewConversationView(props: {
|
|
|
192
187
|
footerStart={
|
|
193
188
|
<div
|
|
194
189
|
aria-label="Conversation visibility"
|
|
195
|
-
className="inline-flex items-center gap-
|
|
190
|
+
className="inline-flex shrink-0 items-center gap-0.5 rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-0.5"
|
|
196
191
|
role="group"
|
|
197
192
|
>
|
|
198
193
|
<ToggleButton
|
|
194
|
+
className="min-w-20 gap-1.5"
|
|
199
195
|
onClick={() => setVisibility("public")}
|
|
200
196
|
pressed={isPublic}
|
|
201
197
|
type="button"
|
|
202
198
|
variant="segment"
|
|
203
199
|
>
|
|
204
|
-
<Globe2 aria-hidden="true" className="
|
|
200
|
+
<Globe2 aria-hidden="true" className="size-3" />
|
|
205
201
|
Public
|
|
206
202
|
</ToggleButton>
|
|
207
203
|
<ToggleButton
|
|
204
|
+
className="min-w-20 gap-1.5"
|
|
208
205
|
onClick={() => setVisibility("private")}
|
|
209
206
|
pressed={!isPublic}
|
|
210
207
|
type="button"
|
|
211
208
|
variant="segment"
|
|
212
209
|
>
|
|
213
|
-
<LockKeyhole aria-hidden="true" className="
|
|
210
|
+
<LockKeyhole aria-hidden="true" className="size-3" />
|
|
214
211
|
Private
|
|
215
212
|
</ToggleButton>
|
|
216
213
|
</div>
|
|
@@ -58,10 +58,7 @@ function ConversationHomeLoading() {
|
|
|
58
58
|
<main
|
|
59
59
|
aria-busy="true"
|
|
60
60
|
aria-live="polite"
|
|
61
|
-
className=
|
|
62
|
-
dashboardContainerClass,
|
|
63
|
-
"h-full min-h-0 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6",
|
|
64
|
-
)}
|
|
61
|
+
className="h-full min-h-0 w-full overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6"
|
|
65
62
|
role="status"
|
|
66
63
|
>
|
|
67
64
|
<span className="sr-only">Loading conversations</span>
|
|
@@ -40,10 +40,26 @@ shared controls and overlays.
|
|
|
40
40
|
Automation. Render this text with `TranscriptText` or `MentionText`. Both
|
|
41
41
|
require `mentions`, so a caller cannot forget them. Do not resolve names in
|
|
42
42
|
the dashboard, and do not rewrite tokens into links on the server.
|
|
43
|
-
- `TranscriptTurnContextView` owns context content. The shared `Drawer`
|
|
44
|
-
|
|
43
|
+
- `TranscriptTurnContextView` owns context content. The shared `Drawer` adds
|
|
44
|
+
its header and layout to the Base UI `Sheet` focus and dismissal boundary.
|
|
45
45
|
|
|
46
46
|
Review these components together in `/dev/transcripts`. Use the conversation
|
|
47
47
|
fixture for messages, events, activity, context, and typing. Use the attachment
|
|
48
48
|
fixture for file wrapping. Keep visual checks out of automated assertions;
|
|
49
49
|
browser tests cover opening, closing, and keyboard interaction.
|
|
50
|
+
|
|
51
|
+
## Conversation details
|
|
52
|
+
|
|
53
|
+
`ConversationPage` owns whether details are open. The page is keyed by the
|
|
54
|
+
Conversation, so switching Conversations closes details. `ConversationHeader`
|
|
55
|
+
only toggles details and restores focus through explicit control refs after
|
|
56
|
+
dismissal.
|
|
57
|
+
|
|
58
|
+
`ConversationDetails` owns one set of tabs and content. `ConversationDetailsFrame`
|
|
59
|
+
uses a 400px aside at 1280px and above. The chat and aside scroll separately.
|
|
60
|
+
Smaller windows use `BottomSheet` with a plain accessible label. The sheet and
|
|
61
|
+
side drawers share the Base UI `Sheet` focus rules and the app's document
|
|
62
|
+
scroll lock. A resize changes the frame and keeps the selected tab. Tabs stay fixed above the scrolling content in both
|
|
63
|
+
layouts. Only the swipe handle sits above the sheet tabs. Swipe down, tap the
|
|
64
|
+
backdrop, or press Escape to dismiss it. Base UI owns the transition lifecycle.
|
|
65
|
+
A pending exit cannot close a new panel or a different Conversation.
|
|
@@ -354,7 +354,7 @@ export function CostMetric(props: {
|
|
|
354
354
|
align?: "left" | "right";
|
|
355
355
|
auxiliaryCosts?: ConversationAuxiliaryCosts;
|
|
356
356
|
childAgentTotals?: ConversationChildAgentTotals;
|
|
357
|
-
/** Point to
|
|
357
|
+
/** Point to conversation details, which has the full breakdown. */
|
|
358
358
|
detailsHint?: boolean;
|
|
359
359
|
live?: boolean;
|
|
360
360
|
liveModelId?: string;
|
|
@@ -398,7 +398,7 @@ export function TokenMetric(props: {
|
|
|
398
398
|
align?: "left" | "right";
|
|
399
399
|
childAgentTotals?: ConversationChildAgentTotals;
|
|
400
400
|
compactionCount?: number;
|
|
401
|
-
/** Point to
|
|
401
|
+
/** Point to conversation details, which has the full breakdown. */
|
|
402
402
|
detailsHint?: boolean;
|
|
403
403
|
live?: boolean;
|
|
404
404
|
liveModelId?: string;
|