@sentry/junior-dashboard 0.266.0 → 0.267.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 +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/ImageAttachment.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +4 -1
- package/src/client/components/README.md +11 -2
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/dialog.tsx +29 -35
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +30 -1
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/DetailsList.tsx +33 -0
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +310 -112
- package/src/client/conversations/README.md +30 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptTurnContextView.tsx +33 -18
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useAppendConversationMessage,
|
|
20
20
|
useArchiveConversation,
|
|
21
21
|
useCancelConversationPendingMessages,
|
|
22
|
+
useSteerConversationPendingMessages,
|
|
22
23
|
useConversationData,
|
|
23
24
|
useStopConversationTurn,
|
|
24
25
|
type PendingArchiveConversationUpdate,
|
|
@@ -32,7 +33,13 @@ import {
|
|
|
32
33
|
CONVERSATION_EVENT_PARAM,
|
|
33
34
|
parseConversationEventSeq,
|
|
34
35
|
} from "./conversationRoutes";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
ConversationComposer,
|
|
38
|
+
restoreStoredDraftText,
|
|
39
|
+
type ConversationComposerHandle,
|
|
40
|
+
} from "./ConversationComposer";
|
|
41
|
+
import { readStoredComposerImages } from "./ComposerImages";
|
|
42
|
+
import { useDashboardCoreData } from "../api";
|
|
36
43
|
import { ConversationHeader } from "./ConversationHeader";
|
|
37
44
|
import { ConversationDetails } from "./ConversationDetails";
|
|
38
45
|
import { ConversationEventDetails } from "./ConversationEventDetails";
|
|
@@ -66,6 +73,11 @@ import {
|
|
|
66
73
|
hasConversationUsageBreakdown,
|
|
67
74
|
} from "./ConversationUsageBreakdown";
|
|
68
75
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
76
|
+
import { UserInputRequestCard } from "./UserInputRequestCard";
|
|
77
|
+
import {
|
|
78
|
+
openUserInputRequest,
|
|
79
|
+
type OpenUserInputRequest,
|
|
80
|
+
} from "./userInputRequest";
|
|
69
81
|
import {
|
|
70
82
|
buildConversations,
|
|
71
83
|
conversationDetailTitle,
|
|
@@ -184,6 +196,10 @@ export function ConversationPage(props: {
|
|
|
184
196
|
() => committedMessageIds(detail.data?.events),
|
|
185
197
|
[detail.data?.events],
|
|
186
198
|
);
|
|
199
|
+
const userInputRequest = useMemo(
|
|
200
|
+
() => openUserInputRequest(detail.data?.events),
|
|
201
|
+
[detail.data?.events],
|
|
202
|
+
);
|
|
187
203
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
188
204
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
189
205
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
@@ -434,6 +450,7 @@ export function ConversationPage(props: {
|
|
|
434
450
|
loadingPreviousPage={detail.isLoadingPreviousPage}
|
|
435
451
|
onLoadPreviousPage={detail.loadPreviousPage}
|
|
436
452
|
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
453
|
+
pendingMessages={detail.inlineMessages}
|
|
437
454
|
pinRequestVersion={pinRequestVersion}
|
|
438
455
|
responding={
|
|
439
456
|
!detail.error && conversationIsResponding(transcript)
|
|
@@ -466,7 +483,17 @@ export function ConversationPage(props: {
|
|
|
466
483
|
// every 2s; a prop would bust footer memo while the reader types.
|
|
467
484
|
pendingGeneratedAtRef={pendingGeneratedAtRef}
|
|
468
485
|
pendingMessages={detail.pendingMessages}
|
|
486
|
+
// Without live history, an inline send would never leave the
|
|
487
|
+
// transcript tail. A started Turn reads a new send only later.
|
|
488
|
+
// Keep those sends in the queue. An earlier inline send that
|
|
489
|
+
// history does not have yet can start that Turn first.
|
|
490
|
+
sendsInline={
|
|
491
|
+
detail.data.eventHistory.status === "available" &&
|
|
492
|
+
!conversationIsResponding(detail.data) &&
|
|
493
|
+
detail.inlineMessages.length === 0
|
|
494
|
+
}
|
|
469
495
|
stoppable={live}
|
|
496
|
+
userInputRequest={userInputRequest}
|
|
470
497
|
/>
|
|
471
498
|
) : undefined
|
|
472
499
|
}
|
|
@@ -548,8 +575,15 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
548
575
|
pendingAuthorization?: ConversationPendingMessagesReport["authorization"];
|
|
549
576
|
pendingGeneratedAtRef: { current: string | undefined };
|
|
550
577
|
pendingMessages: readonly ConversationMailboxMessage[];
|
|
578
|
+
/**
|
|
579
|
+
* True when history is live and no Turn has started, so the next Turn will
|
|
580
|
+
* read a new send at once.
|
|
581
|
+
*/
|
|
582
|
+
sendsInline: boolean;
|
|
551
583
|
/** True while the conversation has a Turn that a stop can end. */
|
|
552
584
|
stoppable: boolean;
|
|
585
|
+
/** Junior's open question, if no user message answers it yet. */
|
|
586
|
+
userInputRequest?: OpenUserInputRequest;
|
|
553
587
|
}) {
|
|
554
588
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
555
589
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
@@ -558,6 +592,9 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
558
592
|
const cancelPendingMessages = useCancelConversationPendingMessages(
|
|
559
593
|
props.conversationId,
|
|
560
594
|
);
|
|
595
|
+
const [editTargetInboundMessageId, setEditTargetInboundMessageId] =
|
|
596
|
+
useState<string>();
|
|
597
|
+
const [queueActionError, setQueueActionError] = useState<string>();
|
|
561
598
|
// Keep submit identity stable across mutation status flips so the memoized
|
|
562
599
|
// composer does not re-render while the reader is still typing.
|
|
563
600
|
const appendMessageRef = useRef(appendMessage);
|
|
@@ -583,10 +620,21 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
583
620
|
if (!window.matchMedia("(max-width: 767px)").matches) return;
|
|
584
621
|
onPinRequestRef.current();
|
|
585
622
|
}, [pendingMessageVersion]);
|
|
623
|
+
// No Turn runs and nothing waits in the queue, so the next send would
|
|
624
|
+
// leave the queue almost at once. Show it in the transcript instead. A
|
|
625
|
+
// conversation can be active before its Turn starts; that still counts.
|
|
626
|
+
const committedIds = new Set(props.committedMessageIds);
|
|
627
|
+
const sendInlineRef = useRef(false);
|
|
628
|
+
sendInlineRef.current =
|
|
629
|
+
props.sendsInline &&
|
|
630
|
+
!props.pendingMessages.some(
|
|
631
|
+
(message) => !committedIds.has(message.messageId),
|
|
632
|
+
);
|
|
586
633
|
const onSubmit = useCallback(
|
|
587
634
|
async (message: string, idempotencyKey: string, images?: InputImage[]) => {
|
|
588
635
|
await appendMessageRef.current.mutateAsync({
|
|
589
636
|
idempotencyKey,
|
|
637
|
+
inline: sendInlineRef.current,
|
|
590
638
|
message,
|
|
591
639
|
images,
|
|
592
640
|
});
|
|
@@ -608,11 +656,19 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
608
656
|
cancelPendingMessagesRef.current.reset();
|
|
609
657
|
}
|
|
610
658
|
if (!stopTurnRef.current.isPending) stopTurnRef.current.reset();
|
|
659
|
+
setQueueActionError(undefined);
|
|
611
660
|
onPinRequestRef.current();
|
|
612
661
|
}, []);
|
|
613
662
|
const cancellableMessageIds = props.pendingMessages
|
|
614
663
|
.filter((message) => message.clientStatus === undefined)
|
|
615
664
|
.map((message) => message.inboundMessageId);
|
|
665
|
+
const onAnswerUserInput = useCallback(async (answer: string) => {
|
|
666
|
+
onPinRequestRef.current();
|
|
667
|
+
await appendMessageRef.current.mutateAsync({
|
|
668
|
+
idempotencyKey: crypto.randomUUID(),
|
|
669
|
+
message: answer,
|
|
670
|
+
});
|
|
671
|
+
}, []);
|
|
616
672
|
const onCancelMessage = useCallback(
|
|
617
673
|
(message: ConversationMailboxMessage) => {
|
|
618
674
|
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
@@ -633,6 +689,86 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
633
689
|
),
|
|
634
690
|
);
|
|
635
691
|
|
|
692
|
+
// Edit uses its own cancel mutation so remove errors stay on remove.
|
|
693
|
+
const editCancel = useCancelConversationPendingMessages(props.conversationId);
|
|
694
|
+
const editCancelRef = useRef(editCancel);
|
|
695
|
+
editCancelRef.current = editCancel;
|
|
696
|
+
const editingRef = useRef(false);
|
|
697
|
+
const composerRef = useRef<ConversationComposerHandle>(null);
|
|
698
|
+
const viewerEmail = useDashboardCoreData().data?.me.user.email;
|
|
699
|
+
const onEditMessage = useCallback(
|
|
700
|
+
async (message: ConversationMailboxMessage) => {
|
|
701
|
+
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
702
|
+
// The image read runs before the cancel starts, so isPending alone does
|
|
703
|
+
// not stop a second click.
|
|
704
|
+
if (!receivedBefore || editingRef.current) return;
|
|
705
|
+
editingRef.current = true;
|
|
706
|
+
const inboundMessageId = message.inboundMessageId;
|
|
707
|
+
setEditTargetInboundMessageId(inboundMessageId);
|
|
708
|
+
setQueueActionError(undefined);
|
|
709
|
+
try {
|
|
710
|
+
// Read images first. A failed read must leave the message queued.
|
|
711
|
+
const images = await readStoredComposerImages(
|
|
712
|
+
props.conversationId,
|
|
713
|
+
message.attachments,
|
|
714
|
+
);
|
|
715
|
+
// The reader left during the read. Keep the message in the queue.
|
|
716
|
+
if (!composerRef.current) return;
|
|
717
|
+
// Check before the cancel. After it, images that do not fit are lost.
|
|
718
|
+
if (!composerRef.current.canRestoreImages(images)) {
|
|
719
|
+
setQueueActionError("Remove some draft images to edit that message.");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const result = await editCancelRef.current.mutateAsync({
|
|
723
|
+
inboundMessageIds: [inboundMessageId],
|
|
724
|
+
receivedBefore,
|
|
725
|
+
});
|
|
726
|
+
// Junior can read the message before the cancel lands. Then it is
|
|
727
|
+
// already in the Conversation and there is nothing to edit.
|
|
728
|
+
if (!result.cancelledInboundMessageIds.includes(inboundMessageId)) {
|
|
729
|
+
setQueueActionError("Junior already read that message.");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const restored = { images, text: message.text ?? "" };
|
|
733
|
+
if (composerRef.current) {
|
|
734
|
+
composerRef.current.restoreMessage(restored);
|
|
735
|
+
} else {
|
|
736
|
+
// The reader left during the cancel. Keep the text in the saved
|
|
737
|
+
// draft so it is there when they come back.
|
|
738
|
+
restoreStoredDraftText(props.conversationId, restored.text);
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
setQueueActionError("Could not edit the queued message. Try again.");
|
|
742
|
+
} finally {
|
|
743
|
+
editingRef.current = false;
|
|
744
|
+
setEditTargetInboundMessageId(undefined);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
[props.conversationId, props.pendingGeneratedAtRef],
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
const steerPendingMessages = useSteerConversationPendingMessages(
|
|
751
|
+
props.conversationId,
|
|
752
|
+
);
|
|
753
|
+
const steerPendingMessagesRef = useRef(steerPendingMessages);
|
|
754
|
+
steerPendingMessagesRef.current = steerPendingMessages;
|
|
755
|
+
const onSteerMessage = useCallback((message: ConversationMailboxMessage) => {
|
|
756
|
+
if (steerPendingMessagesRef.current.isPending) return;
|
|
757
|
+
setQueueActionError(undefined);
|
|
758
|
+
steerPendingMessagesRef.current.mutate(
|
|
759
|
+
{ inboundMessageIds: [message.inboundMessageId] },
|
|
760
|
+
{
|
|
761
|
+
onError: () =>
|
|
762
|
+
setQueueActionError(
|
|
763
|
+
"Could not send the queued message now. Try again.",
|
|
764
|
+
),
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
}, []);
|
|
768
|
+
const steerTargetInboundMessageId = steerPendingMessages.isPending
|
|
769
|
+
? steerPendingMessages.variables?.inboundMessageIds[0]
|
|
770
|
+
: undefined;
|
|
771
|
+
|
|
636
772
|
const onStop = useCallback(async () => {
|
|
637
773
|
const result = await stopTurnRef.current.mutateAsync();
|
|
638
774
|
const restored = result.stoppedMessages.map((message) => message.text);
|
|
@@ -657,22 +793,40 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
657
793
|
{props.pendingAuthorization ? (
|
|
658
794
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
659
795
|
) : null}
|
|
796
|
+
{/* A queued message already answers the question. */}
|
|
797
|
+
{props.userInputRequest && props.pendingMessages.length === 0 ? (
|
|
798
|
+
<UserInputRequestCard
|
|
799
|
+
key={props.userInputRequest.toolCallId}
|
|
800
|
+
onAnswer={onAnswerUserInput}
|
|
801
|
+
request={props.userInputRequest}
|
|
802
|
+
/>
|
|
803
|
+
) : null}
|
|
660
804
|
<PendingMailboxStack
|
|
661
805
|
cancelError={cancelError}
|
|
662
806
|
cancelPending={cancelPendingMessages.isPending}
|
|
663
807
|
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
664
808
|
committedMessageIds={props.committedMessageIds}
|
|
809
|
+
conversationId={props.conversationId}
|
|
810
|
+
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
665
811
|
messages={props.pendingMessages}
|
|
666
812
|
onCancelMessage={onCancelMessage}
|
|
813
|
+
onEditMessage={onEditMessage}
|
|
667
814
|
onLayoutChange={onMailboxLayoutChange}
|
|
668
815
|
onRetry={onRetry}
|
|
816
|
+
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
817
|
+
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
818
|
+
viewerEmail={viewerEmail}
|
|
669
819
|
/>
|
|
670
820
|
</>
|
|
671
821
|
}
|
|
672
822
|
>
|
|
673
823
|
<ConversationComposer
|
|
674
824
|
draftId={props.conversationId}
|
|
675
|
-
error={
|
|
825
|
+
error={
|
|
826
|
+
queueActionError ??
|
|
827
|
+
(stopTurn.error ? "Could not stop Junior. Try again." : undefined)
|
|
828
|
+
}
|
|
829
|
+
ref={composerRef}
|
|
676
830
|
label="Continue this conversation"
|
|
677
831
|
submitLabel="Send"
|
|
678
832
|
onFocus={onComposerFocus}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { memo, useMemo, useRef, type ReactNode } from "react";
|
|
2
|
+
import { History } from "lucide-react";
|
|
3
|
+
import type { ConversationMailboxMessage } from "./conversationOutbox";
|
|
2
4
|
|
|
5
|
+
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
6
|
+
import {
|
|
7
|
+
Empty,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyMedia,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from "../components/ui/empty";
|
|
3
12
|
import { Card } from "../components/layout/Card";
|
|
4
|
-
import { unavailableTranscriptLabel } from "../format";
|
|
13
|
+
import { formatTime, unavailableTranscriptLabel } from "../format";
|
|
5
14
|
import { ConversationBrief } from "./ConversationBrief";
|
|
6
15
|
import { transcriptMessagesFromEvents } from "./eventTranscript";
|
|
7
16
|
import type {
|
|
@@ -39,7 +48,6 @@ import {
|
|
|
39
48
|
groupTranscriptMessages,
|
|
40
49
|
type RenderedTranscriptEntry,
|
|
41
50
|
} from "./transcriptRenderModel";
|
|
42
|
-
import { transcriptEmptyClass } from "./transcriptStyles";
|
|
43
51
|
import { entryMatchesSearch, useTranscriptSearch } from "./transcriptSearch";
|
|
44
52
|
import { TranscriptRow, TranscriptRows } from "./TranscriptRows";
|
|
45
53
|
import { TranscriptTimestampProvider } from "./TranscriptTimestamp";
|
|
@@ -74,13 +82,20 @@ export const ConversationTranscriptView = memo(
|
|
|
74
82
|
conversation: ConversationTranscript;
|
|
75
83
|
}) => void;
|
|
76
84
|
conversation: ConversationTranscript;
|
|
85
|
+
/** Sends that history does not have yet. History wins on message id. */
|
|
86
|
+
pendingMessages?: readonly ConversationMailboxMessage[];
|
|
77
87
|
responding?: boolean;
|
|
78
88
|
}) {
|
|
79
89
|
// Cards can change while their Message events stay unchanged.
|
|
80
90
|
const { events, annotations } = props.conversation;
|
|
81
91
|
const messages = useMemo(
|
|
82
|
-
() =>
|
|
83
|
-
|
|
92
|
+
() =>
|
|
93
|
+
transcriptMessagesFromEvents(
|
|
94
|
+
events,
|
|
95
|
+
annotations,
|
|
96
|
+
props.pendingMessages,
|
|
97
|
+
),
|
|
98
|
+
[events, annotations, props.pendingMessages],
|
|
84
99
|
);
|
|
85
100
|
|
|
86
101
|
return (
|
|
@@ -110,7 +125,10 @@ function SegmentEvents(props: {
|
|
|
110
125
|
return (
|
|
111
126
|
<TranscriptRows>
|
|
112
127
|
{props.conversation.eventHistory.status === "expired" ? (
|
|
113
|
-
<ExpiredTranscriptView
|
|
128
|
+
<ExpiredTranscriptView
|
|
129
|
+
brief={props.conversation.brief}
|
|
130
|
+
expiredAt={props.conversation.eventHistory.expiredAt}
|
|
131
|
+
/>
|
|
114
132
|
) : props.conversation.eventHistory.status === "available" ? (
|
|
115
133
|
<VisibleTranscriptEntries
|
|
116
134
|
onOpenSubagentTranscript={props.onOpenSubagentTranscript}
|
|
@@ -131,9 +149,9 @@ function SegmentEvents(props: {
|
|
|
131
149
|
conversation={props.conversation}
|
|
132
150
|
/>
|
|
133
151
|
) : (
|
|
134
|
-
<
|
|
152
|
+
<EmptyTelemetry>
|
|
135
153
|
{unavailableTranscriptLabel(props.conversation)}
|
|
136
|
-
</
|
|
154
|
+
</EmptyTelemetry>
|
|
137
155
|
)}
|
|
138
156
|
{props.responding ? (
|
|
139
157
|
<TranscriptRow indent>
|
|
@@ -145,21 +163,37 @@ function SegmentEvents(props: {
|
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
function ExpiredTranscriptView(props: {
|
|
148
|
-
|
|
166
|
+
brief: ConversationTranscript["brief"];
|
|
167
|
+
expiredAt: string;
|
|
149
168
|
}) {
|
|
150
169
|
return (
|
|
151
170
|
<>
|
|
152
|
-
{props.
|
|
171
|
+
{props.brief ? (
|
|
153
172
|
<Card as="section" padding="md" variant="section">
|
|
154
173
|
<h3 className="mt-0 mb-3 font-display text-lg font-medium text-dashboard-text">
|
|
155
174
|
Brief
|
|
156
175
|
</h3>
|
|
157
|
-
<ConversationBrief brief={props.
|
|
176
|
+
<ConversationBrief brief={props.brief} />
|
|
158
177
|
</Card>
|
|
159
178
|
) : null}
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
<Empty className="flex items-start gap-3 border-dashed border-dashboard-border-emphasis bg-dashboard-fill-soft">
|
|
180
|
+
<EmptyMedia>
|
|
181
|
+
<History />
|
|
182
|
+
</EmptyMedia>
|
|
183
|
+
<div className="min-w-0">
|
|
184
|
+
<EmptyTitle>Transcript expired</EmptyTitle>
|
|
185
|
+
<EmptyDescription className="mt-1 text-sm">
|
|
186
|
+
The retention policy removed these events on{" "}
|
|
187
|
+
<time dateTime={props.expiredAt}>
|
|
188
|
+
{formatTime(props.expiredAt, {
|
|
189
|
+
dateStyle: "medium",
|
|
190
|
+
timeStyle: "short",
|
|
191
|
+
})}
|
|
192
|
+
</time>
|
|
193
|
+
.{props.brief ? " The brief above is still available." : null}
|
|
194
|
+
</EmptyDescription>
|
|
195
|
+
</div>
|
|
196
|
+
</Empty>
|
|
163
197
|
</>
|
|
164
198
|
);
|
|
165
199
|
}
|
|
@@ -348,9 +382,7 @@ function TranscriptEntryList(props: {
|
|
|
348
382
|
}
|
|
349
383
|
|
|
350
384
|
if (search.active && rows.length === 0) {
|
|
351
|
-
return
|
|
352
|
-
<div className={transcriptEmptyClass()}>No events match your search.</div>
|
|
353
|
-
);
|
|
385
|
+
return <EmptyTelemetry>No events match your search.</EmptyTelemetry>;
|
|
354
386
|
}
|
|
355
387
|
|
|
356
388
|
// Let rows participate in the parent transcript stack so messages, tool
|
|
@@ -3,6 +3,39 @@ import { useState, type ReactNode } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { cn, dashboardDisclosureChevronClass } from "../styles";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Group one kind of detail in a compact card. Conversation details and event
|
|
8
|
+
* details share it, so both panels read the same way. A `label` names the
|
|
9
|
+
* card as a region.
|
|
10
|
+
*/
|
|
11
|
+
export function DetailsCard(props: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
count?: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<section
|
|
19
|
+
aria-label={props.label}
|
|
20
|
+
className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3"
|
|
21
|
+
>
|
|
22
|
+
{props.title ? (
|
|
23
|
+
<h3 className="m-0 flex min-w-0 items-center gap-1.5 break-words font-sans text-xs font-medium text-dashboard-text-muted">
|
|
24
|
+
{props.title}
|
|
25
|
+
{props.count ? (
|
|
26
|
+
<span className="tabular-nums text-dashboard-text-faint">
|
|
27
|
+
{props.count.toLocaleString("en-US")}
|
|
28
|
+
</span>
|
|
29
|
+
) : null}
|
|
30
|
+
</h3>
|
|
31
|
+
) : null}
|
|
32
|
+
<div className="min-w-0 break-words font-sans text-xs leading-relaxed text-dashboard-text-muted">
|
|
33
|
+
{props.children}
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
/** Rows a details list shows before the reader asks for the rest. */
|
|
7
40
|
const COLLAPSED_ROW_COUNT = 3;
|
|
8
41
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { dashboardDisclosureChevronClass } from "../styles";
|
|
13
13
|
|
|
14
14
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
15
|
+
import { DetailsCard } from "./DetailsList";
|
|
15
16
|
import { TranscriptText } from "./TranscriptText";
|
|
16
17
|
import { eventLogModel } from "./eventLog";
|
|
17
18
|
import { formatCostBreakdown } from "../format";
|
|
@@ -61,7 +62,9 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
61
62
|
content = (
|
|
62
63
|
<>
|
|
63
64
|
{Object.keys(metadata).length > 0 ? (
|
|
64
|
-
<
|
|
65
|
+
<DetailSection>
|
|
66
|
+
<DetailValue value={metadata} />
|
|
67
|
+
</DetailSection>
|
|
65
68
|
) : null}
|
|
66
69
|
{summary ? (
|
|
67
70
|
<DetailSection title="Continuation summary">
|
|
@@ -76,7 +79,9 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
76
79
|
const { type: _, text, redacted, ...metadata } = data;
|
|
77
80
|
content = (
|
|
78
81
|
<>
|
|
79
|
-
<
|
|
82
|
+
<DetailSection>
|
|
83
|
+
<DetailValue value={metadata} />
|
|
84
|
+
</DetailSection>
|
|
80
85
|
<DetailSection title="Message">
|
|
81
86
|
{redacted ? (
|
|
82
87
|
<RedactedMarker />
|
|
@@ -97,14 +102,14 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
97
102
|
<DetailSection key={call.toolCallId} title={name}>
|
|
98
103
|
<DetailValue value={metadata} />
|
|
99
104
|
{input !== undefined ? (
|
|
100
|
-
<
|
|
105
|
+
<DetailSubsection title="Input">
|
|
101
106
|
<DetailValue value={input} />
|
|
102
|
-
</
|
|
107
|
+
</DetailSubsection>
|
|
103
108
|
) : null}
|
|
104
109
|
{output !== undefined ? (
|
|
105
|
-
<
|
|
110
|
+
<DetailSubsection title="Output">
|
|
106
111
|
<DetailValue value={output} />
|
|
107
|
-
</
|
|
112
|
+
</DetailSubsection>
|
|
108
113
|
) : null}
|
|
109
114
|
</DetailSection>
|
|
110
115
|
);
|
|
@@ -132,13 +137,15 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
132
137
|
const { type: _, presentation, ...metadata } = data;
|
|
133
138
|
content = (
|
|
134
139
|
<>
|
|
135
|
-
<
|
|
140
|
+
<DetailSection>
|
|
141
|
+
<DetailValue value={metadata} />
|
|
142
|
+
</DetailSection>
|
|
136
143
|
<DetailSection title={presentation.title}>
|
|
137
144
|
{presentation.preview ? (
|
|
138
145
|
<TranscriptText mentions={mentions} text={presentation.preview} />
|
|
139
146
|
) : null}
|
|
140
147
|
{presentation.details?.map((detail, index) => (
|
|
141
|
-
<
|
|
148
|
+
<DetailSubsection key={index} title={detail.title}>
|
|
142
149
|
{detail.description ? (
|
|
143
150
|
<TranscriptText
|
|
144
151
|
mentions={mentions}
|
|
@@ -149,7 +156,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
149
156
|
{detail.metadata ? (
|
|
150
157
|
<DetailValue value={detail.metadata} />
|
|
151
158
|
) : null}
|
|
152
|
-
</
|
|
159
|
+
</DetailSubsection>
|
|
153
160
|
))}
|
|
154
161
|
</DetailSection>
|
|
155
162
|
</>
|
|
@@ -158,12 +165,16 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
158
165
|
}
|
|
159
166
|
default: {
|
|
160
167
|
const { type: _, ...fields } = data;
|
|
161
|
-
content =
|
|
168
|
+
content = (
|
|
169
|
+
<DetailSection>
|
|
170
|
+
<DetailValue value={fields} />
|
|
171
|
+
</DetailSection>
|
|
172
|
+
);
|
|
162
173
|
}
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
return (
|
|
166
|
-
<div className="@container grid min-w-0 gap-
|
|
177
|
+
<div className="@container grid min-w-0 gap-3">
|
|
167
178
|
<DetailSection
|
|
168
179
|
title={
|
|
169
180
|
data.type === "handoff" ? "Target configuration" : "Configuration"
|
|
@@ -197,7 +208,7 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
|
|
|
197
208
|
const [open, setOpen] = useState(false);
|
|
198
209
|
const json = open ? JSON.stringify(event, null, 2) : "";
|
|
199
210
|
return (
|
|
200
|
-
<div className="relative min-w-0
|
|
211
|
+
<div className="relative min-w-0">
|
|
201
212
|
<Collapsible onOpenChange={setOpen} open={open}>
|
|
202
213
|
<CollapsibleTrigger className="group flex min-h-9 cursor-pointer items-center gap-1 pr-12 text-xs text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-dashboard-focus">
|
|
203
214
|
<ChevronRight
|
|
@@ -207,7 +218,7 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
|
|
|
207
218
|
Raw JSON
|
|
208
219
|
</CollapsibleTrigger>
|
|
209
220
|
<CollapsibleContent>
|
|
210
|
-
<div className="absolute right-0 top-
|
|
221
|
+
<div className="absolute right-0 top-0">
|
|
211
222
|
<CopyButton getText={() => json} label="Copy JSON" />
|
|
212
223
|
</div>
|
|
213
224
|
<div
|
|
@@ -224,12 +235,28 @@ function RawEventJson({ event }: { event: ConversationReportEvent }) {
|
|
|
224
235
|
);
|
|
225
236
|
}
|
|
226
237
|
|
|
227
|
-
|
|
238
|
+
// Top-level groups use the conversation details card. A titled card is a
|
|
239
|
+
// region, so readers can jump to a section such as "Message".
|
|
240
|
+
function DetailSection(props: { title?: string; children: ReactNode }) {
|
|
241
|
+
return (
|
|
242
|
+
<DetailsCard label={props.title} title={props.title}>
|
|
243
|
+
<div className="grid min-w-0 gap-3 text-sm text-dashboard-text">
|
|
244
|
+
{props.children}
|
|
245
|
+
</div>
|
|
246
|
+
</DetailsCard>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A group inside a card, such as a tool input. It does not nest another card.
|
|
251
|
+
function DetailSubsection(props: { title: string; children: ReactNode }) {
|
|
228
252
|
return (
|
|
229
|
-
<section
|
|
230
|
-
|
|
253
|
+
<section
|
|
254
|
+
aria-label={props.title}
|
|
255
|
+
className="grid min-w-0 gap-1.5 border-t border-dashboard-border-subtle pt-3"
|
|
256
|
+
>
|
|
257
|
+
<h4 className="m-0 font-sans text-xs font-medium text-dashboard-text-muted">
|
|
231
258
|
{props.title}
|
|
232
|
-
</
|
|
259
|
+
</h4>
|
|
233
260
|
{props.children}
|
|
234
261
|
</section>
|
|
235
262
|
);
|
|
@@ -275,8 +302,8 @@ function DetailValue({ value }: { value: unknown }) {
|
|
|
275
302
|
key={key}
|
|
276
303
|
className={
|
|
277
304
|
nested
|
|
278
|
-
? "grid min-w-0 gap-2 py-2"
|
|
279
|
-
: "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-baseline gap-x-4 py-2 @min-[30rem]:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]"
|
|
305
|
+
? "grid min-w-0 gap-2 py-2 first:pt-0 last:pb-0"
|
|
306
|
+
: "grid min-w-0 grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-baseline gap-x-4 py-2 first:pt-0 last:pb-0 @min-[30rem]:grid-cols-[minmax(0,10rem)_minmax(0,1fr)]"
|
|
280
307
|
}
|
|
281
308
|
>
|
|
282
309
|
<dt className="break-words text-xs leading-relaxed text-dashboard-text-muted">
|