@sentry/junior-dashboard 0.268.0 → 0.270.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/README.md +7 -105
- package/dist/app.js +12 -9
- package/dist/client.js +81 -81
- package/dist/index.js +12 -9
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +2 -1
- package/src/app.ts +11 -7
- package/src/client/App.tsx +15 -9
- package/src/client/api.ts +4 -19
- package/src/client/components/AGENTS.md +65 -0
- package/src/client/components/ActorAvatar.tsx +1 -1
- package/src/client/components/AutomationCard.tsx +2 -1
- package/src/client/components/ParticipantAvatarStack.tsx +1 -0
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +110 -37
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/conversations/AGENTS.md +38 -0
- 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 +90 -56
- 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/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/format.ts +7 -23
- package/src/client/pages/automations/AGENTS.md +34 -0
- package/src/client/pages/automations/AutomationActions.tsx +18 -20
- package/src/client/pages/automations/AutomationEditor.tsx +3 -4
- package/src/client/pages/automations/AutomationFilters.tsx +6 -6
- package/src/client/pages/automations/AutomationOverview.tsx +5 -5
- package/src/client/pages/automations/AutomationStatus.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/system/AGENTS.md +12 -0
- package/src/client/review-desk/ConversationReviewBar.tsx +14 -9
- package/src/client/review-desk/ReviewDeskPage.tsx +99 -79
- package/src/client/review-desk/ReviewItemActions.tsx +6 -2
- package/src/client/review-desk/ReviewItemCard.tsx +58 -17
- package/src/client/review-desk/ReviewItemDetail.tsx +17 -9
- package/src/client/review-desk/UnsentAnswers.tsx +148 -0
- package/src/client/review-desk/queries.ts +25 -16
- package/src/client/review-desk/reviewItemFormat.ts +29 -1
- package/src/mock-reporting/routes.ts +1 -1
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -219
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/TranscriptText.tsx +0 -47
- package/src/client/conversations/UserInputRequestCard.tsx +0 -39
- package/src/client/conversations/conversationActivityPreview.ts +0 -16
- package/src/client/conversations/transcriptMentions.tsx +0 -134
- package/src/client/conversations/userInputRequest.ts +0 -56
- package/src/client/pages/automations/README.md +0 -97
- package/src/client/pages/system/README.md +0 -23
|
@@ -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
|
) : (
|
|
@@ -73,11 +73,6 @@ import {
|
|
|
73
73
|
hasConversationUsageBreakdown,
|
|
74
74
|
} from "./ConversationUsageBreakdown";
|
|
75
75
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
76
|
-
import { UserInputRequestCard } from "./UserInputRequestCard";
|
|
77
|
-
import {
|
|
78
|
-
openUserInputRequest,
|
|
79
|
-
type OpenUserInputRequest,
|
|
80
|
-
} from "./userInputRequest";
|
|
81
76
|
import {
|
|
82
77
|
buildConversations,
|
|
83
78
|
conversationDetailTitle,
|
|
@@ -94,6 +89,12 @@ import {
|
|
|
94
89
|
import { ChatLayout } from "./ChatLayout";
|
|
95
90
|
import { ComposerDock } from "./ComposerDock";
|
|
96
91
|
import { ConversationReviewBar } from "../review-desk/ConversationReviewBar";
|
|
92
|
+
import { useReviewItemData } from "../review-desk/queries";
|
|
93
|
+
import {
|
|
94
|
+
latestReviewItemId,
|
|
95
|
+
reviewItemWaitsForAnswer,
|
|
96
|
+
} from "../review-desk/reviewItemFormat";
|
|
97
|
+
import { useConfirmUnsentAnswers } from "../review-desk/UnsentAnswers";
|
|
97
98
|
import { Transcript } from "./TranscriptView";
|
|
98
99
|
import { TranscriptLoading } from "./TranscriptLoading";
|
|
99
100
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
@@ -197,8 +198,8 @@ export function ConversationPage(props: {
|
|
|
197
198
|
() => committedMessageIds(detail.data?.events),
|
|
198
199
|
[detail.data?.events],
|
|
199
200
|
);
|
|
200
|
-
const
|
|
201
|
-
() =>
|
|
201
|
+
const reviewItemId = useMemo(
|
|
202
|
+
() => latestReviewItemId(detail.data?.events),
|
|
202
203
|
[detail.data?.events],
|
|
203
204
|
);
|
|
204
205
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
@@ -484,6 +485,7 @@ export function ConversationPage(props: {
|
|
|
484
485
|
// every 2s; a prop would bust footer memo while the reader types.
|
|
485
486
|
pendingGeneratedAtRef={pendingGeneratedAtRef}
|
|
486
487
|
pendingMessages={detail.pendingMessages}
|
|
488
|
+
reviewItemId={reviewItemId}
|
|
487
489
|
// Without live history, an inline send would never leave the
|
|
488
490
|
// transcript tail. A started Turn reads a new send only later.
|
|
489
491
|
// Keep those sends in the queue. An earlier inline send that
|
|
@@ -494,7 +496,6 @@ export function ConversationPage(props: {
|
|
|
494
496
|
detail.inlineMessages.length === 0
|
|
495
497
|
}
|
|
496
498
|
stoppable={live}
|
|
497
|
-
userInputRequest={userInputRequest}
|
|
498
499
|
/>
|
|
499
500
|
) : undefined
|
|
500
501
|
}
|
|
@@ -578,6 +579,8 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
578
579
|
pendingAuthorization?: ConversationPendingMessagesReport["authorization"];
|
|
579
580
|
pendingGeneratedAtRef: { current: string | undefined };
|
|
580
581
|
pendingMessages: readonly ConversationMailboxMessage[];
|
|
582
|
+
/** The last Review item that Junior added in the transcript. */
|
|
583
|
+
reviewItemId?: string;
|
|
581
584
|
/**
|
|
582
585
|
* True when history is live and no Turn has started, so the next Turn will
|
|
583
586
|
* read a new send at once.
|
|
@@ -585,10 +588,11 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
585
588
|
sendsInline: boolean;
|
|
586
589
|
/** True while the conversation has a Turn that a stop can end. */
|
|
587
590
|
stoppable: boolean;
|
|
588
|
-
/** Junior's open question, if no user message answers it yet. */
|
|
589
|
-
userInputRequest?: OpenUserInputRequest;
|
|
590
591
|
}) {
|
|
591
592
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
593
|
+
// A message closes the review items that it answers. Ask before it drops
|
|
594
|
+
// answers that the reader did not send.
|
|
595
|
+
const unsentAnswers = useConfirmUnsentAnswers();
|
|
592
596
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
593
597
|
const stopTurnRef = useRef(stopTurn);
|
|
594
598
|
stopTurnRef.current = stopTurn;
|
|
@@ -598,6 +602,31 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
598
602
|
const [editTargetInboundMessageId, setEditTargetInboundMessageId] =
|
|
599
603
|
useState<string>();
|
|
600
604
|
const [queueActionError, setQueueActionError] = useState<string>();
|
|
605
|
+
// While Junior waits for an answer, hide the message box, the review bar,
|
|
606
|
+
// and the queued messages, so the reader answers in the question and does
|
|
607
|
+
// not type the answer as a new message.
|
|
608
|
+
// Dismiss on the question closes the item and shows the message box again.
|
|
609
|
+
// The message box stays mounted, so its text and images stay, and it does
|
|
610
|
+
// not hide while it has focus.
|
|
611
|
+
const reviewItem = useReviewItemData(props.reviewItemId).data;
|
|
612
|
+
const [composerShownForItemId, setComposerShownForItemId] =
|
|
613
|
+
useState<string>();
|
|
614
|
+
const [composerFocused, setComposerFocused] = useState(false);
|
|
615
|
+
const question =
|
|
616
|
+
reviewItem &&
|
|
617
|
+
!props.stoppable &&
|
|
618
|
+
!composerFocused &&
|
|
619
|
+
reviewItemWaitsForAnswer(reviewItem) &&
|
|
620
|
+
composerShownForItemId !== reviewItem.id
|
|
621
|
+
? reviewItem
|
|
622
|
+
: undefined;
|
|
623
|
+
// The message box can show only because it has focus. Using the review bar
|
|
624
|
+
// or queued messages moves focus out of it, so keep them shown for this item.
|
|
625
|
+
const keepComposerShown = () => {
|
|
626
|
+
if (composerFocused && reviewItem && reviewItemWaitsForAnswer(reviewItem)) {
|
|
627
|
+
setComposerShownForItemId(reviewItem.id);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
601
630
|
// Keep submit identity stable across mutation status flips so the memoized
|
|
602
631
|
// composer does not re-render while the reader is still typing.
|
|
603
632
|
const appendMessageRef = useRef(appendMessage);
|
|
@@ -665,13 +694,6 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
665
694
|
const cancellableMessageIds = props.pendingMessages
|
|
666
695
|
.filter((message) => message.clientStatus === undefined)
|
|
667
696
|
.map((message) => message.inboundMessageId);
|
|
668
|
-
const onAnswerUserInput = useCallback(async (answer: string) => {
|
|
669
|
-
onPinRequestRef.current();
|
|
670
|
-
await appendMessageRef.current.mutateAsync({
|
|
671
|
-
idempotencyKey: crypto.randomUUID(),
|
|
672
|
-
message: answer,
|
|
673
|
-
});
|
|
674
|
-
}, []);
|
|
675
697
|
const onCancelMessage = useCallback(
|
|
676
698
|
(message: ConversationMailboxMessage) => {
|
|
677
699
|
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
@@ -796,49 +818,61 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
796
818
|
{props.pendingAuthorization ? (
|
|
797
819
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
798
820
|
) : null}
|
|
799
|
-
{/*
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
821
|
+
{/* The review bar and queued messages hide with the message box, so
|
|
822
|
+
the question is the last thing above the dock. */}
|
|
823
|
+
<div
|
|
824
|
+
hidden={Boolean(question)}
|
|
825
|
+
onFocus={keepComposerShown}
|
|
826
|
+
onPointerDown={keepComposerShown}
|
|
827
|
+
>
|
|
828
|
+
<ConversationReviewBar conversationId={props.conversationId} />
|
|
829
|
+
<PendingMailboxStack
|
|
830
|
+
cancelError={cancelError}
|
|
831
|
+
cancelPending={cancelPendingMessages.isPending}
|
|
832
|
+
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
833
|
+
committedMessageIds={props.committedMessageIds}
|
|
834
|
+
conversationId={props.conversationId}
|
|
835
|
+
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
836
|
+
messages={props.pendingMessages}
|
|
837
|
+
onCancelMessage={onCancelMessage}
|
|
838
|
+
onEditMessage={onEditMessage}
|
|
839
|
+
onLayoutChange={onMailboxLayoutChange}
|
|
840
|
+
onRetry={onRetry}
|
|
841
|
+
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
842
|
+
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
843
|
+
viewerEmail={viewerEmail}
|
|
805
844
|
/>
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
<PendingMailboxStack
|
|
809
|
-
cancelError={cancelError}
|
|
810
|
-
cancelPending={cancelPendingMessages.isPending}
|
|
811
|
-
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
812
|
-
committedMessageIds={props.committedMessageIds}
|
|
813
|
-
conversationId={props.conversationId}
|
|
814
|
-
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
815
|
-
messages={props.pendingMessages}
|
|
816
|
-
onCancelMessage={onCancelMessage}
|
|
817
|
-
onEditMessage={onEditMessage}
|
|
818
|
-
onLayoutChange={onMailboxLayoutChange}
|
|
819
|
-
onRetry={onRetry}
|
|
820
|
-
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
821
|
-
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
822
|
-
viewerEmail={viewerEmail}
|
|
823
|
-
/>
|
|
845
|
+
</div>
|
|
846
|
+
{unsentAnswers.dialog}
|
|
824
847
|
</>
|
|
825
848
|
}
|
|
826
849
|
>
|
|
827
|
-
<
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
850
|
+
<div
|
|
851
|
+
hidden={Boolean(question)}
|
|
852
|
+
onBlur={(event) => {
|
|
853
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
854
|
+
setComposerFocused(false);
|
|
855
|
+
}
|
|
856
|
+
}}
|
|
857
|
+
onFocus={() => setComposerFocused(true)}
|
|
858
|
+
>
|
|
859
|
+
<ConversationComposer
|
|
860
|
+
confirmSubmit={unsentAnswers.confirmSubmit}
|
|
861
|
+
draftId={props.conversationId}
|
|
862
|
+
error={
|
|
863
|
+
queueActionError ??
|
|
864
|
+
(stopTurn.error ? "Could not stop Junior. Try again." : undefined)
|
|
865
|
+
}
|
|
866
|
+
ref={composerRef}
|
|
867
|
+
label="Continue this conversation"
|
|
868
|
+
submitLabel="Send"
|
|
869
|
+
onFocus={onComposerFocus}
|
|
870
|
+
onStop={props.stoppable ? onStop : undefined}
|
|
871
|
+
stopPending={stopTurn.isPending}
|
|
872
|
+
onSubmitStart={onSubmitStart}
|
|
873
|
+
onSubmit={onSubmit}
|
|
874
|
+
/>
|
|
875
|
+
</div>
|
|
842
876
|
</ComposerDock>
|
|
843
877
|
);
|
|
844
878
|
});
|
|
@@ -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">
|
|
@@ -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
|
);
|