@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
|
@@ -17,16 +17,18 @@ import {
|
|
|
17
17
|
import { cn } from "../styles";
|
|
18
18
|
import type { Conversation } from "../types";
|
|
19
19
|
import { ConversationSidebarAnnotations } from "./ConversationMeta";
|
|
20
|
-
import { formatConversationActivityPreview } from "./conversationActivityPreview";
|
|
21
20
|
import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
|
|
22
21
|
import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
|
|
23
22
|
import { conversationPath } from "./conversationRoutes";
|
|
24
23
|
import { useArchiveConversation } from "./queries";
|
|
25
|
-
import {
|
|
24
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
26
25
|
import {
|
|
26
|
+
attentionConversationIds,
|
|
27
27
|
buildConversationSections,
|
|
28
28
|
type ConversationSection,
|
|
29
29
|
} from "./conversationSections";
|
|
30
|
+
import { ConversationSectionHeading } from "./ConversationSectionHeading";
|
|
31
|
+
import { useCollapsedConversationSections } from "./useCollapsedConversationSections";
|
|
30
32
|
|
|
31
33
|
/** Render grouped conversation cards on the dashboard home page. */
|
|
32
34
|
export function ConversationHomeList(props: {
|
|
@@ -37,6 +39,8 @@ export function ConversationHomeList(props: {
|
|
|
37
39
|
timeZone: string;
|
|
38
40
|
waitingConversationIds: ReadonlySet<string>;
|
|
39
41
|
}) {
|
|
42
|
+
const { collapsedKeys, toggle: toggleSection } =
|
|
43
|
+
useCollapsedConversationSections();
|
|
40
44
|
const [archivedConversation, setArchivedConversation] =
|
|
41
45
|
useState<Conversation>();
|
|
42
46
|
const [archiveError, setArchiveError] = useState<{
|
|
@@ -83,6 +87,10 @@ export function ConversationHomeList(props: {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
const sections = buildConversationSections(props.conversations, {
|
|
90
|
+
attentionIds: attentionConversationIds(
|
|
91
|
+
props.waitingConversationIds,
|
|
92
|
+
props.finishedConversationIds,
|
|
93
|
+
),
|
|
86
94
|
nowMs: Date.now(),
|
|
87
95
|
timeZone: props.timeZone,
|
|
88
96
|
});
|
|
@@ -91,10 +99,12 @@ export function ConversationHomeList(props: {
|
|
|
91
99
|
<div aria-label="Your conversations" className="grid gap-5" role="list">
|
|
92
100
|
{sections.map((section) => (
|
|
93
101
|
<ConversationCardSection
|
|
102
|
+
collapsed={collapsedKeys.has(section.key)}
|
|
94
103
|
finishedConversationIds={props.finishedConversationIds}
|
|
95
104
|
key={section.key}
|
|
96
105
|
onArchiveError={handleArchiveError}
|
|
97
106
|
onArchived={handleArchived}
|
|
107
|
+
onToggle={() => toggleSection(section.key)}
|
|
98
108
|
waitingConversationIds={props.waitingConversationIds}
|
|
99
109
|
section={section}
|
|
100
110
|
/>
|
|
@@ -167,34 +177,42 @@ function ConversationCardLoading(props: { index: number }) {
|
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
function ConversationCardSection(props: {
|
|
180
|
+
collapsed: boolean;
|
|
170
181
|
finishedConversationIds: ReadonlySet<string>;
|
|
171
182
|
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
|
|
172
183
|
onArchived(conversation: Conversation): void;
|
|
184
|
+
onToggle(): void;
|
|
173
185
|
section: ConversationSection;
|
|
174
186
|
waitingConversationIds: ReadonlySet<string>;
|
|
175
187
|
}) {
|
|
176
188
|
return (
|
|
177
189
|
<section aria-labelledby={`conversation-section-${props.section.key}`}>
|
|
178
|
-
<
|
|
179
|
-
className="
|
|
190
|
+
<ConversationSectionHeading
|
|
191
|
+
className={cn("px-1", !props.collapsed && "mb-2")}
|
|
192
|
+
collapsed={props.collapsed}
|
|
193
|
+
count={props.section.conversations.length}
|
|
180
194
|
id={`conversation-section-${props.section.key}`}
|
|
181
|
-
|
|
182
|
-
{props.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
label={props.section.label}
|
|
196
|
+
onToggle={props.onToggle}
|
|
197
|
+
/>
|
|
198
|
+
{props.collapsed ? null : (
|
|
199
|
+
<div className="grid gap-2">
|
|
200
|
+
{props.section.conversations.map((conversation) => (
|
|
201
|
+
<ConversationCard
|
|
202
|
+
conversation={conversation}
|
|
203
|
+
finishedSinceSeen={props.finishedConversationIds.has(
|
|
204
|
+
conversation.id,
|
|
205
|
+
)}
|
|
206
|
+
key={conversation.id}
|
|
207
|
+
onArchiveError={props.onArchiveError}
|
|
208
|
+
onArchived={props.onArchived}
|
|
209
|
+
waitingOnViewer={props.waitingConversationIds.has(
|
|
210
|
+
conversation.id,
|
|
211
|
+
)}
|
|
212
|
+
/>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
198
216
|
</section>
|
|
199
217
|
);
|
|
200
218
|
}
|
|
@@ -272,11 +290,10 @@ function ConversationCard(props: {
|
|
|
272
290
|
</div>
|
|
273
291
|
{conversation.activityPreview ? (
|
|
274
292
|
<p className="relative z-[1] m-0 line-clamp-2 font-sans text-sm leading-relaxed text-dashboard-text-subtle pointer-events-none">
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
)}
|
|
293
|
+
<DisplayText
|
|
294
|
+
inline
|
|
295
|
+
mentions={conversation.activityPreview.mentions}
|
|
296
|
+
text={conversation.activityPreview.text}
|
|
280
297
|
/>
|
|
281
298
|
</p>
|
|
282
299
|
) : (
|
|
@@ -31,7 +31,9 @@ import { CopyButton } from "../components/CopyButton";
|
|
|
31
31
|
import { ConversationBrief } from "./ConversationBrief";
|
|
32
32
|
import {
|
|
33
33
|
CONVERSATION_EVENT_PARAM,
|
|
34
|
+
CONVERSATION_TURN_PARAM,
|
|
34
35
|
parseConversationEventSeq,
|
|
36
|
+
turnStartedEventSeq,
|
|
35
37
|
} from "./conversationRoutes";
|
|
36
38
|
import {
|
|
37
39
|
ConversationComposer,
|
|
@@ -89,6 +91,12 @@ import {
|
|
|
89
91
|
import { ChatLayout } from "./ChatLayout";
|
|
90
92
|
import { ComposerDock } from "./ComposerDock";
|
|
91
93
|
import { ConversationReviewBar } from "../review-desk/ConversationReviewBar";
|
|
94
|
+
import { useReviewItemData } from "../review-desk/queries";
|
|
95
|
+
import {
|
|
96
|
+
latestReviewItemId,
|
|
97
|
+
reviewItemWaitsForAnswer,
|
|
98
|
+
} from "../review-desk/reviewItemFormat";
|
|
99
|
+
import { useConfirmUnsentAnswers } from "../review-desk/UnsentAnswers";
|
|
92
100
|
import { Transcript } from "./TranscriptView";
|
|
93
101
|
import { TranscriptLoading } from "./TranscriptLoading";
|
|
94
102
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
@@ -132,20 +140,24 @@ export function ConversationPage(props: {
|
|
|
132
140
|
const selectedEventSeq = parseConversationEventSeq(
|
|
133
141
|
searchParams.get(CONVERSATION_EVENT_PARAM),
|
|
134
142
|
);
|
|
143
|
+
// A linked Turn opens the event that started it. Until that event is
|
|
144
|
+
// found, it acts like a linked event that is still loading.
|
|
145
|
+
const linkedTurnId = searchParams.get(CONVERSATION_TURN_PARAM) ?? undefined;
|
|
146
|
+
const eventOpen =
|
|
147
|
+
selectedEventSeq !== undefined || linkedTurnId !== undefined;
|
|
135
148
|
// Event details and turn context show over Conversation details. Closing
|
|
136
149
|
// them shows details again when details are open. A linked event starts
|
|
137
150
|
// with details closed, so closing it does not open a panel nobody chose.
|
|
138
151
|
const [detailsChoice, setDetailsChoice] = useState<boolean | undefined>(() =>
|
|
139
|
-
|
|
152
|
+
eventOpen ? false : undefined,
|
|
140
153
|
);
|
|
141
154
|
const detailsOpen = detailsChoice ?? detailsDefaultOpen;
|
|
142
155
|
const [turnContext, setTurnContext] = useState<TurnContextSelection>();
|
|
143
|
-
const eventOpen = selectedEventSeq !== undefined;
|
|
144
156
|
const showDetails = detailsOpen && !eventOpen && !turnContext;
|
|
145
157
|
const [viewState, setView] = useState<TranscriptViewMode>(() =>
|
|
146
|
-
|
|
158
|
+
eventOpen ? "raw" : "rich",
|
|
147
159
|
);
|
|
148
|
-
const view =
|
|
160
|
+
const view = eventOpen ? "raw" : viewState;
|
|
149
161
|
const [search, setSearch] = useState("");
|
|
150
162
|
const [pinRequestVersion, setPinRequestVersion] = useState(0);
|
|
151
163
|
const conversationId = props.conversationId;
|
|
@@ -192,6 +204,10 @@ export function ConversationPage(props: {
|
|
|
192
204
|
() => committedMessageIds(detail.data?.events),
|
|
193
205
|
[detail.data?.events],
|
|
194
206
|
);
|
|
207
|
+
const reviewItemId = useMemo(
|
|
208
|
+
() => latestReviewItemId(detail.data?.events),
|
|
209
|
+
[detail.data?.events],
|
|
210
|
+
);
|
|
195
211
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
196
212
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
197
213
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
@@ -203,6 +219,7 @@ export function ConversationPage(props: {
|
|
|
203
219
|
setSearchParams(
|
|
204
220
|
(current) => {
|
|
205
221
|
const next = new URLSearchParams(current);
|
|
222
|
+
next.delete(CONVERSATION_TURN_PARAM);
|
|
206
223
|
if (seq === undefined) next.delete(CONVERSATION_EVENT_PARAM);
|
|
207
224
|
else next.set(CONVERSATION_EVENT_PARAM, String(seq));
|
|
208
225
|
return next;
|
|
@@ -214,14 +231,14 @@ export function ConversationPage(props: {
|
|
|
214
231
|
);
|
|
215
232
|
const onViewChange = useCallback(
|
|
216
233
|
(value: TranscriptViewMode) => {
|
|
217
|
-
if (value !== "raw" &&
|
|
234
|
+
if (value !== "raw" && eventOpen) {
|
|
218
235
|
setSelectedEventSeq(undefined);
|
|
219
236
|
}
|
|
220
237
|
// Turn context belongs to a message in the Conversation view.
|
|
221
238
|
setTurnContext(undefined);
|
|
222
239
|
setView(value);
|
|
223
240
|
},
|
|
224
|
-
[
|
|
241
|
+
[eventOpen, setSelectedEventSeq],
|
|
225
242
|
);
|
|
226
243
|
// A linked event can be older than the first page. Load earlier pages until
|
|
227
244
|
// it is present or history ends.
|
|
@@ -240,12 +257,41 @@ export function ConversationPage(props: {
|
|
|
240
257
|
useEffect(() => {
|
|
241
258
|
if (loadLinkedEvent) loadPreviousPageRef.current();
|
|
242
259
|
}, [loadLinkedEvent, oldestLoadedSeq]);
|
|
243
|
-
//
|
|
260
|
+
// Load earlier pages until the linked Turn starts or history ends. Then
|
|
261
|
+
// replace the Turn link with a link to its first event, or drop it when
|
|
262
|
+
// history has no such event. A history error keeps the link, so a retry
|
|
263
|
+
// or reload can still find the Turn.
|
|
264
|
+
const linkedTurnSeq = linkedTurnId
|
|
265
|
+
? turnStartedEventSeq(transcript?.events, linkedTurnId)
|
|
266
|
+
: undefined;
|
|
267
|
+
const linkedTurnMissing =
|
|
268
|
+
linkedTurnId !== undefined &&
|
|
269
|
+
linkedTurnSeq === undefined &&
|
|
270
|
+
transcript !== undefined;
|
|
271
|
+
const loadLinkedTurn =
|
|
272
|
+
linkedTurnMissing &&
|
|
273
|
+
detail.hasPreviousPage &&
|
|
274
|
+
!detail.isLoadingPreviousPage &&
|
|
275
|
+
!detail.historyError;
|
|
276
|
+
const linkedTurnNotFound =
|
|
277
|
+
linkedTurnMissing &&
|
|
278
|
+
!detail.hasPreviousPage &&
|
|
279
|
+
!detail.isLoadingPreviousPage;
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (loadLinkedTurn) loadPreviousPageRef.current();
|
|
282
|
+
}, [loadLinkedTurn, oldestLoadedSeq]);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (linkedTurnSeq !== undefined) setSelectedEventSeq(linkedTurnSeq);
|
|
285
|
+
else if (linkedTurnNotFound) setSelectedEventSeq(undefined);
|
|
286
|
+
}, [linkedTurnNotFound, linkedTurnSeq, setSelectedEventSeq]);
|
|
287
|
+
// Keep the panel while the transcript or a linked event loads, and for the
|
|
288
|
+
// render before a found Turn link becomes an event link.
|
|
244
289
|
const eventLoading =
|
|
245
290
|
eventOpen &&
|
|
246
291
|
!selectedEvent &&
|
|
247
292
|
(detail.isPending ||
|
|
248
|
-
|
|
293
|
+
linkedTurnSeq !== undefined ||
|
|
294
|
+
((linkedEventMissing || linkedTurnMissing) &&
|
|
249
295
|
(detail.hasPreviousPage || detail.isLoadingPreviousPage) &&
|
|
250
296
|
!detail.historyError));
|
|
251
297
|
const showEvent = Boolean(selectedEvent) || eventLoading;
|
|
@@ -475,6 +521,7 @@ export function ConversationPage(props: {
|
|
|
475
521
|
// every 2s; a prop would bust footer memo while the reader types.
|
|
476
522
|
pendingGeneratedAtRef={pendingGeneratedAtRef}
|
|
477
523
|
pendingMessages={detail.pendingMessages}
|
|
524
|
+
reviewItemId={reviewItemId}
|
|
478
525
|
// Without live history, an inline send would never leave the
|
|
479
526
|
// transcript tail. A started Turn reads a new send only later.
|
|
480
527
|
// Keep those sends in the queue. An earlier inline send that
|
|
@@ -568,6 +615,8 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
568
615
|
pendingAuthorization?: ConversationPendingMessagesReport["authorization"];
|
|
569
616
|
pendingGeneratedAtRef: { current: string | undefined };
|
|
570
617
|
pendingMessages: readonly ConversationMailboxMessage[];
|
|
618
|
+
/** The last Review item that Junior added in the transcript. */
|
|
619
|
+
reviewItemId?: string;
|
|
571
620
|
/**
|
|
572
621
|
* True when history is live and no Turn has started, so the next Turn will
|
|
573
622
|
* read a new send at once.
|
|
@@ -577,6 +626,9 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
577
626
|
stoppable: boolean;
|
|
578
627
|
}) {
|
|
579
628
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
629
|
+
// A message closes the review items that it answers. Ask before it drops
|
|
630
|
+
// answers that the reader did not send.
|
|
631
|
+
const unsentAnswers = useConfirmUnsentAnswers();
|
|
580
632
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
581
633
|
const stopTurnRef = useRef(stopTurn);
|
|
582
634
|
stopTurnRef.current = stopTurn;
|
|
@@ -586,6 +638,31 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
586
638
|
const [editTargetInboundMessageId, setEditTargetInboundMessageId] =
|
|
587
639
|
useState<string>();
|
|
588
640
|
const [queueActionError, setQueueActionError] = useState<string>();
|
|
641
|
+
// While Junior waits for an answer, hide the message box, the review bar,
|
|
642
|
+
// and the queued messages, so the reader answers in the question and does
|
|
643
|
+
// not type the answer as a new message.
|
|
644
|
+
// Dismiss on the question closes the item and shows the message box again.
|
|
645
|
+
// The message box stays mounted, so its text and images stay, and it does
|
|
646
|
+
// not hide while it has focus.
|
|
647
|
+
const reviewItem = useReviewItemData(props.reviewItemId).data;
|
|
648
|
+
const [composerShownForItemId, setComposerShownForItemId] =
|
|
649
|
+
useState<string>();
|
|
650
|
+
const [composerFocused, setComposerFocused] = useState(false);
|
|
651
|
+
const question =
|
|
652
|
+
reviewItem &&
|
|
653
|
+
!props.stoppable &&
|
|
654
|
+
!composerFocused &&
|
|
655
|
+
reviewItemWaitsForAnswer(reviewItem) &&
|
|
656
|
+
composerShownForItemId !== reviewItem.id
|
|
657
|
+
? reviewItem
|
|
658
|
+
: undefined;
|
|
659
|
+
// The message box can show only because it has focus. Using the review bar
|
|
660
|
+
// or queued messages moves focus out of it, so keep them shown for this item.
|
|
661
|
+
const keepComposerShown = () => {
|
|
662
|
+
if (composerFocused && reviewItem && reviewItemWaitsForAnswer(reviewItem)) {
|
|
663
|
+
setComposerShownForItemId(reviewItem.id);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
589
666
|
// Keep submit identity stable across mutation status flips so the memoized
|
|
590
667
|
// composer does not re-render while the reader is still typing.
|
|
591
668
|
const appendMessageRef = useRef(appendMessage);
|
|
@@ -777,41 +854,61 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
777
854
|
{props.pendingAuthorization ? (
|
|
778
855
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
779
856
|
) : null}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
857
|
+
{/* The review bar and queued messages hide with the message box, so
|
|
858
|
+
the question is the last thing above the dock. */}
|
|
859
|
+
<div
|
|
860
|
+
hidden={Boolean(question)}
|
|
861
|
+
onFocus={keepComposerShown}
|
|
862
|
+
onPointerDown={keepComposerShown}
|
|
863
|
+
>
|
|
864
|
+
<ConversationReviewBar conversationId={props.conversationId} />
|
|
865
|
+
<PendingMailboxStack
|
|
866
|
+
cancelError={cancelError}
|
|
867
|
+
cancelPending={cancelPendingMessages.isPending}
|
|
868
|
+
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
869
|
+
committedMessageIds={props.committedMessageIds}
|
|
870
|
+
conversationId={props.conversationId}
|
|
871
|
+
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
872
|
+
messages={props.pendingMessages}
|
|
873
|
+
onCancelMessage={onCancelMessage}
|
|
874
|
+
onEditMessage={onEditMessage}
|
|
875
|
+
onLayoutChange={onMailboxLayoutChange}
|
|
876
|
+
onRetry={onRetry}
|
|
877
|
+
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
878
|
+
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
879
|
+
viewerEmail={viewerEmail}
|
|
880
|
+
/>
|
|
881
|
+
</div>
|
|
882
|
+
{unsentAnswers.dialog}
|
|
797
883
|
</>
|
|
798
884
|
}
|
|
799
885
|
>
|
|
800
|
-
<
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
886
|
+
<div
|
|
887
|
+
hidden={Boolean(question)}
|
|
888
|
+
onBlur={(event) => {
|
|
889
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
890
|
+
setComposerFocused(false);
|
|
891
|
+
}
|
|
892
|
+
}}
|
|
893
|
+
onFocus={() => setComposerFocused(true)}
|
|
894
|
+
>
|
|
895
|
+
<ConversationComposer
|
|
896
|
+
confirmSubmit={unsentAnswers.confirmSubmit}
|
|
897
|
+
draftId={props.conversationId}
|
|
898
|
+
error={
|
|
899
|
+
queueActionError ??
|
|
900
|
+
(stopTurn.error ? "Could not stop Junior. Try again." : undefined)
|
|
901
|
+
}
|
|
902
|
+
ref={composerRef}
|
|
903
|
+
label="Continue this conversation"
|
|
904
|
+
submitLabel="Send"
|
|
905
|
+
onFocus={onComposerFocus}
|
|
906
|
+
onStop={props.stoppable ? onStop : undefined}
|
|
907
|
+
stopPending={stopTurn.isPending}
|
|
908
|
+
onSubmitStart={onSubmitStart}
|
|
909
|
+
onSubmit={onSubmit}
|
|
910
|
+
/>
|
|
911
|
+
</div>
|
|
815
912
|
</ComposerDock>
|
|
816
913
|
);
|
|
817
914
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../styles";
|
|
4
|
+
|
|
5
|
+
/** Render a conversation section label that collapses its rows. */
|
|
6
|
+
export function ConversationSectionHeading(props: {
|
|
7
|
+
className?: string;
|
|
8
|
+
collapsed: boolean;
|
|
9
|
+
count: number;
|
|
10
|
+
id?: string;
|
|
11
|
+
label: string;
|
|
12
|
+
onToggle(): void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<h3
|
|
16
|
+
className={cn(
|
|
17
|
+
"m-0 font-sans text-xs font-medium text-dashboard-text-muted",
|
|
18
|
+
props.className,
|
|
19
|
+
)}
|
|
20
|
+
id={props.id}
|
|
21
|
+
>
|
|
22
|
+
<button
|
|
23
|
+
aria-expanded={!props.collapsed}
|
|
24
|
+
className="flex cursor-pointer items-center gap-1 rounded-sm border-0 bg-transparent p-0 font-[inherit] text-inherit transition-colors hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
25
|
+
onClick={props.onToggle}
|
|
26
|
+
type="button"
|
|
27
|
+
>
|
|
28
|
+
<ChevronRight
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
className={cn(
|
|
31
|
+
"size-3 shrink-0 transition-transform motion-reduce:transition-none",
|
|
32
|
+
!props.collapsed && "rotate-90",
|
|
33
|
+
)}
|
|
34
|
+
/>
|
|
35
|
+
{props.label}
|
|
36
|
+
{props.collapsed ? (
|
|
37
|
+
<span className="tabular-nums text-dashboard-text-faint">
|
|
38
|
+
{props.count}
|
|
39
|
+
</span>
|
|
40
|
+
) : null}
|
|
41
|
+
</button>
|
|
42
|
+
</h3>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -14,9 +14,12 @@ import type { Conversation } from "../types";
|
|
|
14
14
|
import { Notice, NoticeAction } from "../components/Notice";
|
|
15
15
|
import { AnimatedList } from "./AnimatedList";
|
|
16
16
|
import {
|
|
17
|
+
attentionConversationIds,
|
|
17
18
|
buildConversationSections,
|
|
18
19
|
type ConversationSection,
|
|
19
20
|
} from "./conversationSections";
|
|
21
|
+
import { ConversationSectionHeading } from "./ConversationSectionHeading";
|
|
22
|
+
import { useCollapsedConversationSections } from "./useCollapsedConversationSections";
|
|
20
23
|
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
21
24
|
import { SearchInput } from "../components/SearchInput";
|
|
22
25
|
import { Button } from "../components/ui/button";
|
|
@@ -30,7 +33,15 @@ import { ConversationSidebarAnnotations } from "./ConversationMeta";
|
|
|
30
33
|
import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
|
|
31
34
|
|
|
32
35
|
type ConversationSidebarEntry =
|
|
33
|
-
| {
|
|
36
|
+
| {
|
|
37
|
+
collapsed: boolean;
|
|
38
|
+
count: number;
|
|
39
|
+
first: boolean;
|
|
40
|
+
key: string;
|
|
41
|
+
kind: "section";
|
|
42
|
+
label: string;
|
|
43
|
+
sectionKey: string;
|
|
44
|
+
}
|
|
34
45
|
| { conversation: Conversation; key: string; kind: "conversation" };
|
|
35
46
|
|
|
36
47
|
// Tracks the attempted direction alongside the conversation so a failed
|
|
@@ -78,17 +89,30 @@ export function ConversationSidebar(props: {
|
|
|
78
89
|
);
|
|
79
90
|
setArchivedConversation(conversation);
|
|
80
91
|
}, []);
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
const { collapsedKeys, toggle: toggleSection } =
|
|
93
|
+
useCollapsedConversationSections();
|
|
94
|
+
// Rebuild section rows only when their inputs change. Avoid fresh Date.now()
|
|
95
|
+
// arrays on unrelated parent renders while the reader is scrolling.
|
|
83
96
|
const entries = useMemo(
|
|
84
97
|
() =>
|
|
85
98
|
conversationSidebarEntries(
|
|
86
99
|
buildConversationSections(props.conversations, {
|
|
100
|
+
attentionIds: attentionConversationIds(
|
|
101
|
+
props.waitingConversationIds,
|
|
102
|
+
props.finishedConversationIds,
|
|
103
|
+
),
|
|
87
104
|
nowMs: Date.now(),
|
|
88
105
|
timeZone: props.timeZone,
|
|
89
106
|
}),
|
|
107
|
+
collapsedKeys,
|
|
90
108
|
),
|
|
91
|
-
[
|
|
109
|
+
[
|
|
110
|
+
collapsedKeys,
|
|
111
|
+
props.conversations,
|
|
112
|
+
props.finishedConversationIds,
|
|
113
|
+
props.timeZone,
|
|
114
|
+
props.waitingConversationIds,
|
|
115
|
+
],
|
|
92
116
|
);
|
|
93
117
|
return (
|
|
94
118
|
<aside
|
|
@@ -170,14 +194,13 @@ export function ConversationSidebar(props: {
|
|
|
170
194
|
items={entries}
|
|
171
195
|
renderItem={(entry) =>
|
|
172
196
|
entry.kind === "section" ? (
|
|
173
|
-
<
|
|
174
|
-
className={cn(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
</h3>
|
|
197
|
+
<ConversationSectionHeading
|
|
198
|
+
className={cn("px-3 pb-1", entry.first ? "pt-1" : "pt-3")}
|
|
199
|
+
collapsed={entry.collapsed}
|
|
200
|
+
count={entry.count}
|
|
201
|
+
label={entry.label}
|
|
202
|
+
onToggle={() => toggleSection(entry.sectionKey)}
|
|
203
|
+
/>
|
|
181
204
|
) : (
|
|
182
205
|
<ConversationSidebarRow
|
|
183
206
|
conversation={entry.conversation}
|
|
@@ -253,20 +276,27 @@ function ConversationRowsLoading() {
|
|
|
253
276
|
|
|
254
277
|
function conversationSidebarEntries(
|
|
255
278
|
sections: ConversationSection[],
|
|
279
|
+
collapsedKeys: ReadonlySet<string>,
|
|
256
280
|
): ConversationSidebarEntry[] {
|
|
257
|
-
return sections.flatMap((section, index) =>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
return sections.flatMap((section, index) => {
|
|
282
|
+
const collapsed = collapsedKeys.has(section.key);
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
collapsed,
|
|
286
|
+
count: section.conversations.length,
|
|
287
|
+
first: index === 0,
|
|
288
|
+
key: `section-${section.key}`,
|
|
289
|
+
kind: "section" as const,
|
|
290
|
+
label: section.label,
|
|
291
|
+
sectionKey: section.key,
|
|
292
|
+
},
|
|
293
|
+
...(collapsed ? [] : section.conversations).map((conversation) => ({
|
|
294
|
+
conversation,
|
|
295
|
+
key: `conversation-${conversation.id}`,
|
|
296
|
+
kind: "conversation" as const,
|
|
297
|
+
})),
|
|
298
|
+
];
|
|
299
|
+
});
|
|
270
300
|
}
|
|
271
301
|
|
|
272
302
|
const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
@@ -360,7 +360,7 @@ function TranscriptEntryList(props: {
|
|
|
360
360
|
knownKeys: activityKeys.current,
|
|
361
361
|
});
|
|
362
362
|
rows.push(
|
|
363
|
-
<TranscriptRow indent key={activityKey}>
|
|
363
|
+
<TranscriptRow anchorKey={activityKey} indent key={activityKey}>
|
|
364
364
|
<TranscriptActivityGroup
|
|
365
365
|
entries={visibleEntries}
|
|
366
366
|
renderEntry={renderEntry}
|
|
@@ -372,8 +372,9 @@ function TranscriptEntryList(props: {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
if (!search.active || entryMatchesSearch(entry, search.normalizedQuery)) {
|
|
375
|
+
const rowKey = `${props.keyPrefix}:${entry.key}`;
|
|
375
376
|
rows.push(
|
|
376
|
-
<TranscriptRow
|
|
377
|
+
<TranscriptRow anchorKey={rowKey} key={rowKey}>
|
|
377
378
|
{renderEntry(entry)}
|
|
378
379
|
</TranscriptRow>,
|
|
379
380
|
);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type ConversationWatch,
|
|
22
22
|
} from "@sentry/junior/api/schema";
|
|
23
23
|
|
|
24
|
+
import { DisplayText } from "../display-text/DisplayText";
|
|
24
25
|
import { formatTime } from "../format";
|
|
25
26
|
import { fetchDashboardJson } from "../http";
|
|
26
27
|
import { dashboardDisclosureChevronClass } from "../styles";
|
|
@@ -106,7 +107,9 @@ function WatchRow(props: { watch: ConversationWatch }) {
|
|
|
106
107
|
{/* Indent under the title: icon column, column gap, and row padding. */}
|
|
107
108
|
<div className="grid min-w-0 gap-1.5 pb-2 pl-9 pr-1.5 pt-0.5 text-xs leading-4 text-dashboard-text-muted">
|
|
108
109
|
{watch.intent ? (
|
|
109
|
-
<p className="m-0 break-words">
|
|
110
|
+
<p className="m-0 break-words">
|
|
111
|
+
<DisplayText inline text={watch.intent} />
|
|
112
|
+
</p>
|
|
110
113
|
) : null}
|
|
111
114
|
{watch.firesAt ? (
|
|
112
115
|
<p className="m-0">
|