@sentry/junior-dashboard 0.265.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/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +13 -10
- package/src/client/components/README.md +28 -3
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +60 -0
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +61 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- 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/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +77 -29
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +312 -119
- package/src/client/conversations/README.md +30 -10
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- 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/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +13 -7
- package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
- 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/eventLog.ts +1 -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/transcriptMentions.tsx +18 -5
- 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/mock-reporting/watches.ts +1 -1
- 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
|
@@ -37,6 +37,19 @@ function TableBody({ className, ...props }: ComponentProps<"tbody">) {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function TableFooter({ className, ...props }: ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t border-dashboard-border font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function TableRow({ className, ...props }: ComponentProps<"tr">) {
|
|
41
54
|
return (
|
|
42
55
|
<tr
|
|
@@ -77,4 +90,12 @@ function TableCell({ className, ...props }: ComponentProps<"td">) {
|
|
|
77
90
|
);
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
export {
|
|
93
|
+
export {
|
|
94
|
+
Table,
|
|
95
|
+
TableHeader,
|
|
96
|
+
TableBody,
|
|
97
|
+
TableFooter,
|
|
98
|
+
TableHead,
|
|
99
|
+
TableRow,
|
|
100
|
+
TableCell,
|
|
101
|
+
};
|
|
@@ -132,7 +132,7 @@ function TooltipContent({
|
|
|
132
132
|
collisionPadding={8}
|
|
133
133
|
side={side}
|
|
134
134
|
sideOffset={sideOffset}
|
|
135
|
-
className="isolate z-50 data-anchor-hidden:
|
|
135
|
+
className="isolate z-50 data-anchor-hidden:invisible"
|
|
136
136
|
>
|
|
137
137
|
<TooltipPrimitive.Popup
|
|
138
138
|
data-slot="tooltip-content"
|
|
@@ -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>
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
CircleDot,
|
|
8
8
|
CirclePause,
|
|
9
9
|
CircleX,
|
|
10
|
-
LoaderCircle,
|
|
11
10
|
LockKeyhole,
|
|
12
11
|
type LucideIcon,
|
|
13
12
|
} from "lucide-react";
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
} from "@sentry/junior/api/schema";
|
|
18
17
|
|
|
19
18
|
import type { StatusChipTone } from "../components/StatusChip";
|
|
19
|
+
import { Spinner } from "../components/ui/spinner";
|
|
20
20
|
import {
|
|
21
21
|
formatConversationCostTotal,
|
|
22
22
|
formatRuntime,
|
|
@@ -115,7 +115,6 @@ const statusIcons: Partial<Record<string, LucideIcon>> = {
|
|
|
115
115
|
error: CircleX,
|
|
116
116
|
failed: CircleX,
|
|
117
117
|
paused: CirclePause,
|
|
118
|
-
running: LoaderCircle,
|
|
119
118
|
};
|
|
120
119
|
|
|
121
120
|
const statusIconToneClass: Record<StatusChipTone, string> = {
|
|
@@ -129,7 +128,10 @@ const statusIconToneClass: Record<StatusChipTone, string> = {
|
|
|
129
128
|
|
|
130
129
|
/** Mark a child agent status with one icon and an audible label. */
|
|
131
130
|
function ChildAgentStatusIcon(props: { status: string }) {
|
|
132
|
-
const Icon =
|
|
131
|
+
const Icon =
|
|
132
|
+
props.status === "running"
|
|
133
|
+
? Spinner
|
|
134
|
+
: (statusIcons[props.status] ?? Circle);
|
|
133
135
|
return (
|
|
134
136
|
<span className="mt-px grid size-4 shrink-0 place-items-center">
|
|
135
137
|
<Icon
|
|
@@ -137,8 +139,6 @@ function ChildAgentStatusIcon(props: { status: string }) {
|
|
|
137
139
|
className={cn(
|
|
138
140
|
"size-3.5",
|
|
139
141
|
statusIconToneClass[subagentStatusTone(props.status)],
|
|
140
|
-
props.status === "running" &&
|
|
141
|
-
"animate-spin motion-reduce:animate-none",
|
|
142
142
|
)}
|
|
143
143
|
/>
|
|
144
144
|
<span className="sr-only">{props.status}</span>
|
|
@@ -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 (
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { Ellipsis, Menu } from "lucide-react";
|
|
11
11
|
|
|
12
12
|
import { SearchInput } from "../components/SearchInput";
|
|
13
|
+
import { Button } from "../components/ui/button";
|
|
13
14
|
import { cn } from "../styles";
|
|
14
15
|
import {
|
|
15
16
|
MobileHeaderActionsPortal,
|
|
@@ -149,17 +150,17 @@ export function ConversationHeader(props: {
|
|
|
149
150
|
view={props.view}
|
|
150
151
|
/>
|
|
151
152
|
{openMobileNavigation ? (
|
|
152
|
-
<
|
|
153
|
-
className="mt-0.5
|
|
153
|
+
<Button
|
|
154
|
+
className="mt-0.5"
|
|
154
155
|
onClick={() => {
|
|
155
156
|
setMobileMenuOpen(false);
|
|
156
157
|
openMobileNavigation();
|
|
157
158
|
}}
|
|
158
|
-
|
|
159
|
+
size="menu"
|
|
159
160
|
>
|
|
160
161
|
<Menu aria-hidden="true" size={16} strokeWidth={2} />
|
|
161
162
|
App menu
|
|
162
|
-
</
|
|
163
|
+
</Button>
|
|
163
164
|
) : null}
|
|
164
165
|
</div>
|
|
165
166
|
) : null}
|
|
@@ -235,19 +235,16 @@ function MenuActionButton(props: {
|
|
|
235
235
|
pressed?: boolean;
|
|
236
236
|
}) {
|
|
237
237
|
return (
|
|
238
|
-
<
|
|
238
|
+
<Button
|
|
239
239
|
aria-label={props.label}
|
|
240
240
|
aria-pressed={props.pressed}
|
|
241
|
-
className={
|
|
242
|
-
"flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none",
|
|
243
|
-
props.pressed && "bg-dashboard-fill-strong",
|
|
244
|
-
)}
|
|
241
|
+
className={props.pressed ? "bg-dashboard-fill-strong" : undefined}
|
|
245
242
|
onClick={props.onClick}
|
|
246
|
-
|
|
243
|
+
size="menu"
|
|
247
244
|
>
|
|
248
245
|
{props.children}
|
|
249
246
|
<span>{props.label}</span>
|
|
250
|
-
</
|
|
247
|
+
</Button>
|
|
251
248
|
);
|
|
252
249
|
}
|
|
253
250
|
|
|
@@ -262,16 +259,15 @@ function ArchiveConversationButton(
|
|
|
262
259
|
const Icon = props.archived ? ArchiveRestore : Archive;
|
|
263
260
|
if (props.layout === "menu") {
|
|
264
261
|
return (
|
|
265
|
-
<
|
|
262
|
+
<Button
|
|
266
263
|
aria-label={label}
|
|
267
|
-
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
268
264
|
disabled={props.disabled}
|
|
269
265
|
onClick={props.onClick}
|
|
270
|
-
|
|
266
|
+
size="menu"
|
|
271
267
|
>
|
|
272
268
|
<Icon aria-hidden="true" size={16} strokeWidth={2} />
|
|
273
269
|
<span>{label}</span>
|
|
274
|
-
</
|
|
270
|
+
</Button>
|
|
275
271
|
);
|
|
276
272
|
}
|
|
277
273
|
return (
|