@sentry/junior-dashboard 0.266.0 → 0.268.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 +85 -12
- package/dist/client.js +94 -64
- package/dist/index.js +85 -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 +55 -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 +6 -2
- package/src/client/components/README.md +15 -2
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +11 -1
- 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/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +160 -2
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- 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 +27 -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/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +10 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +174 -161
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +39 -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/turnContext.ts +15 -0
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +31 -8
- package/src/client/pages/SettingsPage.tsx +103 -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/pages/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +62 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +277 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +115 -0
- package/src/client/review-desk/queries.ts +164 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +54 -2
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- 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,
|
|
@@ -81,6 +93,7 @@ import {
|
|
|
81
93
|
} from "../components/ParticipantAvatarStack";
|
|
82
94
|
import { ChatLayout } from "./ChatLayout";
|
|
83
95
|
import { ComposerDock } from "./ComposerDock";
|
|
96
|
+
import { ConversationReviewBar } from "../review-desk/ConversationReviewBar";
|
|
84
97
|
import { Transcript } from "./TranscriptView";
|
|
85
98
|
import { TranscriptLoading } from "./TranscriptLoading";
|
|
86
99
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
@@ -184,6 +197,10 @@ export function ConversationPage(props: {
|
|
|
184
197
|
() => committedMessageIds(detail.data?.events),
|
|
185
198
|
[detail.data?.events],
|
|
186
199
|
);
|
|
200
|
+
const userInputRequest = useMemo(
|
|
201
|
+
() => openUserInputRequest(detail.data?.events),
|
|
202
|
+
[detail.data?.events],
|
|
203
|
+
);
|
|
187
204
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
188
205
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
189
206
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
@@ -434,6 +451,7 @@ export function ConversationPage(props: {
|
|
|
434
451
|
loadingPreviousPage={detail.isLoadingPreviousPage}
|
|
435
452
|
onLoadPreviousPage={detail.loadPreviousPage}
|
|
436
453
|
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
454
|
+
pendingMessages={detail.inlineMessages}
|
|
437
455
|
pinRequestVersion={pinRequestVersion}
|
|
438
456
|
responding={
|
|
439
457
|
!detail.error && conversationIsResponding(transcript)
|
|
@@ -466,7 +484,17 @@ export function ConversationPage(props: {
|
|
|
466
484
|
// every 2s; a prop would bust footer memo while the reader types.
|
|
467
485
|
pendingGeneratedAtRef={pendingGeneratedAtRef}
|
|
468
486
|
pendingMessages={detail.pendingMessages}
|
|
487
|
+
// Without live history, an inline send would never leave the
|
|
488
|
+
// transcript tail. A started Turn reads a new send only later.
|
|
489
|
+
// Keep those sends in the queue. An earlier inline send that
|
|
490
|
+
// history does not have yet can start that Turn first.
|
|
491
|
+
sendsInline={
|
|
492
|
+
detail.data.eventHistory.status === "available" &&
|
|
493
|
+
!conversationIsResponding(detail.data) &&
|
|
494
|
+
detail.inlineMessages.length === 0
|
|
495
|
+
}
|
|
469
496
|
stoppable={live}
|
|
497
|
+
userInputRequest={userInputRequest}
|
|
470
498
|
/>
|
|
471
499
|
) : undefined
|
|
472
500
|
}
|
|
@@ -520,9 +548,11 @@ export function ConversationPage(props: {
|
|
|
520
548
|
{turnContext ? (
|
|
521
549
|
<TranscriptSearchProvider query={search}>
|
|
522
550
|
<TurnContextDetails
|
|
551
|
+
actor={turnContext.actor}
|
|
523
552
|
contexts={turnContext.contexts}
|
|
524
553
|
key={turnContext.id}
|
|
525
554
|
onClose={closeTurnContext}
|
|
555
|
+
timestamp={turnContext.timestamp}
|
|
526
556
|
/>
|
|
527
557
|
</TranscriptSearchProvider>
|
|
528
558
|
) : null}
|
|
@@ -548,8 +578,15 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
548
578
|
pendingAuthorization?: ConversationPendingMessagesReport["authorization"];
|
|
549
579
|
pendingGeneratedAtRef: { current: string | undefined };
|
|
550
580
|
pendingMessages: readonly ConversationMailboxMessage[];
|
|
581
|
+
/**
|
|
582
|
+
* True when history is live and no Turn has started, so the next Turn will
|
|
583
|
+
* read a new send at once.
|
|
584
|
+
*/
|
|
585
|
+
sendsInline: boolean;
|
|
551
586
|
/** True while the conversation has a Turn that a stop can end. */
|
|
552
587
|
stoppable: boolean;
|
|
588
|
+
/** Junior's open question, if no user message answers it yet. */
|
|
589
|
+
userInputRequest?: OpenUserInputRequest;
|
|
553
590
|
}) {
|
|
554
591
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
555
592
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
@@ -558,6 +595,9 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
558
595
|
const cancelPendingMessages = useCancelConversationPendingMessages(
|
|
559
596
|
props.conversationId,
|
|
560
597
|
);
|
|
598
|
+
const [editTargetInboundMessageId, setEditTargetInboundMessageId] =
|
|
599
|
+
useState<string>();
|
|
600
|
+
const [queueActionError, setQueueActionError] = useState<string>();
|
|
561
601
|
// Keep submit identity stable across mutation status flips so the memoized
|
|
562
602
|
// composer does not re-render while the reader is still typing.
|
|
563
603
|
const appendMessageRef = useRef(appendMessage);
|
|
@@ -583,10 +623,21 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
583
623
|
if (!window.matchMedia("(max-width: 767px)").matches) return;
|
|
584
624
|
onPinRequestRef.current();
|
|
585
625
|
}, [pendingMessageVersion]);
|
|
626
|
+
// No Turn runs and nothing waits in the queue, so the next send would
|
|
627
|
+
// leave the queue almost at once. Show it in the transcript instead. A
|
|
628
|
+
// conversation can be active before its Turn starts; that still counts.
|
|
629
|
+
const committedIds = new Set(props.committedMessageIds);
|
|
630
|
+
const sendInlineRef = useRef(false);
|
|
631
|
+
sendInlineRef.current =
|
|
632
|
+
props.sendsInline &&
|
|
633
|
+
!props.pendingMessages.some(
|
|
634
|
+
(message) => !committedIds.has(message.messageId),
|
|
635
|
+
);
|
|
586
636
|
const onSubmit = useCallback(
|
|
587
637
|
async (message: string, idempotencyKey: string, images?: InputImage[]) => {
|
|
588
638
|
await appendMessageRef.current.mutateAsync({
|
|
589
639
|
idempotencyKey,
|
|
640
|
+
inline: sendInlineRef.current,
|
|
590
641
|
message,
|
|
591
642
|
images,
|
|
592
643
|
});
|
|
@@ -608,11 +659,19 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
608
659
|
cancelPendingMessagesRef.current.reset();
|
|
609
660
|
}
|
|
610
661
|
if (!stopTurnRef.current.isPending) stopTurnRef.current.reset();
|
|
662
|
+
setQueueActionError(undefined);
|
|
611
663
|
onPinRequestRef.current();
|
|
612
664
|
}, []);
|
|
613
665
|
const cancellableMessageIds = props.pendingMessages
|
|
614
666
|
.filter((message) => message.clientStatus === undefined)
|
|
615
667
|
.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
|
+
}, []);
|
|
616
675
|
const onCancelMessage = useCallback(
|
|
617
676
|
(message: ConversationMailboxMessage) => {
|
|
618
677
|
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
@@ -633,6 +692,86 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
633
692
|
),
|
|
634
693
|
);
|
|
635
694
|
|
|
695
|
+
// Edit uses its own cancel mutation so remove errors stay on remove.
|
|
696
|
+
const editCancel = useCancelConversationPendingMessages(props.conversationId);
|
|
697
|
+
const editCancelRef = useRef(editCancel);
|
|
698
|
+
editCancelRef.current = editCancel;
|
|
699
|
+
const editingRef = useRef(false);
|
|
700
|
+
const composerRef = useRef<ConversationComposerHandle>(null);
|
|
701
|
+
const viewerEmail = useDashboardCoreData().data?.me.user.email;
|
|
702
|
+
const onEditMessage = useCallback(
|
|
703
|
+
async (message: ConversationMailboxMessage) => {
|
|
704
|
+
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
705
|
+
// The image read runs before the cancel starts, so isPending alone does
|
|
706
|
+
// not stop a second click.
|
|
707
|
+
if (!receivedBefore || editingRef.current) return;
|
|
708
|
+
editingRef.current = true;
|
|
709
|
+
const inboundMessageId = message.inboundMessageId;
|
|
710
|
+
setEditTargetInboundMessageId(inboundMessageId);
|
|
711
|
+
setQueueActionError(undefined);
|
|
712
|
+
try {
|
|
713
|
+
// Read images first. A failed read must leave the message queued.
|
|
714
|
+
const images = await readStoredComposerImages(
|
|
715
|
+
props.conversationId,
|
|
716
|
+
message.attachments,
|
|
717
|
+
);
|
|
718
|
+
// The reader left during the read. Keep the message in the queue.
|
|
719
|
+
if (!composerRef.current) return;
|
|
720
|
+
// Check before the cancel. After it, images that do not fit are lost.
|
|
721
|
+
if (!composerRef.current.canRestoreImages(images)) {
|
|
722
|
+
setQueueActionError("Remove some draft images to edit that message.");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const result = await editCancelRef.current.mutateAsync({
|
|
726
|
+
inboundMessageIds: [inboundMessageId],
|
|
727
|
+
receivedBefore,
|
|
728
|
+
});
|
|
729
|
+
// Junior can read the message before the cancel lands. Then it is
|
|
730
|
+
// already in the Conversation and there is nothing to edit.
|
|
731
|
+
if (!result.cancelledInboundMessageIds.includes(inboundMessageId)) {
|
|
732
|
+
setQueueActionError("Junior already read that message.");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const restored = { images, text: message.text ?? "" };
|
|
736
|
+
if (composerRef.current) {
|
|
737
|
+
composerRef.current.restoreMessage(restored);
|
|
738
|
+
} else {
|
|
739
|
+
// The reader left during the cancel. Keep the text in the saved
|
|
740
|
+
// draft so it is there when they come back.
|
|
741
|
+
restoreStoredDraftText(props.conversationId, restored.text);
|
|
742
|
+
}
|
|
743
|
+
} catch {
|
|
744
|
+
setQueueActionError("Could not edit the queued message. Try again.");
|
|
745
|
+
} finally {
|
|
746
|
+
editingRef.current = false;
|
|
747
|
+
setEditTargetInboundMessageId(undefined);
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
[props.conversationId, props.pendingGeneratedAtRef],
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const steerPendingMessages = useSteerConversationPendingMessages(
|
|
754
|
+
props.conversationId,
|
|
755
|
+
);
|
|
756
|
+
const steerPendingMessagesRef = useRef(steerPendingMessages);
|
|
757
|
+
steerPendingMessagesRef.current = steerPendingMessages;
|
|
758
|
+
const onSteerMessage = useCallback((message: ConversationMailboxMessage) => {
|
|
759
|
+
if (steerPendingMessagesRef.current.isPending) return;
|
|
760
|
+
setQueueActionError(undefined);
|
|
761
|
+
steerPendingMessagesRef.current.mutate(
|
|
762
|
+
{ inboundMessageIds: [message.inboundMessageId] },
|
|
763
|
+
{
|
|
764
|
+
onError: () =>
|
|
765
|
+
setQueueActionError(
|
|
766
|
+
"Could not send the queued message now. Try again.",
|
|
767
|
+
),
|
|
768
|
+
},
|
|
769
|
+
);
|
|
770
|
+
}, []);
|
|
771
|
+
const steerTargetInboundMessageId = steerPendingMessages.isPending
|
|
772
|
+
? steerPendingMessages.variables?.inboundMessageIds[0]
|
|
773
|
+
: undefined;
|
|
774
|
+
|
|
636
775
|
const onStop = useCallback(async () => {
|
|
637
776
|
const result = await stopTurnRef.current.mutateAsync();
|
|
638
777
|
const restored = result.stoppedMessages.map((message) => message.text);
|
|
@@ -657,22 +796,41 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
657
796
|
{props.pendingAuthorization ? (
|
|
658
797
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
659
798
|
) : null}
|
|
799
|
+
{/* A queued message already answers the question. */}
|
|
800
|
+
{props.userInputRequest && props.pendingMessages.length === 0 ? (
|
|
801
|
+
<UserInputRequestCard
|
|
802
|
+
key={props.userInputRequest.toolCallId}
|
|
803
|
+
onAnswer={onAnswerUserInput}
|
|
804
|
+
request={props.userInputRequest}
|
|
805
|
+
/>
|
|
806
|
+
) : null}
|
|
807
|
+
<ConversationReviewBar conversationId={props.conversationId} />
|
|
660
808
|
<PendingMailboxStack
|
|
661
809
|
cancelError={cancelError}
|
|
662
810
|
cancelPending={cancelPendingMessages.isPending}
|
|
663
811
|
cancelTargetInboundMessageId={cancelTargetInboundMessageId}
|
|
664
812
|
committedMessageIds={props.committedMessageIds}
|
|
813
|
+
conversationId={props.conversationId}
|
|
814
|
+
editTargetInboundMessageId={editTargetInboundMessageId}
|
|
665
815
|
messages={props.pendingMessages}
|
|
666
816
|
onCancelMessage={onCancelMessage}
|
|
817
|
+
onEditMessage={onEditMessage}
|
|
667
818
|
onLayoutChange={onMailboxLayoutChange}
|
|
668
819
|
onRetry={onRetry}
|
|
820
|
+
onSteerMessage={props.stoppable ? onSteerMessage : undefined}
|
|
821
|
+
steerTargetInboundMessageId={steerTargetInboundMessageId}
|
|
822
|
+
viewerEmail={viewerEmail}
|
|
669
823
|
/>
|
|
670
824
|
</>
|
|
671
825
|
}
|
|
672
826
|
>
|
|
673
827
|
<ConversationComposer
|
|
674
828
|
draftId={props.conversationId}
|
|
675
|
-
error={
|
|
829
|
+
error={
|
|
830
|
+
queueActionError ??
|
|
831
|
+
(stopTurn.error ? "Could not stop Junior. Try again." : undefined)
|
|
832
|
+
}
|
|
833
|
+
ref={composerRef}
|
|
676
834
|
label="Continue this conversation"
|
|
677
835
|
submitLabel="Send"
|
|
678
836
|
onFocus={onComposerFocus}
|
|
@@ -48,6 +48,7 @@ export function ConversationSidebar(props: {
|
|
|
48
48
|
query: string;
|
|
49
49
|
selectedId?: string;
|
|
50
50
|
timeZone: string;
|
|
51
|
+
waitingConversationIds: ReadonlySet<string>;
|
|
51
52
|
/**
|
|
52
53
|
* `panel` = split-pane dock with internal scroll.
|
|
53
54
|
* `landing` = flow nav under create compose (parent owns scroll).
|
|
@@ -186,6 +187,9 @@ export function ConversationSidebar(props: {
|
|
|
186
187
|
onArchiveError={handleArchiveError}
|
|
187
188
|
onArchived={handleArchived}
|
|
188
189
|
selected={entry.conversation.id === props.selectedId}
|
|
190
|
+
waitingOnViewer={props.waitingConversationIds.has(
|
|
191
|
+
entry.conversation.id,
|
|
192
|
+
)}
|
|
189
193
|
/>
|
|
190
194
|
)
|
|
191
195
|
}
|
|
@@ -271,6 +275,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
|
271
275
|
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
|
|
272
276
|
onArchived(conversation: Conversation): void;
|
|
273
277
|
selected: boolean;
|
|
278
|
+
waitingOnViewer: boolean;
|
|
274
279
|
}) {
|
|
275
280
|
const archive = useArchiveConversation(props.conversation.id, {
|
|
276
281
|
// Read the attempted direction from the mutation call, not the (possibly
|
|
@@ -316,6 +321,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(props: {
|
|
|
316
321
|
finishedSinceSeen={props.finishedSinceSeen}
|
|
317
322
|
isPrivate={isPrivate}
|
|
318
323
|
status={status}
|
|
324
|
+
waitingOnViewer={props.waitingOnViewer}
|
|
319
325
|
/>
|
|
320
326
|
</div>
|
|
321
327
|
<div className="col-start-2 row-start-1 min-w-0 truncate font-sans text-sm font-medium leading-snug text-dashboard-text">
|
|
@@ -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
|
|
@@ -20,6 +20,7 @@ import { buildConversations, getDashboardTimeZone } from "../format";
|
|
|
20
20
|
import { useDebouncedValue } from "../debouncedValue";
|
|
21
21
|
import type { Conversation } from "../types";
|
|
22
22
|
import { ConversationPage } from "./ConversationPage";
|
|
23
|
+
import { useWaitingConversationIds } from "../review-desk/queries";
|
|
23
24
|
import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";
|
|
24
25
|
|
|
25
26
|
const CONVERSATION_PAGE_SIZE = 20;
|
|
@@ -61,6 +62,7 @@ export function ConversationWorkspace() {
|
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
if (page > totalPages) setPage(totalPages);
|
|
63
64
|
}, [page, totalPages]);
|
|
65
|
+
const waitingConversationIds = useWaitingConversationIds();
|
|
64
66
|
const { finishedConversationIds, markRead } =
|
|
65
67
|
useConversationFinishedIndicators(
|
|
66
68
|
conversations,
|
|
@@ -107,6 +109,7 @@ export function ConversationWorkspace() {
|
|
|
107
109
|
finishedConversationIds={finishedConversationIds}
|
|
108
110
|
loading={feed.isPending}
|
|
109
111
|
timeZone={getDashboardTimeZone()}
|
|
112
|
+
waitingConversationIds={waitingConversationIds}
|
|
110
113
|
/>
|
|
111
114
|
<PagePagination
|
|
112
115
|
className="pt-1"
|
|
@@ -135,6 +138,7 @@ export function ConversationWorkspace() {
|
|
|
135
138
|
query={query}
|
|
136
139
|
selectedId={selectedId}
|
|
137
140
|
timeZone={getDashboardTimeZone()}
|
|
141
|
+
waitingConversationIds={waitingConversationIds}
|
|
138
142
|
/>
|
|
139
143
|
</div>
|
|
140
144
|
<section
|
|
@@ -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">
|