@sentry/junior-dashboard 0.230.0 → 0.232.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 +1 -1
- package/dist/client.js +33 -32
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/client/conversations/ConversationDetailsDrawer.tsx +23 -17
- package/src/client/conversations/ConversationHeader.tsx +2 -0
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -0
- package/src/client/conversations/ShareConversationButton.tsx +101 -0
- package/src/client/conversations/SlackMark.tsx +6 -2
- package/src/client/conversations/conversationOutbox.ts +5 -0
- package/src/client/conversations/queries.ts +19 -0
- package/src/client/conversations/transcriptBottomPinning.ts +2 -11
- package/src/client/pages/dev/ConversationFixture.tsx +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.232.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"react-router": "^7.18.2",
|
|
35
35
|
"shiki": "4.1.0",
|
|
36
36
|
"zod": "^4.5.4",
|
|
37
|
-
"@sentry/junior": "0.
|
|
38
|
-
"@sentry/junior-plugin-api": "0.
|
|
37
|
+
"@sentry/junior": "0.232.0",
|
|
38
|
+
"@sentry/junior-plugin-api": "0.232.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -6,6 +6,7 @@ import { ButtonLink } from "../components/Button";
|
|
|
6
6
|
import { Drawer } from "../components/Drawer";
|
|
7
7
|
import { SegmentedTabs } from "../components/SegmentedTabs";
|
|
8
8
|
import { ConversationMemories } from "./ConversationMemories";
|
|
9
|
+
import { ShareConversationButton } from "./ShareConversationButton";
|
|
9
10
|
|
|
10
11
|
const tabs = [
|
|
11
12
|
{ label: "Details", value: "details" },
|
|
@@ -85,9 +86,8 @@ export function ConversationDetailsDrawer(props: {
|
|
|
85
86
|
No additional conversation details.
|
|
86
87
|
</p>
|
|
87
88
|
)}
|
|
88
|
-
{tab === "details"
|
|
89
|
-
|
|
90
|
-
<footer className="mt-5 grid gap-3 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
|
|
89
|
+
{tab === "details" ? (
|
|
90
|
+
<footer className="mt-5 grid gap-6 border-t border-dashboard-border pt-4 text-xs text-dashboard-text-muted">
|
|
91
91
|
{props.lastActivityAt ? (
|
|
92
92
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
93
93
|
<span>Last activity</span>
|
|
@@ -102,20 +102,26 @@ export function ConversationDetailsDrawer(props: {
|
|
|
102
102
|
</time>
|
|
103
103
|
</div>
|
|
104
104
|
) : null}
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
106
|
+
<ShareConversationButton
|
|
107
|
+
key={props.conversationId}
|
|
108
|
+
conversationId={props.conversationId}
|
|
109
|
+
/>
|
|
110
|
+
{props.sentryConversationUrl ? (
|
|
111
|
+
<ButtonLink
|
|
112
|
+
className="w-fit focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
113
|
+
to={props.sentryConversationUrl}
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
target="_blank"
|
|
116
|
+
>
|
|
117
|
+
Open in Sentry
|
|
118
|
+
<ExternalLink
|
|
119
|
+
aria-hidden="true"
|
|
120
|
+
className="size-3.5 shrink-0"
|
|
121
|
+
/>
|
|
122
|
+
</ButtonLink>
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
119
125
|
</footer>
|
|
120
126
|
) : null}
|
|
121
127
|
</SegmentedTabs>
|
|
@@ -111,6 +111,7 @@ export function ConversationHeader(props: {
|
|
|
111
111
|
<div className="absolute right-0 top-[calc(100%+0.35rem)] z-40 w-56 rounded-xl border border-white/[0.08] bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl">
|
|
112
112
|
<ConversationHeaderActions
|
|
113
113
|
archive={props.archive}
|
|
114
|
+
conversationId={props.conversationId}
|
|
114
115
|
copyAction={menuCopyAction}
|
|
115
116
|
detailsOpen={detailsOpen}
|
|
116
117
|
layout="menu"
|
|
@@ -185,6 +186,7 @@ export function ConversationHeader(props: {
|
|
|
185
186
|
</div>
|
|
186
187
|
<ConversationHeaderActions
|
|
187
188
|
archive={props.archive}
|
|
189
|
+
conversationId={props.conversationId}
|
|
188
190
|
copyAction={props.copyAction}
|
|
189
191
|
detailsOpen={detailsOpen}
|
|
190
192
|
onDetailsClick={() => setDetailsOpen(true)}
|
|
@@ -14,6 +14,7 @@ import { Button, ToggleButton } from "../components/Button";
|
|
|
14
14
|
import { IconButtonTooltip } from "../components/Tooltip";
|
|
15
15
|
import { cn } from "../styles";
|
|
16
16
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
17
|
+
import { ShareConversationButton } from "./ShareConversationButton";
|
|
17
18
|
|
|
18
19
|
export type ConversationArchiveAction = {
|
|
19
20
|
archived: boolean;
|
|
@@ -26,6 +27,7 @@ export type ConversationArchiveAction = {
|
|
|
26
27
|
/** Show conversation actions in the header or mobile menu. */
|
|
27
28
|
export function ConversationHeaderActions(props: {
|
|
28
29
|
archive: ConversationArchiveAction;
|
|
30
|
+
conversationId: string;
|
|
29
31
|
copyAction?: ReactNode;
|
|
30
32
|
detailsOpen: boolean;
|
|
31
33
|
/** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
|
|
@@ -65,6 +67,11 @@ export function ConversationHeaderActions(props: {
|
|
|
65
67
|
>
|
|
66
68
|
<ScrollText aria-hidden="true" size={16} strokeWidth={2} />
|
|
67
69
|
</MenuActionButton>
|
|
70
|
+
<ShareConversationButton
|
|
71
|
+
key={props.conversationId}
|
|
72
|
+
conversationId={props.conversationId}
|
|
73
|
+
layout="menu"
|
|
74
|
+
/>
|
|
68
75
|
{props.copyAction}
|
|
69
76
|
<ArchiveConversationButton {...props.archive} layout="menu" />
|
|
70
77
|
<MenuActionButton
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useHref } from "react-router";
|
|
3
|
+
import { Check, Share2 } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "../components/Button";
|
|
6
|
+
import { conversationPath } from "./conversationRoutes";
|
|
7
|
+
|
|
8
|
+
/** Share a conversation link without changing who can access it. */
|
|
9
|
+
export function ShareConversationButton(props: {
|
|
10
|
+
conversationId: string;
|
|
11
|
+
layout?: "button" | "menu";
|
|
12
|
+
}) {
|
|
13
|
+
const href = useHref(conversationPath(props.conversationId));
|
|
14
|
+
const [status, setStatus] = useState<
|
|
15
|
+
"idle" | "sharing" | "copied" | "copy-failed" | "share-failed"
|
|
16
|
+
>("idle");
|
|
17
|
+
const label =
|
|
18
|
+
status === "copied"
|
|
19
|
+
? "Link copied"
|
|
20
|
+
: status === "copy-failed"
|
|
21
|
+
? "Copy failed — retry"
|
|
22
|
+
: status === "share-failed"
|
|
23
|
+
? "Share failed — retry"
|
|
24
|
+
: "Share";
|
|
25
|
+
const Icon = status === "copied" ? Check : Share2;
|
|
26
|
+
const feedback =
|
|
27
|
+
status === "copied"
|
|
28
|
+
? "Conversation link copied. Access has not changed."
|
|
29
|
+
: status === "copy-failed"
|
|
30
|
+
? "Could not copy the link. Try again or copy it from the address bar."
|
|
31
|
+
: status === "share-failed"
|
|
32
|
+
? "Could not share the link. Try again or copy it from the address bar."
|
|
33
|
+
: "";
|
|
34
|
+
|
|
35
|
+
async function shareLink() {
|
|
36
|
+
setStatus("sharing");
|
|
37
|
+
const url = new URL(href, window.location.origin).href;
|
|
38
|
+
// Touch-first devices use the native sheet, including tablets. Desktop
|
|
39
|
+
// browsers keep copy-link behavior even when they support native sharing.
|
|
40
|
+
if (
|
|
41
|
+
window.matchMedia("(pointer: coarse)").matches &&
|
|
42
|
+
typeof navigator.share === "function"
|
|
43
|
+
) {
|
|
44
|
+
try {
|
|
45
|
+
await navigator.share({ url });
|
|
46
|
+
setStatus("idle");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Dismissing the sheet must not copy a link or show an error.
|
|
49
|
+
setStatus(
|
|
50
|
+
error instanceof DOMException && error.name === "AbortError"
|
|
51
|
+
? "idle"
|
|
52
|
+
: "share-failed",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await navigator.clipboard.writeText(url);
|
|
60
|
+
setStatus("copied");
|
|
61
|
+
} catch {
|
|
62
|
+
setStatus("copy-failed");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const content = (
|
|
67
|
+
<>
|
|
68
|
+
<Icon aria-hidden="true" size={16} strokeWidth={2} />
|
|
69
|
+
<span>{label}</span>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
const button =
|
|
73
|
+
props.layout === "menu" ? (
|
|
74
|
+
<button
|
|
75
|
+
aria-label={label}
|
|
76
|
+
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"
|
|
77
|
+
disabled={status === "sharing"}
|
|
78
|
+
onClick={() => void shareLink()}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
{content}
|
|
82
|
+
</button>
|
|
83
|
+
) : (
|
|
84
|
+
<Button
|
|
85
|
+
aria-label={label}
|
|
86
|
+
disabled={status === "sharing"}
|
|
87
|
+
onClick={() => void shareLink()}
|
|
88
|
+
>
|
|
89
|
+
{content}
|
|
90
|
+
</Button>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{button}
|
|
96
|
+
<span className="sr-only" role="status">
|
|
97
|
+
{feedback}
|
|
98
|
+
</span>
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -6,9 +6,13 @@ export function SlackMark(props: { className?: string }) {
|
|
|
6
6
|
className={props.className}
|
|
7
7
|
fill="currentColor"
|
|
8
8
|
role="img"
|
|
9
|
-
viewBox="0 0
|
|
9
|
+
viewBox="0 0 127 127"
|
|
10
10
|
>
|
|
11
|
-
|
|
11
|
+
{/* Keep the geometry from packages/docs/public/integrations/slack.svg. */}
|
|
12
|
+
<path d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z" />
|
|
13
|
+
<path d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z" />
|
|
14
|
+
<path d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z" />
|
|
15
|
+
<path d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z" />
|
|
12
16
|
</svg>
|
|
13
17
|
);
|
|
14
18
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ActorIdentity,
|
|
2
3
|
ConversationPendingMessage,
|
|
3
4
|
InputImage,
|
|
4
5
|
} from "@sentry/junior/api/schema";
|
|
5
6
|
|
|
6
7
|
/** Client-owned mailbox row waiting on accept, server visibility, or retry. */
|
|
7
8
|
export type ConversationOutboxMessage = {
|
|
9
|
+
actorIdentity?: ActorIdentity;
|
|
8
10
|
createdAt: string;
|
|
9
11
|
idempotencyKey: string;
|
|
10
12
|
message: string;
|
|
@@ -27,6 +29,7 @@ export function conversationOutboxQueryKey(conversationId: string | undefined) {
|
|
|
27
29
|
|
|
28
30
|
/** Build one optimistic outbox row for a composer submit. */
|
|
29
31
|
export function conversationOutboxMessageForSubmit(input: {
|
|
32
|
+
actorIdentity?: ActorIdentity;
|
|
30
33
|
idempotencyKey: string;
|
|
31
34
|
message: string;
|
|
32
35
|
images?: InputImage[];
|
|
@@ -35,6 +38,7 @@ export function conversationOutboxMessageForSubmit(input: {
|
|
|
35
38
|
}): ConversationOutboxMessage {
|
|
36
39
|
const createdAt = input.now ?? new Date().toISOString();
|
|
37
40
|
return {
|
|
41
|
+
actorIdentity: input.actorIdentity,
|
|
38
42
|
createdAt,
|
|
39
43
|
idempotencyKey: input.idempotencyKey,
|
|
40
44
|
message: input.message,
|
|
@@ -49,6 +53,7 @@ export function mailboxMessageFromOutbox(
|
|
|
49
53
|
message: ConversationOutboxMessage,
|
|
50
54
|
): ConversationMailboxMessage {
|
|
51
55
|
return {
|
|
56
|
+
actorIdentity: message.actorIdentity,
|
|
52
57
|
clientStatus: message.status,
|
|
53
58
|
createdAt: message.createdAt,
|
|
54
59
|
delivery: "defer",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useDeferredValue, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
queryOptions,
|
|
4
|
+
type QueryClient,
|
|
4
5
|
useInfiniteQuery,
|
|
5
6
|
useMutation,
|
|
6
7
|
useMutationState,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
useQueryClient,
|
|
9
10
|
} from "@tanstack/react-query";
|
|
10
11
|
import type {
|
|
12
|
+
ActorIdentity,
|
|
11
13
|
ConversationDetailReport,
|
|
12
14
|
ConversationEventPage,
|
|
13
15
|
ConversationFeed,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
conversationPendingMessagesReportSchema,
|
|
26
28
|
} from "@sentry/junior/api/schema";
|
|
27
29
|
|
|
30
|
+
import type { DashboardCoreData } from "../types";
|
|
28
31
|
import {
|
|
29
32
|
DashboardApiError,
|
|
30
33
|
del,
|
|
@@ -176,6 +179,20 @@ export function conversationDetailQueryOptions(
|
|
|
176
179
|
});
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
function outboxActorIdentity(
|
|
183
|
+
queryClient: QueryClient,
|
|
184
|
+
): ActorIdentity | undefined {
|
|
185
|
+
const user = queryClient.getQueryData<DashboardCoreData>([
|
|
186
|
+
"dashboard",
|
|
187
|
+
"core",
|
|
188
|
+
])?.me.user;
|
|
189
|
+
if (!user?.email) return undefined;
|
|
190
|
+
return {
|
|
191
|
+
email: user.email,
|
|
192
|
+
fullName: user.name || undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
179
196
|
/** Create one dashboard conversation and refresh the personal feed. */
|
|
180
197
|
export function useCreateConversation() {
|
|
181
198
|
const queryClient = useQueryClient();
|
|
@@ -193,6 +210,7 @@ export function useCreateConversation() {
|
|
|
193
210
|
upsertConversationOutboxMessage(current, {
|
|
194
211
|
...conversationOutboxMessageForSubmit({
|
|
195
212
|
...args,
|
|
213
|
+
actorIdentity: outboxActorIdentity(queryClient),
|
|
196
214
|
messageId: accepted.messageId,
|
|
197
215
|
}),
|
|
198
216
|
status: "accepted",
|
|
@@ -233,6 +251,7 @@ export function useAppendConversationMessage(conversationId: string) {
|
|
|
233
251
|
});
|
|
234
252
|
const optimistic = conversationOutboxMessageForSubmit({
|
|
235
253
|
...args,
|
|
254
|
+
actorIdentity: outboxActorIdentity(queryClient),
|
|
236
255
|
messageId,
|
|
237
256
|
});
|
|
238
257
|
queryClient.setQueryData<ConversationOutboxMessage[]>(
|
|
@@ -15,7 +15,6 @@ import type { ConversationTranscript } from "../types";
|
|
|
15
15
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
16
16
|
|
|
17
17
|
const BOTTOM_PROXIMITY_PX = 96;
|
|
18
|
-
const MOBILE_MEDIA_QUERY = "(max-width: 767px)";
|
|
19
18
|
const USER_SCROLL_DELTA_PX = 2;
|
|
20
19
|
|
|
21
20
|
type ScrollRoot = HTMLElement | Window;
|
|
@@ -528,16 +527,8 @@ export function usePinnedTranscriptBottom(input: {
|
|
|
528
527
|
}
|
|
529
528
|
}
|
|
530
529
|
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
tailChanged &&
|
|
534
|
-
input.enabled &&
|
|
535
|
-
typeof window !== "undefined" &&
|
|
536
|
-
window.matchMedia(MOBILE_MEDIA_QUERY).matches
|
|
537
|
-
) {
|
|
538
|
-
setFollowingIntent(true);
|
|
539
|
-
}
|
|
540
|
-
|
|
530
|
+
// New activity must preserve scroll-away intent on mobile and desktop.
|
|
531
|
+
// The reader can resume following with the jump control or by scrolling down.
|
|
541
532
|
if (
|
|
542
533
|
shouldAutoPinTranscriptBottom({
|
|
543
534
|
enabled: input.enabled,
|
|
@@ -13,6 +13,7 @@ const EVENTS: ConversationReportEventData[] = [
|
|
|
13
13
|
type: "message",
|
|
14
14
|
messageId: "gallery-user",
|
|
15
15
|
role: "user",
|
|
16
|
+
source: "slack",
|
|
16
17
|
actorIdentity: { fullName: "Alex Rivera" },
|
|
17
18
|
text: "Show me the visual QA.",
|
|
18
19
|
},
|
|
@@ -39,6 +40,7 @@ const EVENTS: ConversationReportEventData[] = [
|
|
|
39
40
|
type: "message",
|
|
40
41
|
messageId: "gallery-reply",
|
|
41
42
|
role: "assistant",
|
|
43
|
+
source: "slack",
|
|
42
44
|
text: "The change is ready for review.\n\nI tightened the conversation layout and kept the event details available below.",
|
|
43
45
|
cards: [
|
|
44
46
|
{
|
|
@@ -117,7 +119,7 @@ const CONVERSATION: ConversationDetailReport = {
|
|
|
117
119
|
lastSeenAt: TIMESTAMP,
|
|
118
120
|
lastProgressAt: TIMESTAMP,
|
|
119
121
|
generatedAt: TIMESTAMP,
|
|
120
|
-
surface: "
|
|
122
|
+
surface: "slack",
|
|
121
123
|
eventHistory: { status: "available" },
|
|
122
124
|
annotations: [
|
|
123
125
|
{
|