@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
|
@@ -15,6 +15,35 @@ function Empty({ className, ...props }: ComponentProps<"div">) {
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Hold the icon that names the kind of empty state. */
|
|
19
|
+
function EmptyMedia({ className, ...props }: ComponentProps<"div">) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
data-slot="empty-media"
|
|
24
|
+
className={cn(
|
|
25
|
+
"grid size-8 shrink-0 place-items-center rounded-md bg-dashboard-fill-strong text-dashboard-text-subtle [&_svg:not([class*='size-'])]:size-4",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Name the empty state in a few words. */
|
|
34
|
+
function EmptyTitle({ className, ...props }: ComponentProps<"div">) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
data-slot="empty-title"
|
|
38
|
+
className={cn(
|
|
39
|
+
"font-display text-base font-medium leading-tight text-dashboard-text",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
18
47
|
/** Explain why the region is empty and what the user can do next. */
|
|
19
48
|
function EmptyDescription({ className, ...props }: ComponentProps<"div">) {
|
|
20
49
|
return (
|
|
@@ -29,4 +58,4 @@ function EmptyDescription({ className, ...props }: ComponentProps<"div">) {
|
|
|
29
58
|
);
|
|
30
59
|
}
|
|
31
60
|
|
|
32
|
-
export { Empty, EmptyDescription };
|
|
61
|
+
export { Empty, EmptyDescription, EmptyMedia, EmptyTitle };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
function Separator({
|
|
5
|
+
className,
|
|
6
|
+
orientation = "horizontal",
|
|
7
|
+
...props
|
|
8
|
+
}: SeparatorPrimitive.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<SeparatorPrimitive
|
|
11
|
+
data-slot="separator"
|
|
12
|
+
orientation={orientation}
|
|
13
|
+
className={cn(
|
|
14
|
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { Separator };
|
|
@@ -48,7 +48,11 @@ export function useSheet(
|
|
|
48
48
|
open,
|
|
49
49
|
modal: topmost ? "trap-focus" : false,
|
|
50
50
|
disablePointerDismissal: !topmost,
|
|
51
|
-
|
|
51
|
+
// Dialog and Drawer roots both use this handler, so read only `cancel`.
|
|
52
|
+
onOpenChange(
|
|
53
|
+
nextOpen: boolean,
|
|
54
|
+
details: Pick<Dialog.Root.ChangeEventDetails, "cancel">,
|
|
55
|
+
) {
|
|
52
56
|
if (!nextOpen && !topmost) {
|
|
53
57
|
details.cancel();
|
|
54
58
|
return;
|
|
@@ -4,7 +4,31 @@ import {
|
|
|
4
4
|
MAX_INPUT_IMAGE_BYTES,
|
|
5
5
|
MAX_INPUT_IMAGES,
|
|
6
6
|
type InputImage,
|
|
7
|
+
type MessageAttachment,
|
|
7
8
|
} from "@sentry/junior/api/schema";
|
|
9
|
+
import { Button } from "../components/ui/button";
|
|
10
|
+
import {
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "../components/ui/tooltip";
|
|
15
|
+
import {
|
|
16
|
+
conversationAttachmentUrl,
|
|
17
|
+
inputImageSrc,
|
|
18
|
+
} from "./TranscriptAttachment";
|
|
19
|
+
|
|
20
|
+
/** Decoded size of base64 image data, as the server limit counts it. */
|
|
21
|
+
function inputImageBytes(images: readonly InputImage[]): number {
|
|
22
|
+
return images.reduce((sum, image) => sum + (image.data.length * 3) / 4, 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** True when these images fit the message image count and size limits. */
|
|
26
|
+
export function composerImagesFit(images: readonly InputImage[]): boolean {
|
|
27
|
+
return (
|
|
28
|
+
images.length <= MAX_INPUT_IMAGES &&
|
|
29
|
+
inputImageBytes(images) <= MAX_INPUT_IMAGE_BYTES
|
|
30
|
+
);
|
|
31
|
+
}
|
|
8
32
|
|
|
9
33
|
/** Read a bounded image selection for the message request and local previews. */
|
|
10
34
|
export async function readComposerImages(
|
|
@@ -13,10 +37,7 @@ export async function readComposerImages(
|
|
|
13
37
|
): Promise<InputImage[]> {
|
|
14
38
|
if (current.length + files.length > MAX_INPUT_IMAGES)
|
|
15
39
|
throw new Error("Attach up to 3 images.");
|
|
16
|
-
const currentBytes = current
|
|
17
|
-
(sum, image) => sum + (image.data.length * 3) / 4,
|
|
18
|
-
0,
|
|
19
|
-
);
|
|
40
|
+
const currentBytes = inputImageBytes(current);
|
|
20
41
|
if (
|
|
21
42
|
currentBytes + files.reduce((sum, file) => sum + file.size, 0) >
|
|
22
43
|
MAX_INPUT_IMAGE_BYTES
|
|
@@ -51,6 +72,28 @@ export async function readComposerImages(
|
|
|
51
72
|
return [...current, ...images];
|
|
52
73
|
}
|
|
53
74
|
|
|
75
|
+
/** Read stored image attachments back into composer images for an edit. */
|
|
76
|
+
export async function readStoredComposerImages(
|
|
77
|
+
conversationId: string,
|
|
78
|
+
attachments: readonly MessageAttachment[] | undefined,
|
|
79
|
+
): Promise<InputImage[]> {
|
|
80
|
+
const images = (attachments ?? []).filter((attachment) =>
|
|
81
|
+
INPUT_IMAGE_TYPES.some((type) => type === attachment.contentType),
|
|
82
|
+
);
|
|
83
|
+
const files = await Promise.all(
|
|
84
|
+
images.map(async (attachment) => {
|
|
85
|
+
const response = await fetch(
|
|
86
|
+
conversationAttachmentUrl(conversationId, attachment.id),
|
|
87
|
+
);
|
|
88
|
+
if (!response.ok) throw new Error("Could not read the queued image.");
|
|
89
|
+
return new File([await response.blob()], attachment.filename, {
|
|
90
|
+
type: attachment.contentType,
|
|
91
|
+
});
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
return await readComposerImages(files, []);
|
|
95
|
+
}
|
|
96
|
+
|
|
54
97
|
/** Show removable images before sending; bytes stay in memory until submit. */
|
|
55
98
|
export function ComposerImages(props: {
|
|
56
99
|
images: InputImage[];
|
|
@@ -67,17 +110,26 @@ export function ComposerImages(props: {
|
|
|
67
110
|
<img
|
|
68
111
|
alt={image.filename}
|
|
69
112
|
className="h-20 w-24 rounded-md object-contain"
|
|
70
|
-
src={
|
|
113
|
+
src={inputImageSrc(image)}
|
|
71
114
|
/>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
<Tooltip>
|
|
116
|
+
<TooltipTrigger
|
|
117
|
+
render={
|
|
118
|
+
<Button
|
|
119
|
+
aria-label={`Remove ${image.filename}`}
|
|
120
|
+
className="absolute right-0 top-0 size-7 rounded-full border border-dashboard-border bg-dashboard-surface-raised text-dashboard-text data-disabled:hover:!bg-dashboard-surface-raised data-disabled:hover:!text-dashboard-text"
|
|
121
|
+
disabled={props.disabled}
|
|
122
|
+
focusableWhenDisabled
|
|
123
|
+
onClick={() => props.onRemove(index)}
|
|
124
|
+
size="icon"
|
|
125
|
+
type="button"
|
|
126
|
+
>
|
|
127
|
+
<X aria-hidden="true" size={14} />
|
|
128
|
+
</Button>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
<TooltipContent size="compact">Remove image</TooltipContent>
|
|
132
|
+
</Tooltip>
|
|
81
133
|
</div>
|
|
82
134
|
))}
|
|
83
135
|
</div>
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { INPUT_IMAGE_TYPES, type InputImage } from "@sentry/junior/api/schema";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ComposerImages,
|
|
4
|
+
composerImagesFit,
|
|
5
|
+
readComposerImages,
|
|
6
|
+
} from "./ComposerImages";
|
|
3
7
|
import {
|
|
4
8
|
memo,
|
|
5
9
|
useEffect,
|
|
6
10
|
useId,
|
|
11
|
+
useImperativeHandle,
|
|
7
12
|
useRef,
|
|
8
13
|
useState,
|
|
9
14
|
type FormEvent,
|
|
10
15
|
type KeyboardEvent,
|
|
11
16
|
type ReactNode,
|
|
17
|
+
type Ref,
|
|
12
18
|
} from "react";
|
|
13
19
|
import { ImagePlus, Send, Square } from "lucide-react";
|
|
14
20
|
|
|
@@ -56,6 +62,14 @@ export function conversationAttemptForSubmit(
|
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** Imperative composer actions for the reply footer. */
|
|
66
|
+
export type ConversationComposerHandle = {
|
|
67
|
+
/** True when the draft has room for these images right now. */
|
|
68
|
+
canRestoreImages(images: readonly InputImage[]): boolean;
|
|
69
|
+
/** Put queued text and images ahead of the draft. Queued images win. */
|
|
70
|
+
restoreMessage(message: { images?: InputImage[]; text: string }): void;
|
|
71
|
+
};
|
|
72
|
+
|
|
59
73
|
type ConversationComposerProps = {
|
|
60
74
|
disabled?: boolean;
|
|
61
75
|
draftId: string;
|
|
@@ -73,6 +87,8 @@ type ConversationComposerProps = {
|
|
|
73
87
|
* the pending queue instead.
|
|
74
88
|
*/
|
|
75
89
|
restoreDraftOnError?: boolean;
|
|
90
|
+
/** Lets the parent put a message taken out of the queue back in the draft. */
|
|
91
|
+
ref?: Ref<ConversationComposerHandle>;
|
|
76
92
|
submitLabel: string;
|
|
77
93
|
onFocus?: () => void;
|
|
78
94
|
onSubmit(
|
|
@@ -93,7 +109,7 @@ type ConversationComposerProps = {
|
|
|
93
109
|
export const ConversationComposer = memo(function ConversationComposer(
|
|
94
110
|
props: ConversationComposerProps,
|
|
95
111
|
) {
|
|
96
|
-
const storageKey =
|
|
112
|
+
const storageKey = draftStorageKey(props.draftId);
|
|
97
113
|
const [initialDraft] = useState<ConversationDraft>(() =>
|
|
98
114
|
readStoredDraft(storageKey),
|
|
99
115
|
);
|
|
@@ -184,7 +200,18 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
184
200
|
setReadingImages(true);
|
|
185
201
|
setImageError(undefined);
|
|
186
202
|
try {
|
|
187
|
-
|
|
203
|
+
const before = imagesRef.current;
|
|
204
|
+
const added = (await readComposerImages(files, before)).slice(
|
|
205
|
+
before.length,
|
|
206
|
+
);
|
|
207
|
+
// An edit can restore images during the read. Add to those images
|
|
208
|
+
// instead of replacing them.
|
|
209
|
+
const next = [...imagesRef.current, ...added];
|
|
210
|
+
if (!composerImagesFit(next)) {
|
|
211
|
+
setImageError("Attach up to 3 images that total 3 MB or less.");
|
|
212
|
+
} else {
|
|
213
|
+
replaceImages(next);
|
|
214
|
+
}
|
|
188
215
|
textareaRef.current?.focus({ preventScroll: true });
|
|
189
216
|
} catch (error) {
|
|
190
217
|
setImageError(
|
|
@@ -306,6 +333,12 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
306
333
|
};
|
|
307
334
|
|
|
308
335
|
// Put queued text back ahead of anything the reader already typed.
|
|
336
|
+
const prependDraftText = (text: string) => {
|
|
337
|
+
const next = withPrependedText(draftRef.current, text);
|
|
338
|
+
replaceMessage(next);
|
|
339
|
+
storeDraft(storageKey, next);
|
|
340
|
+
};
|
|
341
|
+
|
|
309
342
|
const stopTurn = async () => {
|
|
310
343
|
if (!props.onStop || stoppingRef.current) return;
|
|
311
344
|
stoppingRef.current = true;
|
|
@@ -319,16 +352,33 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
319
352
|
stoppingRef.current = false;
|
|
320
353
|
}
|
|
321
354
|
if (!restored) return;
|
|
322
|
-
|
|
323
|
-
const next: ConversationDraft = {
|
|
324
|
-
...draftRef.current,
|
|
325
|
-
text: current.trim() ? `${restored}\n${current}` : restored,
|
|
326
|
-
};
|
|
327
|
-
replaceMessage(next);
|
|
328
|
-
storeDraft(storageKey, next);
|
|
355
|
+
prependDraftText(restored);
|
|
329
356
|
textareaRef.current?.focus({ preventScroll: true });
|
|
330
357
|
};
|
|
331
358
|
|
|
359
|
+
useImperativeHandle(props.ref, () => ({
|
|
360
|
+
canRestoreImages: (images) =>
|
|
361
|
+
!images.length || composerImagesFit([...images, ...imagesRef.current]),
|
|
362
|
+
restoreMessage: (message) => {
|
|
363
|
+
// An image-only message has no text to put ahead of the draft.
|
|
364
|
+
if (message.text.trim()) prependDraftText(message.text);
|
|
365
|
+
const restoredImages = message.images ?? [];
|
|
366
|
+
if (restoredImages.length) {
|
|
367
|
+
// The queued images already left the queue, so they win. A draft
|
|
368
|
+
// image added during the cancel stays only while it still fits.
|
|
369
|
+
const merged = [...restoredImages];
|
|
370
|
+
for (const image of imagesRef.current) {
|
|
371
|
+
if (composerImagesFit([...merged, image])) merged.push(image);
|
|
372
|
+
}
|
|
373
|
+
if (merged.length < restoredImages.length + imagesRef.current.length) {
|
|
374
|
+
setImageError("Some draft images did not fit. Add them again.");
|
|
375
|
+
}
|
|
376
|
+
replaceImages(merged);
|
|
377
|
+
}
|
|
378
|
+
textareaRef.current?.focus({ preventScroll: true });
|
|
379
|
+
},
|
|
380
|
+
}));
|
|
381
|
+
|
|
332
382
|
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
333
383
|
if (
|
|
334
384
|
event.key === "Enter" &&
|
|
@@ -484,7 +534,6 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
484
534
|
aria-label={props.stopPending ? "Stopping" : "Stop"}
|
|
485
535
|
disabled={props.stopPending || !online}
|
|
486
536
|
onClick={() => void stopTurn()}
|
|
487
|
-
title="Stop Junior"
|
|
488
537
|
>
|
|
489
538
|
<Square aria-hidden="true" fill="currentColor" size={12} />
|
|
490
539
|
<span className="hidden md:inline">
|
|
@@ -501,7 +550,6 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
501
550
|
sendLocked ||
|
|
502
551
|
readingImages
|
|
503
552
|
}
|
|
504
|
-
title={sendLocked ? "Sending message" : props.submitLabel}
|
|
505
553
|
variant="primary"
|
|
506
554
|
type="submit"
|
|
507
555
|
>
|
|
@@ -571,6 +619,30 @@ function readStoredDraft(storageKey: string): ConversationDraft {
|
|
|
571
619
|
return emptyDraft();
|
|
572
620
|
}
|
|
573
621
|
|
|
622
|
+
function draftStorageKey(draftId: string): string {
|
|
623
|
+
return `${DRAFT_STORAGE_PREFIX}${encodeURIComponent(draftId)}`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function withPrependedText(
|
|
627
|
+
draft: ConversationDraft,
|
|
628
|
+
text: string,
|
|
629
|
+
): ConversationDraft {
|
|
630
|
+
return {
|
|
631
|
+
...draft,
|
|
632
|
+
text: draft.text.trim() ? `${text}\n${draft.text}` : text,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Put queued text ahead of the saved draft when the composer is not mounted.
|
|
638
|
+
* The composer reads the saved draft the next time it opens.
|
|
639
|
+
*/
|
|
640
|
+
export function restoreStoredDraftText(draftId: string, text: string): void {
|
|
641
|
+
if (!text.trim()) return;
|
|
642
|
+
const storageKey = draftStorageKey(draftId);
|
|
643
|
+
storeDraft(storageKey, withPrependedText(readStoredDraft(storageKey), text));
|
|
644
|
+
}
|
|
645
|
+
|
|
574
646
|
function storeDraft(storageKey: string, draft: ConversationDraft): void {
|
|
575
647
|
if (typeof window === "undefined") return;
|
|
576
648
|
try {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
ConversationWatches,
|
|
29
29
|
useConversationWatches,
|
|
30
30
|
} from "./ConversationWatches";
|
|
31
|
+
import { DetailsCard } from "./DetailsList";
|
|
31
32
|
import { ShareConversationButton } from "./ShareConversationButton";
|
|
32
33
|
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
33
34
|
|
|
@@ -158,13 +159,13 @@ export function ConversationDetails(props: {
|
|
|
158
159
|
</div>
|
|
159
160
|
) : null}
|
|
160
161
|
{sections.map((section) => (
|
|
161
|
-
<
|
|
162
|
+
<DetailsCard
|
|
162
163
|
count={section.count}
|
|
163
164
|
key={section.key}
|
|
164
165
|
title={section.title}
|
|
165
166
|
>
|
|
166
167
|
{section.content}
|
|
167
|
-
</
|
|
168
|
+
</DetailsCard>
|
|
168
169
|
))}
|
|
169
170
|
</div>
|
|
170
171
|
) : (
|
|
@@ -201,31 +202,6 @@ function TabCount(props: { count: number | undefined }) {
|
|
|
201
202
|
);
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/** Group one kind of conversation detail in a compact card. */
|
|
205
|
-
function DetailsSection(props: {
|
|
206
|
-
children: ReactNode;
|
|
207
|
-
count?: number;
|
|
208
|
-
title?: string;
|
|
209
|
-
}) {
|
|
210
|
-
return (
|
|
211
|
-
<section className="grid min-w-0 gap-2.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3">
|
|
212
|
-
{props.title ? (
|
|
213
|
-
<h3 className="m-0 flex items-center gap-1.5 font-sans text-xs font-medium text-dashboard-text-muted">
|
|
214
|
-
{props.title}
|
|
215
|
-
{props.count ? (
|
|
216
|
-
<span className="tabular-nums text-dashboard-text-faint">
|
|
217
|
-
{props.count.toLocaleString("en-US")}
|
|
218
|
-
</span>
|
|
219
|
-
) : null}
|
|
220
|
-
</h3>
|
|
221
|
-
) : null}
|
|
222
|
-
<div className="min-w-0 break-words font-sans text-xs leading-relaxed text-dashboard-text-muted">
|
|
223
|
-
{props.children}
|
|
224
|
-
</div>
|
|
225
|
-
</section>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
205
|
/** Draw the Sentry glyph in the current text color, sized like a header icon. */
|
|
230
206
|
function SentryGlyph() {
|
|
231
207
|
return (
|
|
@@ -4,6 +4,10 @@ import { ChevronDown, ChevronUp } from "lucide-react";
|
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import { DetailsPanel } from "../components/DetailsPanel";
|
|
6
6
|
import { Button } from "../components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
ButtonGroup,
|
|
9
|
+
ButtonGroupSeparator,
|
|
10
|
+
} from "../components/ui/button-group";
|
|
7
11
|
import { Skeleton } from "../components/ui/skeleton";
|
|
8
12
|
import type { ConversationTranscript } from "../types";
|
|
9
13
|
import { useEventTime } from "./eventTime";
|
|
@@ -11,8 +15,9 @@ import { EventDetails } from "./EventDetails";
|
|
|
11
15
|
import { ForkConversationButton } from "./ForkConversationButton";
|
|
12
16
|
|
|
13
17
|
// Step buttons keep focus at the ends of the log, so style the ARIA state.
|
|
18
|
+
// They sit in one bordered group, so they are a little smaller than `size-9`.
|
|
14
19
|
const stepButtonClass =
|
|
15
|
-
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:!bg-transparent data-disabled:hover:!text-dashboard-text-muted";
|
|
20
|
+
"size-8 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:!bg-transparent data-disabled:hover:!text-dashboard-text-muted";
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Show the selected event without blocking the main conversation's event log.
|
|
@@ -30,14 +35,21 @@ export function ConversationEventDetails(props: {
|
|
|
30
35
|
}) {
|
|
31
36
|
const { conversation, event, onSelect } = props;
|
|
32
37
|
const time = useEventTime();
|
|
38
|
+
// The sequence number matches the `event` link parameter and the row label.
|
|
39
|
+
// The line stays on one row beside the actions. The tooltip keeps the
|
|
40
|
+
// milliseconds that the short timestamp leaves out.
|
|
33
41
|
const timestamp = event ? (
|
|
34
|
-
<
|
|
35
|
-
className="
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
<p className="m-0 flex min-w-0 gap-x-1.5 whitespace-nowrap font-sans text-xs tabular-nums text-dashboard-text-muted">
|
|
43
|
+
<span className="shrink-0">#{event.seq}</span>
|
|
44
|
+
<span aria-hidden="true">·</span>
|
|
45
|
+
<time
|
|
46
|
+
className="min-w-0 truncate"
|
|
47
|
+
dateTime={event.createdAt}
|
|
48
|
+
title={time.formatTimestamp(event.createdAt)}
|
|
49
|
+
>
|
|
50
|
+
{time.formatShortTimestamp(event.createdAt)}
|
|
51
|
+
</time>
|
|
52
|
+
</p>
|
|
41
53
|
) : null;
|
|
42
54
|
const title = event ? (
|
|
43
55
|
<>
|
|
@@ -54,7 +66,10 @@ export function ConversationEventDetails(props: {
|
|
|
54
66
|
);
|
|
55
67
|
const actions =
|
|
56
68
|
onSelect && event ? (
|
|
57
|
-
|
|
69
|
+
<ButtonGroup
|
|
70
|
+
aria-label="Event navigation"
|
|
71
|
+
className="rounded-md border border-dashboard-border"
|
|
72
|
+
>
|
|
58
73
|
<Button
|
|
59
74
|
aria-label="Previous event"
|
|
60
75
|
className={stepButtonClass}
|
|
@@ -68,6 +83,7 @@ export function ConversationEventDetails(props: {
|
|
|
68
83
|
>
|
|
69
84
|
<ChevronUp aria-hidden="true" size={15} />
|
|
70
85
|
</Button>
|
|
86
|
+
<ButtonGroupSeparator />
|
|
71
87
|
<Button
|
|
72
88
|
aria-label="Next event"
|
|
73
89
|
className={stepButtonClass}
|
|
@@ -81,7 +97,7 @@ export function ConversationEventDetails(props: {
|
|
|
81
97
|
>
|
|
82
98
|
<ChevronDown aria-hidden="true" size={15} />
|
|
83
99
|
</Button>
|
|
84
|
-
|
|
100
|
+
</ButtonGroup>
|
|
85
101
|
) : undefined;
|
|
86
102
|
|
|
87
103
|
return (
|
|
@@ -35,6 +35,7 @@ export function ConversationHomeList(props: {
|
|
|
35
35
|
finishedConversationIds: ReadonlySet<string>;
|
|
36
36
|
loading?: boolean;
|
|
37
37
|
timeZone: string;
|
|
38
|
+
waitingConversationIds: ReadonlySet<string>;
|
|
38
39
|
}) {
|
|
39
40
|
const [archivedConversation, setArchivedConversation] =
|
|
40
41
|
useState<Conversation>();
|
|
@@ -94,6 +95,7 @@ export function ConversationHomeList(props: {
|
|
|
94
95
|
key={section.key}
|
|
95
96
|
onArchiveError={handleArchiveError}
|
|
96
97
|
onArchived={handleArchived}
|
|
98
|
+
waitingConversationIds={props.waitingConversationIds}
|
|
97
99
|
section={section}
|
|
98
100
|
/>
|
|
99
101
|
))}
|
|
@@ -169,6 +171,7 @@ function ConversationCardSection(props: {
|
|
|
169
171
|
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
|
|
170
172
|
onArchived(conversation: Conversation): void;
|
|
171
173
|
section: ConversationSection;
|
|
174
|
+
waitingConversationIds: ReadonlySet<string>;
|
|
172
175
|
}) {
|
|
173
176
|
return (
|
|
174
177
|
<section aria-labelledby={`conversation-section-${props.section.key}`}>
|
|
@@ -188,6 +191,7 @@ function ConversationCardSection(props: {
|
|
|
188
191
|
key={conversation.id}
|
|
189
192
|
onArchiveError={props.onArchiveError}
|
|
190
193
|
onArchived={props.onArchived}
|
|
194
|
+
waitingOnViewer={props.waitingConversationIds.has(conversation.id)}
|
|
191
195
|
/>
|
|
192
196
|
))}
|
|
193
197
|
</div>
|
|
@@ -200,6 +204,7 @@ function ConversationCard(props: {
|
|
|
200
204
|
finishedSinceSeen: boolean;
|
|
201
205
|
onArchiveError(conversation: Conversation, wasArchiving: boolean): void;
|
|
202
206
|
onArchived(conversation: Conversation): void;
|
|
207
|
+
waitingOnViewer: boolean;
|
|
203
208
|
}) {
|
|
204
209
|
const conversation = props.conversation;
|
|
205
210
|
const archive = useArchiveConversation(conversation.id, {
|
|
@@ -231,6 +236,7 @@ function ConversationCard(props: {
|
|
|
231
236
|
finishedSinceSeen={props.finishedSinceSeen}
|
|
232
237
|
isPrivate={isPrivate}
|
|
233
238
|
status={status}
|
|
239
|
+
waitingOnViewer={props.waitingOnViewer}
|
|
234
240
|
/>
|
|
235
241
|
</span>
|
|
236
242
|
<h4 className="m-0 truncate font-display text-base font-medium leading-snug text-dashboard-text">
|
|
@@ -4,12 +4,26 @@ import { ActiveIndicator } from "../components/ActiveIndicator";
|
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import type { VisualStatus } from "../types";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Render a conversation status marker. "Waiting on you" replaces every other
|
|
9
|
+
* marker, then new completions take priority.
|
|
10
|
+
*/
|
|
8
11
|
export function ConversationListStatusIcon(props: {
|
|
9
12
|
finishedSinceSeen: boolean;
|
|
10
13
|
isPrivate: boolean;
|
|
11
14
|
status: VisualStatus;
|
|
15
|
+
waitingOnViewer: boolean;
|
|
12
16
|
}) {
|
|
17
|
+
if (props.waitingOnViewer) {
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
aria-label="Waiting on you"
|
|
21
|
+
className="size-2 shrink-0 rounded-full bg-violet-300 shadow-[0_0_6px_rgba(196,181,253,0.7)]"
|
|
22
|
+
role="img"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
if (props.finishedSinceSeen) {
|
|
14
28
|
return (
|
|
15
29
|
<span
|