@sentry/junior-dashboard 0.269.0 → 0.271.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 +78 -75
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/client/App.tsx +103 -65
- package/src/client/components/AGENTS.md +30 -59
- package/src/client/components/AutomationCard.tsx +2 -1
- package/src/client/components/ProfileMenu.tsx +0 -24
- package/src/client/components/UserInputForm.tsx +143 -39
- package/src/client/components/layout/PageHeader.tsx +25 -10
- package/src/client/conversations/AGENTS.md +12 -37
- package/src/client/conversations/ConversationBrief.tsx +55 -36
- package/src/client/conversations/ConversationComposer.tsx +27 -8
- package/src/client/conversations/ConversationEventLog.tsx +1 -0
- package/src/client/conversations/ConversationHomeList.tsx +43 -26
- package/src/client/conversations/ConversationPage.tsx +137 -40
- package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
- package/src/client/conversations/ConversationSidebar.tsx +55 -25
- package/src/client/conversations/ConversationTranscript.tsx +3 -2
- package/src/client/conversations/ConversationWatches.tsx +4 -1
- package/src/client/conversations/ConversationWorkspace.tsx +43 -25
- package/src/client/conversations/EventDetails.tsx +6 -9
- package/src/client/conversations/PendingMailboxStack.tsx +5 -2
- package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +9 -3
- package/src/client/conversations/TranscriptMessageView.tsx +6 -5
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
- package/src/client/conversations/TranscriptRows.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
- package/src/client/conversations/TranscriptToolView.tsx +2 -1
- package/src/client/conversations/TranscriptView.tsx +6 -6
- package/src/client/conversations/conversationOutbox.ts +13 -3
- package/src/client/conversations/conversationRoutes.ts +30 -3
- package/src/client/conversations/conversationSections.ts +36 -8
- package/src/client/conversations/eventTranscript.ts +1 -0
- package/src/client/conversations/transcriptBottomPinning.ts +171 -81
- package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
- package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
- package/src/client/display-text/DisplayText.tsx +68 -0
- package/src/client/display-text/emoji.ts +54 -0
- package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
- package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
- package/src/client/display-text/mentions.tsx +148 -0
- package/src/client/display-text/plainText.ts +29 -0
- package/src/client/pages/automations/AGENTS.md +6 -25
- package/src/client/pages/automations/AutomationFilters.tsx +6 -6
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
- package/src/client/pages/code/RepositoryTabs.tsx +6 -1
- package/src/client/pages/memory/MemoryDetails.tsx +5 -13
- package/src/client/pages/settings/ConnectionsPage.tsx +198 -0
- package/src/client/pages/settings/McpSettingsPage.tsx +77 -0
- package/src/client/pages/settings/PersonalTokensPage.tsx +198 -0
- package/src/client/pages/{SettingsPage.tsx → settings/SettingsPage.tsx} +54 -62
- package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
- package/src/client/review-desk/ReviewDeskPage.tsx +202 -241
- package/src/client/review-desk/ReviewDeskSidebar.tsx +253 -0
- package/src/client/review-desk/ReviewItemActions.tsx +6 -2
- package/src/client/review-desk/ReviewItemCard.tsx +58 -17
- package/src/client/review-desk/ReviewItemContext.tsx +108 -0
- package/src/client/review-desk/ReviewItemDetails.tsx +153 -0
- package/src/client/review-desk/ReviewItemView.tsx +159 -0
- package/src/client/review-desk/UnsentAnswers.tsx +148 -0
- package/src/client/review-desk/queries.ts +4 -2
- package/src/client/review-desk/reviewItemFormat.ts +83 -1
- package/src/client/components/charts/AGENTS.md +0 -15
- package/src/client/conversations/TranscriptText.tsx +0 -47
- package/src/client/conversations/conversationActivityPreview.ts +0 -16
- package/src/client/conversations/transcriptMentions.tsx +0 -134
- package/src/client/pages/ConnectionsPage.tsx +0 -207
- package/src/client/pages/PersonalTokensPage.tsx +0 -205
- package/src/client/pages/system/AGENTS.md +0 -12
- package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
|
@@ -16,11 +16,16 @@ import {
|
|
|
16
16
|
type PendingArchiveConversationUpdate,
|
|
17
17
|
} from "./queries";
|
|
18
18
|
import { conversationPath, NEW_CONVERSATION_PATH } from "./conversationRoutes";
|
|
19
|
+
import {
|
|
20
|
+
attentionConversationIds,
|
|
21
|
+
attentionFirst,
|
|
22
|
+
} from "./conversationSections";
|
|
19
23
|
import { buildConversations, getDashboardTimeZone } from "../format";
|
|
20
24
|
import { useDebouncedValue } from "../debouncedValue";
|
|
21
25
|
import type { Conversation } from "../types";
|
|
22
26
|
import { ConversationPage } from "./ConversationPage";
|
|
23
27
|
import { useWaitingConversationIds } from "../review-desk/queries";
|
|
28
|
+
import { UnsentAnswersProvider } from "../review-desk/UnsentAnswers";
|
|
24
29
|
import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";
|
|
25
30
|
|
|
26
31
|
const CONVERSATION_PAGE_SIZE = 20;
|
|
@@ -51,10 +56,30 @@ export function ConversationWorkspace() {
|
|
|
51
56
|
if (selectedId) navigate(NEW_CONVERSATION_PATH);
|
|
52
57
|
};
|
|
53
58
|
|
|
59
|
+
const waitingConversationIds = useWaitingConversationIds();
|
|
60
|
+
const { finishedConversationIds, markRead } =
|
|
61
|
+
useConversationFinishedIndicators(
|
|
62
|
+
conversations,
|
|
63
|
+
selectedId,
|
|
64
|
+
Boolean(feed.data),
|
|
65
|
+
!search,
|
|
66
|
+
);
|
|
67
|
+
|
|
54
68
|
const totalPages = pageCount(conversations.length, CONVERSATION_PAGE_SIZE);
|
|
55
69
|
const pagedConversations = useMemo(
|
|
56
|
-
() =>
|
|
57
|
-
|
|
70
|
+
() =>
|
|
71
|
+
pageItems(
|
|
72
|
+
attentionFirst(
|
|
73
|
+
conversations,
|
|
74
|
+
attentionConversationIds(
|
|
75
|
+
waitingConversationIds,
|
|
76
|
+
finishedConversationIds,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
page,
|
|
80
|
+
CONVERSATION_PAGE_SIZE,
|
|
81
|
+
),
|
|
82
|
+
[conversations, finishedConversationIds, page, waitingConversationIds],
|
|
58
83
|
);
|
|
59
84
|
useEffect(() => {
|
|
60
85
|
setPage(1);
|
|
@@ -62,14 +87,6 @@ export function ConversationWorkspace() {
|
|
|
62
87
|
useEffect(() => {
|
|
63
88
|
if (page > totalPages) setPage(totalPages);
|
|
64
89
|
}, [page, totalPages]);
|
|
65
|
-
const waitingConversationIds = useWaitingConversationIds();
|
|
66
|
-
const { finishedConversationIds, markRead } =
|
|
67
|
-
useConversationFinishedIndicators(
|
|
68
|
-
conversations,
|
|
69
|
-
selectedId,
|
|
70
|
-
Boolean(feed.data),
|
|
71
|
-
!search,
|
|
72
|
-
);
|
|
73
90
|
|
|
74
91
|
const createView = (
|
|
75
92
|
<NewConversationView
|
|
@@ -145,21 +162,22 @@ export function ConversationWorkspace() {
|
|
|
145
162
|
aria-label="Selected conversation"
|
|
146
163
|
className="grid min-h-0 grid-rows-[minmax(0,1fr)] overflow-hidden bg-dashboard-bg"
|
|
147
164
|
>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
<UnsentAnswersProvider conversationId={selectedId} key={selectedId}>
|
|
166
|
+
<ConversationPage
|
|
167
|
+
conversationId={selectedId}
|
|
168
|
+
data={
|
|
169
|
+
feed.data
|
|
170
|
+
? {
|
|
171
|
+
conversations: feed.data,
|
|
172
|
+
}
|
|
173
|
+
: undefined
|
|
174
|
+
}
|
|
175
|
+
onRead={markRead}
|
|
176
|
+
pendingArchiveUpdate={pendingArchiveUpdates.find(
|
|
177
|
+
(update) => update.conversationId === selectedId,
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
</UnsentAnswersProvider>
|
|
163
181
|
</section>
|
|
164
182
|
</div>
|
|
165
183
|
);
|
|
@@ -13,7 +13,7 @@ import { dashboardDisclosureChevronClass } from "../styles";
|
|
|
13
13
|
|
|
14
14
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
15
15
|
import { DetailsCard } from "./DetailsList";
|
|
16
|
-
import {
|
|
16
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
17
17
|
import { eventLogModel } from "./eventLog";
|
|
18
18
|
import { formatCostBreakdown } from "../format";
|
|
19
19
|
import { EventUsage } from "./EventUsage";
|
|
@@ -68,7 +68,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
68
68
|
) : null}
|
|
69
69
|
{summary ? (
|
|
70
70
|
<DetailSection title="Continuation summary">
|
|
71
|
-
<
|
|
71
|
+
<DisplayText mentions={mentions} text={summary} />
|
|
72
72
|
</DetailSection>
|
|
73
73
|
) : null}
|
|
74
74
|
</>
|
|
@@ -86,7 +86,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
86
86
|
{redacted ? (
|
|
87
87
|
<RedactedMarker />
|
|
88
88
|
) : (
|
|
89
|
-
<
|
|
89
|
+
<DisplayText mentions={mentions} text={text ?? ""} />
|
|
90
90
|
)}
|
|
91
91
|
</DetailSection>
|
|
92
92
|
</>
|
|
@@ -128,7 +128,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
128
128
|
{part.redacted ? (
|
|
129
129
|
<RedactedMarker />
|
|
130
130
|
) : (
|
|
131
|
-
<
|
|
131
|
+
<DisplayText mentions={mentions} text={part.text ?? ""} />
|
|
132
132
|
)}
|
|
133
133
|
</DetailSection>
|
|
134
134
|
));
|
|
@@ -142,15 +142,12 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
142
142
|
</DetailSection>
|
|
143
143
|
<DetailSection title={presentation.title}>
|
|
144
144
|
{presentation.preview ? (
|
|
145
|
-
<
|
|
145
|
+
<DisplayText mentions={mentions} text={presentation.preview} />
|
|
146
146
|
) : null}
|
|
147
147
|
{presentation.details?.map((detail, index) => (
|
|
148
148
|
<DetailSubsection key={index} title={detail.title}>
|
|
149
149
|
{detail.description ? (
|
|
150
|
-
<
|
|
151
|
-
mentions={mentions}
|
|
152
|
-
text={detail.description}
|
|
153
|
-
/>
|
|
150
|
+
<DisplayText mentions={mentions} text={detail.description} />
|
|
154
151
|
) : null}
|
|
155
152
|
{detail.content ? <DetailValue value={detail.content} /> : null}
|
|
156
153
|
{detail.metadata ? (
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
TranscriptHeadingRow,
|
|
39
39
|
} from "./TranscriptHeadingRow";
|
|
40
40
|
import { SlackMark } from "./SlackMark";
|
|
41
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
42
|
+
import { displayPlainText } from "../display-text/plainText";
|
|
41
43
|
|
|
42
44
|
// Rows are one line each, so the stack can show five before it collapses.
|
|
43
45
|
const MAX_EXPANDED_PENDING_ROWS = 5;
|
|
@@ -306,6 +308,7 @@ function PendingRow(props: {
|
|
|
306
308
|
steerPending: boolean;
|
|
307
309
|
}) {
|
|
308
310
|
const text = props.message.text ?? "";
|
|
311
|
+
const mentions = props.message.mentions ?? [];
|
|
309
312
|
const redacted = Boolean(props.message.redacted);
|
|
310
313
|
const showSlack = props.message.source === "slack";
|
|
311
314
|
const canRetry =
|
|
@@ -334,9 +337,9 @@ function PendingRow(props: {
|
|
|
334
337
|
// One line keeps the queue short. The title shows the full text.
|
|
335
338
|
<p
|
|
336
339
|
className="m-0 min-w-0 truncate font-sans text-sm leading-snug text-dashboard-text-subtle"
|
|
337
|
-
title={text}
|
|
340
|
+
title={displayPlainText(text, mentions)}
|
|
338
341
|
>
|
|
339
|
-
{text}
|
|
342
|
+
<DisplayText inline mentions={mentions} text={text} />
|
|
340
343
|
</p>
|
|
341
344
|
) : null}
|
|
342
345
|
</>
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { dashboardDisclosureChevronClass } from "../styles";
|
|
9
9
|
import { formatCompactNumber, formatMessageTimestamp } from "../format";
|
|
10
10
|
import type { TranscriptViewContextEventPart } from "../types";
|
|
11
|
-
import {
|
|
11
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
12
12
|
|
|
13
13
|
/** Render a structural context change from the privacy-safe event API. */
|
|
14
14
|
export function TranscriptContextEventView(props: {
|
|
@@ -57,9 +57,8 @@ export function TranscriptContextEventView(props: {
|
|
|
57
57
|
</CollapsibleTrigger>
|
|
58
58
|
<CollapsibleContent hiddenUntilFound>
|
|
59
59
|
<div className="mt-1.5 whitespace-pre-wrap text-xs leading-relaxed text-dashboard-text-muted">
|
|
60
|
-
<
|
|
61
|
-
mentions={props.part.mentions
|
|
62
|
-
role="system"
|
|
60
|
+
<DisplayText
|
|
61
|
+
mentions={props.part.mentions}
|
|
63
62
|
text={event.summary}
|
|
64
63
|
/>
|
|
65
64
|
</div>
|
|
@@ -3,7 +3,7 @@ import { useState } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
5
5
|
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
6
|
-
import {
|
|
6
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
7
7
|
import { messageRawText } from "./transcriptRenderModel";
|
|
8
8
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
9
9
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
@@ -47,7 +47,12 @@ export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
|
|
|
47
47
|
<span className="flex min-w-0 flex-col gap-1">
|
|
48
48
|
<span className="flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
49
49
|
<span className="min-w-0 break-words font-display text-sm font-semibold leading-6 text-dashboard-text">
|
|
50
|
-
<
|
|
50
|
+
<DisplayText
|
|
51
|
+
inline
|
|
52
|
+
links={links}
|
|
53
|
+
mentions={mentions}
|
|
54
|
+
text={summary}
|
|
55
|
+
/>
|
|
51
56
|
</span>
|
|
52
57
|
{!isBatch && props.message.eventType ? (
|
|
53
58
|
<EventTypeChip eventType={props.message.eventType} />
|
|
@@ -62,7 +67,8 @@ export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
|
|
|
62
67
|
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-5 text-dashboard-text-muted"
|
|
63
68
|
>
|
|
64
69
|
<span className="min-w-0 break-words">
|
|
65
|
-
<
|
|
70
|
+
<DisplayText
|
|
71
|
+
inline
|
|
66
72
|
links={links}
|
|
67
73
|
mentions={mentions}
|
|
68
74
|
text={event.trustedSummary ?? event.eventType}
|
|
@@ -21,7 +21,7 @@ import { messageRawText } from "./transcriptRenderModel";
|
|
|
21
21
|
import { TranscriptMessageHeading } from "./TranscriptHeadingRow";
|
|
22
22
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
23
23
|
import { SlackMark } from "./SlackMark";
|
|
24
|
-
import {
|
|
24
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
25
25
|
import { TranscriptTurnContextView } from "./TranscriptTurnContextView";
|
|
26
26
|
import { TranscriptTimestamp } from "./TranscriptTimestamp";
|
|
27
27
|
import { showsSlackSourceIcon } from "./transcriptSource";
|
|
@@ -33,7 +33,8 @@ export const TranscriptMessageView = memo(
|
|
|
33
33
|
conversation: ConversationTranscript;
|
|
34
34
|
}) {
|
|
35
35
|
const rawText = messageRawText(props.message);
|
|
36
|
-
const
|
|
36
|
+
const roleKind = transcriptRoleKind(props.message.role);
|
|
37
|
+
const chatText = roleKind === "assistant" || roleKind === "user";
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<TranscriptMessageShell
|
|
@@ -58,10 +59,10 @@ export const TranscriptMessageView = memo(
|
|
|
58
59
|
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
|
|
59
60
|
{props.message.parts.map((part, index) =>
|
|
60
61
|
part.type === "text" ? (
|
|
61
|
-
<
|
|
62
|
+
<DisplayText
|
|
63
|
+
chat={chatText}
|
|
62
64
|
key={index}
|
|
63
|
-
mentions={props.message.mentions
|
|
64
|
-
role={role}
|
|
65
|
+
mentions={props.message.mentions}
|
|
65
66
|
text={part.text ?? ""}
|
|
66
67
|
/>
|
|
67
68
|
) : null,
|
|
@@ -6,13 +6,14 @@ import {
|
|
|
6
6
|
TraceCollapsibleContent,
|
|
7
7
|
TraceCollapsibleTrigger,
|
|
8
8
|
} from "../components/ui/trace";
|
|
9
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
9
10
|
import { formatMessageTimestamp } from "../format";
|
|
10
11
|
import type { TranscriptViewReasoningPart } from "../types";
|
|
11
12
|
import {
|
|
12
13
|
TranscriptHeadingMeta,
|
|
13
14
|
TranscriptHeadingRow,
|
|
14
15
|
} from "./TranscriptHeadingRow";
|
|
15
|
-
import {
|
|
16
|
+
import { useTranscriptSearch } from "./transcriptSearch";
|
|
16
17
|
|
|
17
18
|
/** Render reasoning collapsed with a short preview until expanded or searched. */
|
|
18
19
|
export function TranscriptReasoningView(props: {
|
|
@@ -50,7 +51,7 @@ export function TranscriptReasoningView(props: {
|
|
|
50
51
|
</span>
|
|
51
52
|
) : (
|
|
52
53
|
<span className="block min-w-0 truncate italic text-dashboard-text-muted">
|
|
53
|
-
{rendered}
|
|
54
|
+
<DisplayText inline links={false} text={rendered} />
|
|
54
55
|
</span>
|
|
55
56
|
)
|
|
56
57
|
}
|
|
@@ -67,9 +68,7 @@ export function TranscriptReasoningView(props: {
|
|
|
67
68
|
</TraceCollapsibleTrigger>
|
|
68
69
|
{/* Keep closed reasoning in the page, so browser find can reach it. */}
|
|
69
70
|
<TraceCollapsibleContent className="px-3 py-2.5" hiddenUntilFound>
|
|
70
|
-
<
|
|
71
|
-
<HighlightText text={rendered} />
|
|
72
|
-
</div>
|
|
71
|
+
<DisplayText text={rendered} />
|
|
73
72
|
</TraceCollapsibleContent>
|
|
74
73
|
</TraceCollapsible>
|
|
75
74
|
);
|
|
@@ -17,11 +17,16 @@ export function TranscriptRows({
|
|
|
17
17
|
|
|
18
18
|
/** Leave room for control outlines inside the mobile row's paint bounds. */
|
|
19
19
|
export function TranscriptRow(props: {
|
|
20
|
+
/** Stable row id that holds the reader's place while layout changes. */
|
|
21
|
+
anchorKey?: string;
|
|
20
22
|
children: ReactNode;
|
|
21
23
|
indent?: boolean;
|
|
22
24
|
}) {
|
|
23
25
|
return (
|
|
24
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
className="mobile-transcript-row -mx-2 px-2 py-1.5"
|
|
28
|
+
data-scroll-anchor={props.anchorKey}
|
|
29
|
+
>
|
|
25
30
|
{props.indent ? (
|
|
26
31
|
<div className="pl-11">{props.children}</div>
|
|
27
32
|
) : (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
|
|
2
2
|
import { formatMessageTimestamp } from "../format";
|
|
3
3
|
import type { TranscriptViewStructuredEventPart } from "../types";
|
|
4
|
-
import {
|
|
4
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
5
5
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
6
6
|
import { TranscriptSummaryTrigger } from "./TranscriptSummary";
|
|
7
7
|
|
|
@@ -34,8 +34,8 @@ export function TranscriptStructuredEventView(props: {
|
|
|
34
34
|
</div>
|
|
35
35
|
)}
|
|
36
36
|
{detail.description ? (
|
|
37
|
-
<div className="mt-1
|
|
38
|
-
<
|
|
37
|
+
<div className="mt-1">
|
|
38
|
+
<DisplayText mentions={mentions} text={detail.description} />
|
|
39
39
|
</div>
|
|
40
40
|
) : null}
|
|
41
41
|
{detail.content ? (
|
|
@@ -70,7 +70,8 @@ export function TranscriptStructuredEventView(props: {
|
|
|
70
70
|
</span>
|
|
71
71
|
{presentation.preview ? (
|
|
72
72
|
<span className="mt-0.5 block truncate text-xs text-dashboard-text-muted">
|
|
73
|
-
<
|
|
73
|
+
<DisplayText
|
|
74
|
+
inline
|
|
74
75
|
links={!collapsible}
|
|
75
76
|
mentions={mentions}
|
|
76
77
|
text={presentation.preview}
|
|
@@ -15,6 +15,7 @@ import { reviewItemIdFromToolCall } from "../review-desk/reviewItemFormat";
|
|
|
15
15
|
import { cn } from "../styles";
|
|
16
16
|
import { ToolFrame } from "./ToolFrame";
|
|
17
17
|
import { toolCallPreview } from "./toolCallPreview";
|
|
18
|
+
import { transcriptPinStartProps } from "./transcriptBottomPinning";
|
|
18
19
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
19
20
|
|
|
20
21
|
/** Render one tool invocation as it advances from running to a terminal result. */
|
|
@@ -71,7 +72,7 @@ export const TranscriptToolView = memo(function TranscriptToolView(props: {
|
|
|
71
72
|
<div className="min-w-0">
|
|
72
73
|
{frame}
|
|
73
74
|
{reviewItemId ? (
|
|
74
|
-
<div className="mt-2">
|
|
75
|
+
<div className="mt-2" {...transcriptPinStartProps(reviewItemId)}>
|
|
75
76
|
<TranscriptReviewItemCard itemId={reviewItemId} />
|
|
76
77
|
</div>
|
|
77
78
|
) : null}
|
|
@@ -63,7 +63,6 @@ export function Transcript(props: {
|
|
|
63
63
|
enabled: props.live ?? false,
|
|
64
64
|
historyVersion: props.historyVersion ?? "empty",
|
|
65
65
|
juniorMessageVersion: transcriptJuniorMessageVersion(props.transcript),
|
|
66
|
-
loadingPreviousPage: props.loadingPreviousPage ?? false,
|
|
67
66
|
pinRequestVersion: props.pinRequestVersion,
|
|
68
67
|
view,
|
|
69
68
|
versions: {
|
|
@@ -72,10 +71,7 @@ export function Transcript(props: {
|
|
|
72
71
|
},
|
|
73
72
|
});
|
|
74
73
|
|
|
75
|
-
const loadPreviousPage = () =>
|
|
76
|
-
bottomPinning.preserveViewportForPrepend();
|
|
77
|
-
props.onLoadPreviousPage?.();
|
|
78
|
-
};
|
|
74
|
+
const loadPreviousPage = () => props.onLoadPreviousPage?.();
|
|
79
75
|
const previousPageRef = useAutoLoadPreviousPage({
|
|
80
76
|
enabled: Boolean(
|
|
81
77
|
props.onLoadPreviousPage &&
|
|
@@ -97,7 +93,11 @@ export function Transcript(props: {
|
|
|
97
93
|
|
|
98
94
|
return (
|
|
99
95
|
<TranscriptSearchProvider query={search}>
|
|
100
|
-
|
|
96
|
+
{/* The pinning hook holds the reader's place; browser anchoring would fight it. */}
|
|
97
|
+
<div
|
|
98
|
+
className="grid min-w-0 [overflow-anchor:none]"
|
|
99
|
+
ref={bottomPinning.contentRef}
|
|
100
|
+
>
|
|
101
101
|
{redacted ? (
|
|
102
102
|
<div className="mb-2 min-w-0 break-words text-sm leading-relaxed text-dashboard-text-muted">
|
|
103
103
|
Hidden because this conversation is not public.
|
|
@@ -140,12 +140,22 @@ function sameMailboxMessage(
|
|
|
140
140
|
left.createdAt === right.createdAt &&
|
|
141
141
|
left.receivedAt === right.receivedAt &&
|
|
142
142
|
left.idempotencyKey === right.idempotencyKey &&
|
|
143
|
-
// Pending rows render actorLabel(actorIdentity)
|
|
144
|
-
//
|
|
145
|
-
sameActorIdentity(left.actorIdentity, right.actorIdentity)
|
|
143
|
+
// Pending rows render actorLabel(actorIdentity) and mention names. Live
|
|
144
|
+
// polls may enrich them after the first accept; keep that change visible.
|
|
145
|
+
sameActorIdentity(left.actorIdentity, right.actorIdentity) &&
|
|
146
|
+
sameMentions(left.mentions, right.mentions)
|
|
146
147
|
);
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function sameMentions(
|
|
151
|
+
left: ConversationMailboxMessage["mentions"],
|
|
152
|
+
right: ConversationMailboxMessage["mentions"],
|
|
153
|
+
): boolean {
|
|
154
|
+
if (left === right) return true;
|
|
155
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
156
|
+
return left.every((person, index) => sameActorIdentity(person, right[index]));
|
|
157
|
+
}
|
|
158
|
+
|
|
149
159
|
function sameActorIdentity(
|
|
150
160
|
left: ConversationMailboxMessage["actorIdentity"],
|
|
151
161
|
right: ConversationMailboxMessage["actorIdentity"],
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ConversationReportEvent } from "@sentry/junior/api/schema";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the dashboard route for a conversation id. A `turnId` opens the
|
|
5
|
+
* start of that Turn in the event log.
|
|
6
|
+
*/
|
|
7
|
+
export function conversationPath(
|
|
8
|
+
conversationId: string,
|
|
9
|
+
options?: { turnId?: string },
|
|
10
|
+
): string {
|
|
11
|
+
const path = `/conversations/${encodeURIComponent(conversationId)}`;
|
|
12
|
+
return options?.turnId
|
|
13
|
+
? `${path}?${CONVERSATION_TURN_PARAM}=${encodeURIComponent(options.turnId)}`
|
|
14
|
+
: path;
|
|
4
15
|
}
|
|
5
16
|
|
|
6
17
|
/** Dashboard home route. Legacy `/conversations/new` redirects here. */
|
|
@@ -20,3 +31,19 @@ export function parseConversationEventSeq(
|
|
|
20
31
|
): number | undefined {
|
|
21
32
|
return value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
22
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Query param that opens the start of one Turn in the event log. */
|
|
36
|
+
export const CONVERSATION_TURN_PARAM = "turn";
|
|
37
|
+
|
|
38
|
+
/** Find the sequence number of the event that started a Turn. */
|
|
39
|
+
export function turnStartedEventSeq(
|
|
40
|
+
events: readonly ConversationReportEvent[] | undefined,
|
|
41
|
+
turnId: string,
|
|
42
|
+
): number | undefined {
|
|
43
|
+
return events?.find(
|
|
44
|
+
({ data }) =>
|
|
45
|
+
data.type === "turn_lifecycle" &&
|
|
46
|
+
data.state === "started" &&
|
|
47
|
+
data.turnId === turnId,
|
|
48
|
+
)?.seq;
|
|
49
|
+
}
|
|
@@ -8,6 +8,7 @@ export type ConversationSection = {
|
|
|
8
8
|
|
|
9
9
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
10
|
const SECTION_ORDER = [
|
|
11
|
+
"attention",
|
|
11
12
|
"priority",
|
|
12
13
|
"today",
|
|
13
14
|
"yesterday",
|
|
@@ -17,10 +18,18 @@ const SECTION_ORDER = [
|
|
|
17
18
|
"older",
|
|
18
19
|
] as const;
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Group conversations into progressively broader activity sections.
|
|
23
|
+
*
|
|
24
|
+
* Conversations in `attentionIds` go to the first section, before Priority.
|
|
25
|
+
*/
|
|
21
26
|
export function buildConversationSections(
|
|
22
27
|
conversations: Conversation[],
|
|
23
|
-
options: {
|
|
28
|
+
options: {
|
|
29
|
+
attentionIds?: ReadonlySet<string>;
|
|
30
|
+
nowMs: number;
|
|
31
|
+
timeZone: string;
|
|
32
|
+
},
|
|
24
33
|
): ConversationSection[] {
|
|
25
34
|
const nowDay = calendarDay(options.nowMs, options.timeZone);
|
|
26
35
|
const sections = new Map<string, ConversationSection>();
|
|
@@ -30,12 +39,9 @@ export function buildConversationSections(
|
|
|
30
39
|
|
|
31
40
|
for (const conversation of sorted) {
|
|
32
41
|
const time = activityTime(conversation);
|
|
33
|
-
const section =
|
|
34
|
-
|
|
35
|
-
time,
|
|
36
|
-
nowDay,
|
|
37
|
-
options.timeZone,
|
|
38
|
-
);
|
|
42
|
+
const section = options.attentionIds?.has(conversation.id)
|
|
43
|
+
? { key: "attention", label: "Needs attention" }
|
|
44
|
+
: conversationSection(conversation, time, nowDay, options.timeZone);
|
|
39
45
|
const existing = sections.get(section.key);
|
|
40
46
|
if (existing) {
|
|
41
47
|
existing.conversations.push(conversation);
|
|
@@ -49,6 +55,28 @@ export function buildConversationSections(
|
|
|
49
55
|
);
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
/** Join the conversations that wait on the viewer or finished unseen. */
|
|
59
|
+
export function attentionConversationIds(
|
|
60
|
+
waitingConversationIds: ReadonlySet<string>,
|
|
61
|
+
finishedConversationIds: ReadonlySet<string>,
|
|
62
|
+
): ReadonlySet<string> {
|
|
63
|
+
return new Set([...waitingConversationIds, ...finishedConversationIds]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sort attention conversations to the front. The sort is stable, so each group
|
|
68
|
+
* keeps the feed order. Paginated lists call this before paging, so page 1
|
|
69
|
+
* holds every attention conversation.
|
|
70
|
+
*/
|
|
71
|
+
export function attentionFirst(
|
|
72
|
+
conversations: Conversation[],
|
|
73
|
+
attentionIds: ReadonlySet<string>,
|
|
74
|
+
): Conversation[] {
|
|
75
|
+
const rank = (conversation: Conversation) =>
|
|
76
|
+
attentionIds.has(conversation.id) ? 0 : 1;
|
|
77
|
+
return [...conversations].sort((left, right) => rank(left) - rank(right));
|
|
78
|
+
}
|
|
79
|
+
|
|
52
80
|
function activityTime(conversation: Conversation): number {
|
|
53
81
|
const time = Date.parse(conversation.lastSeenAt);
|
|
54
82
|
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|