@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
|
@@ -6,47 +6,91 @@ import {
|
|
|
6
6
|
type ReactElement,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { INPUT_IMAGE_TYPES } from "@sentry/junior/api/schema";
|
|
9
10
|
import {
|
|
10
11
|
AlertCircle,
|
|
11
12
|
Clock3,
|
|
12
|
-
|
|
13
|
+
FileText,
|
|
14
|
+
Pencil,
|
|
13
15
|
SkipForward,
|
|
14
|
-
|
|
16
|
+
Trash2,
|
|
17
|
+
Zap,
|
|
15
18
|
type LucideIcon,
|
|
16
19
|
} from "lucide-react";
|
|
17
20
|
|
|
21
|
+
import { Button } from "../components/ui/button";
|
|
18
22
|
import {
|
|
19
23
|
Tooltip,
|
|
20
24
|
TooltipContent,
|
|
21
25
|
TooltipTrigger,
|
|
22
26
|
} from "../components/ui/tooltip";
|
|
23
|
-
import {
|
|
27
|
+
import { ActorAvatar } from "../components/ActorAvatar";
|
|
28
|
+
import { ImageAttachment } from "../components/ImageAttachment";
|
|
29
|
+
import { ParticipantAvatar } from "../components/ParticipantAvatarStack";
|
|
30
|
+
import { Spinner } from "../components/ui/spinner";
|
|
24
31
|
import type { ConversationMailboxMessage } from "./conversationOutbox";
|
|
32
|
+
import {
|
|
33
|
+
conversationAttachmentUrl,
|
|
34
|
+
inputImageSrc,
|
|
35
|
+
} from "./TranscriptAttachment";
|
|
25
36
|
import {
|
|
26
37
|
TranscriptHeadingMeta,
|
|
27
38
|
TranscriptHeadingRow,
|
|
28
39
|
} from "./TranscriptHeadingRow";
|
|
29
40
|
import { SlackMark } from "./SlackMark";
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
const
|
|
42
|
+
// Rows are one line each, so the stack can show five before it collapses.
|
|
43
|
+
const MAX_EXPANDED_PENDING_ROWS = 5;
|
|
44
|
+
const COLLAPSED_PENDING_ROW_COUNT = 4;
|
|
45
|
+
|
|
46
|
+
// The steer button and the steered status share an icon, so a steered row
|
|
47
|
+
// shows the icon of the action that the reader used.
|
|
48
|
+
const STEER_ICON = Zap;
|
|
33
49
|
|
|
34
50
|
function pendingDeliveryMeta(
|
|
35
|
-
message: Pick<
|
|
36
|
-
|
|
51
|
+
message: Pick<
|
|
52
|
+
ConversationMailboxMessage,
|
|
53
|
+
"clientStatus" | "delivery" | "source"
|
|
54
|
+
>,
|
|
55
|
+
): { icon: LucideIcon | typeof Spinner; label: string; tooltip: string } {
|
|
37
56
|
if (message.clientStatus === "failed") {
|
|
38
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
icon: AlertCircle,
|
|
59
|
+
label: "Failed to send",
|
|
60
|
+
tooltip: "Failed to send",
|
|
61
|
+
};
|
|
39
62
|
}
|
|
40
63
|
if (message.clientStatus === "sending") {
|
|
41
|
-
return { icon:
|
|
64
|
+
return { icon: Spinner, label: "Sending", tooltip: "Sending" };
|
|
65
|
+
}
|
|
66
|
+
// Only a dashboard steer turns a web message into an interrupt. Other
|
|
67
|
+
// sources, such as a Slack mention, arrive as interrupts.
|
|
68
|
+
if (message.delivery === "interrupt" && message.source !== "web") {
|
|
69
|
+
return {
|
|
70
|
+
icon: SkipForward,
|
|
71
|
+
label: "Next step",
|
|
72
|
+
tooltip: "Junior reads it at its next step",
|
|
73
|
+
};
|
|
42
74
|
}
|
|
43
75
|
if (message.delivery === "interrupt") {
|
|
44
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
icon: STEER_ICON,
|
|
78
|
+
label: "Steered",
|
|
79
|
+
tooltip: "Steered: Junior reads it at its next step",
|
|
80
|
+
};
|
|
45
81
|
}
|
|
46
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
icon: Clock3,
|
|
84
|
+
label: "Queued",
|
|
85
|
+
tooltip: "Queued: Junior reads it after the current reply",
|
|
86
|
+
};
|
|
47
87
|
}
|
|
48
88
|
|
|
49
|
-
function PendingMetaIcon(props: {
|
|
89
|
+
function PendingMetaIcon(props: {
|
|
90
|
+
children: ReactElement;
|
|
91
|
+
label: string;
|
|
92
|
+
tooltip?: string;
|
|
93
|
+
}) {
|
|
50
94
|
return (
|
|
51
95
|
<Tooltip>
|
|
52
96
|
<TooltipTrigger
|
|
@@ -59,24 +103,69 @@ function PendingMetaIcon(props: { children: ReactElement; label: string }) {
|
|
|
59
103
|
</span>
|
|
60
104
|
}
|
|
61
105
|
/>
|
|
62
|
-
<TooltipContent>
|
|
106
|
+
<TooltipContent size="compact">
|
|
107
|
+
{props.tooltip ?? props.label}
|
|
108
|
+
</TooltipContent>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One icon-only row action. The tooltip says what the action does. */
|
|
114
|
+
function PendingActionButton(props: {
|
|
115
|
+
disabled: boolean;
|
|
116
|
+
icon: LucideIcon;
|
|
117
|
+
label: string;
|
|
118
|
+
onClick(): void;
|
|
119
|
+
pending: boolean;
|
|
120
|
+
tooltip: string;
|
|
121
|
+
variant?: "danger";
|
|
122
|
+
}) {
|
|
123
|
+
const Icon = props.icon;
|
|
124
|
+
return (
|
|
125
|
+
<Tooltip>
|
|
126
|
+
<TooltipTrigger
|
|
127
|
+
render={
|
|
128
|
+
<Button
|
|
129
|
+
aria-label={props.label}
|
|
130
|
+
className="size-6"
|
|
131
|
+
disabled={props.disabled}
|
|
132
|
+
// A disabled action still shows its tooltip on hover and focus.
|
|
133
|
+
focusableWhenDisabled
|
|
134
|
+
onClick={props.onClick}
|
|
135
|
+
size="icon"
|
|
136
|
+
type="button"
|
|
137
|
+
variant={props.variant}
|
|
138
|
+
>
|
|
139
|
+
{props.pending ? (
|
|
140
|
+
<Spinner aria-hidden="true" className="size-3.25" />
|
|
141
|
+
) : (
|
|
142
|
+
<Icon aria-hidden="true" size={14} strokeWidth={2.2} />
|
|
143
|
+
)}
|
|
144
|
+
</Button>
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
<TooltipContent size="compact">{props.tooltip}</TooltipContent>
|
|
63
148
|
</Tooltip>
|
|
64
149
|
);
|
|
65
150
|
}
|
|
66
151
|
|
|
67
152
|
function PendingMetaIcons(props: {
|
|
68
|
-
|
|
153
|
+
actionsDisabled: boolean;
|
|
69
154
|
cancelError: boolean;
|
|
70
155
|
cancelPending: boolean;
|
|
156
|
+
editPending: boolean;
|
|
71
157
|
message: ConversationMailboxMessage;
|
|
72
158
|
onCancel?: () => void;
|
|
159
|
+
onEdit?: () => void;
|
|
160
|
+
onSteer?: () => void;
|
|
73
161
|
showSlack: boolean;
|
|
162
|
+
steerPending: boolean;
|
|
74
163
|
}) {
|
|
75
164
|
const delivery = pendingDeliveryMeta(props.message);
|
|
76
165
|
const DeliveryIcon = delivery.icon;
|
|
77
166
|
|
|
78
167
|
return (
|
|
79
|
-
<TranscriptHeadingMeta className="flex min-w-0 items-center justify-end gap-1
|
|
168
|
+
<TranscriptHeadingMeta className="flex min-w-0 items-center justify-end gap-1 text-xs leading-none text-dashboard-text-muted">
|
|
80
169
|
{props.showSlack ? (
|
|
81
170
|
<Tooltip>
|
|
82
171
|
<TooltipTrigger
|
|
@@ -86,70 +175,138 @@ function PendingMetaIcons(props: {
|
|
|
86
175
|
</span>
|
|
87
176
|
}
|
|
88
177
|
/>
|
|
89
|
-
<TooltipContent>Slack</TooltipContent>
|
|
178
|
+
<TooltipContent size="compact">From Slack</TooltipContent>
|
|
90
179
|
</Tooltip>
|
|
91
180
|
) : null}
|
|
92
|
-
<PendingMetaIcon label={delivery.label}>
|
|
181
|
+
<PendingMetaIcon label={delivery.label} tooltip={delivery.tooltip}>
|
|
93
182
|
<DeliveryIcon
|
|
94
183
|
aria-hidden="true"
|
|
95
|
-
className=
|
|
96
|
-
size={13}
|
|
184
|
+
className="size-3.25"
|
|
97
185
|
strokeWidth={2.2}
|
|
98
186
|
/>
|
|
99
187
|
</PendingMetaIcon>
|
|
188
|
+
{props.onSteer ? (
|
|
189
|
+
<PendingActionButton
|
|
190
|
+
disabled={props.actionsDisabled}
|
|
191
|
+
icon={STEER_ICON}
|
|
192
|
+
label="Steer queued message"
|
|
193
|
+
onClick={props.onSteer}
|
|
194
|
+
pending={props.steerPending}
|
|
195
|
+
tooltip="Steer: Junior reads it at its next step"
|
|
196
|
+
/>
|
|
197
|
+
) : null}
|
|
198
|
+
{props.onEdit ? (
|
|
199
|
+
<PendingActionButton
|
|
200
|
+
disabled={props.actionsDisabled}
|
|
201
|
+
icon={Pencil}
|
|
202
|
+
label="Edit queued message"
|
|
203
|
+
onClick={props.onEdit}
|
|
204
|
+
pending={props.editPending}
|
|
205
|
+
tooltip="Edit: move it back to the composer"
|
|
206
|
+
/>
|
|
207
|
+
) : null}
|
|
100
208
|
{props.onCancel ? (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
className="animate-spin"
|
|
119
|
-
size={13}
|
|
120
|
-
/>
|
|
121
|
-
) : (
|
|
122
|
-
<X aria-hidden="true" size={14} strokeWidth={2.2} />
|
|
123
|
-
)}
|
|
124
|
-
</button>
|
|
125
|
-
}
|
|
126
|
-
/>
|
|
127
|
-
<TooltipContent>Remove queued message</TooltipContent>
|
|
128
|
-
</Tooltip>
|
|
209
|
+
<PendingActionButton
|
|
210
|
+
disabled={props.actionsDisabled}
|
|
211
|
+
icon={Trash2}
|
|
212
|
+
label={
|
|
213
|
+
props.cancelError
|
|
214
|
+
? "Could not delete. Try again."
|
|
215
|
+
: "Delete queued message"
|
|
216
|
+
}
|
|
217
|
+
onClick={props.onCancel}
|
|
218
|
+
pending={props.cancelPending}
|
|
219
|
+
tooltip={
|
|
220
|
+
props.cancelError
|
|
221
|
+
? "Could not delete. Try again."
|
|
222
|
+
: "Delete from the queue"
|
|
223
|
+
}
|
|
224
|
+
variant="danger"
|
|
225
|
+
/>
|
|
129
226
|
) : null}
|
|
130
227
|
</TranscriptHeadingMeta>
|
|
131
228
|
);
|
|
132
229
|
}
|
|
133
230
|
|
|
231
|
+
function PendingAuthorAvatar(props: { message: ConversationMailboxMessage }) {
|
|
232
|
+
// Pending rows can come from web or Slack. Redacted rows hide the author.
|
|
233
|
+
if (!props.message.actorIdentity) {
|
|
234
|
+
return <ActorAvatar name="?" size="list" />;
|
|
235
|
+
}
|
|
236
|
+
return (
|
|
237
|
+
<ParticipantAvatar participant={props.message.actorIdentity} size="list" />
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Show small previews so the reader can tell which images a queued message has. */
|
|
242
|
+
function PendingAttachmentThumbnails(props: {
|
|
243
|
+
conversationId?: string;
|
|
244
|
+
message: ConversationMailboxMessage;
|
|
245
|
+
}) {
|
|
246
|
+
const thumbnailClassName = "size-6 rounded object-cover";
|
|
247
|
+
const triggerClassName =
|
|
248
|
+
"block size-6 flex-none overflow-hidden rounded border border-dashboard-border bg-dashboard-fill-faint transition-colors hover:border-dashboard-border-interactive focus-visible:outline-2 focus-visible:outline-dashboard-focus";
|
|
249
|
+
const localImages = props.message.images ?? [];
|
|
250
|
+
const attachments = props.message.attachments ?? [];
|
|
251
|
+
if (localImages.length === 0 && attachments.length === 0) return null;
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<span className="flex flex-none items-center gap-1">
|
|
255
|
+
{localImages.map((image, index) => (
|
|
256
|
+
<ImageAttachment
|
|
257
|
+
filename={image.filename}
|
|
258
|
+
imageClassName={thumbnailClassName}
|
|
259
|
+
key={`local:${index}`}
|
|
260
|
+
src={inputImageSrc(image)}
|
|
261
|
+
triggerClassName={triggerClassName}
|
|
262
|
+
/>
|
|
263
|
+
))}
|
|
264
|
+
{attachments.map((attachment) => {
|
|
265
|
+
const isImage = INPUT_IMAGE_TYPES.some(
|
|
266
|
+
(type) => type === attachment.contentType,
|
|
267
|
+
);
|
|
268
|
+
if (isImage && props.conversationId) {
|
|
269
|
+
return (
|
|
270
|
+
<ImageAttachment
|
|
271
|
+
filename={attachment.filename}
|
|
272
|
+
imageClassName={thumbnailClassName}
|
|
273
|
+
key={attachment.id}
|
|
274
|
+
loading="lazy"
|
|
275
|
+
src={conversationAttachmentUrl(
|
|
276
|
+
props.conversationId,
|
|
277
|
+
attachment.id,
|
|
278
|
+
)}
|
|
279
|
+
triggerClassName={triggerClassName}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return (
|
|
284
|
+
<PendingMetaIcon key={attachment.id} label={attachment.filename}>
|
|
285
|
+
<FileText aria-hidden="true" size={14} />
|
|
286
|
+
</PendingMetaIcon>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</span>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
134
293
|
function PendingRow(props: {
|
|
135
|
-
|
|
294
|
+
actionsDisabled: boolean;
|
|
136
295
|
cancelError: boolean;
|
|
137
296
|
cancelPending: boolean;
|
|
297
|
+
canEdit: boolean;
|
|
298
|
+
canSteer: boolean;
|
|
299
|
+
conversationId?: string;
|
|
300
|
+
editPending: boolean;
|
|
138
301
|
message: ConversationMailboxMessage;
|
|
139
302
|
onCancel?(message: ConversationMailboxMessage): void;
|
|
303
|
+
onEdit?(message: ConversationMailboxMessage): void;
|
|
140
304
|
onRetry?(message: ConversationMailboxMessage): void;
|
|
305
|
+
onSteer?(message: ConversationMailboxMessage): void;
|
|
306
|
+
steerPending: boolean;
|
|
141
307
|
}) {
|
|
142
|
-
const
|
|
143
|
-
...(props.message.images ?? []),
|
|
144
|
-
...(props.message.attachments ?? []),
|
|
145
|
-
]
|
|
146
|
-
.map((attachment) => attachment.filename)
|
|
147
|
-
.join(", ");
|
|
148
|
-
const text = [props.message.text, filenames].filter(Boolean).join(" · ");
|
|
308
|
+
const text = props.message.text ?? "";
|
|
149
309
|
const redacted = Boolean(props.message.redacted);
|
|
150
|
-
// Pending rows can come from web or Slack. Avoid rebuilding a full
|
|
151
|
-
// transcript projection just to label the stack above the composer.
|
|
152
|
-
const roleLabel = actorLabel(props.message.actorIdentity) ?? "User";
|
|
153
310
|
const showSlack = props.message.source === "slack";
|
|
154
311
|
const canRetry =
|
|
155
312
|
props.message.clientStatus === "failed" &&
|
|
@@ -157,36 +314,59 @@ function PendingRow(props: {
|
|
|
157
314
|
Boolean(props.onRetry);
|
|
158
315
|
|
|
159
316
|
return (
|
|
160
|
-
<article className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-1 px-3 py-
|
|
317
|
+
<article className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-1 px-3 py-1.5 text-dashboard-text md:px-3.5">
|
|
161
318
|
<TranscriptHeadingRow
|
|
319
|
+
className="items-center"
|
|
162
320
|
left={
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
|
|
321
|
+
<>
|
|
322
|
+
<PendingAuthorAvatar message={props.message} />
|
|
323
|
+
{redacted ? null : (
|
|
324
|
+
<PendingAttachmentThumbnails
|
|
325
|
+
conversationId={props.conversationId}
|
|
326
|
+
message={props.message}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
{redacted ? (
|
|
330
|
+
<p className="m-0 font-sans text-sm leading-snug text-dashboard-text-muted">
|
|
331
|
+
[redacted]
|
|
332
|
+
</p>
|
|
333
|
+
) : text ? (
|
|
334
|
+
// One line keeps the queue short. The title shows the full text.
|
|
335
|
+
<p
|
|
336
|
+
className="m-0 min-w-0 truncate font-sans text-sm leading-snug text-dashboard-text-subtle"
|
|
337
|
+
title={text}
|
|
338
|
+
>
|
|
339
|
+
{text}
|
|
340
|
+
</p>
|
|
341
|
+
) : null}
|
|
342
|
+
</>
|
|
166
343
|
}
|
|
167
|
-
leftClassName="text-sm leading-snug text-dashboard-text"
|
|
344
|
+
leftClassName="flex-1 text-sm leading-snug text-dashboard-text"
|
|
168
345
|
right={
|
|
169
346
|
<PendingMetaIcons
|
|
170
|
-
|
|
347
|
+
actionsDisabled={props.actionsDisabled}
|
|
171
348
|
cancelError={props.cancelError}
|
|
172
349
|
cancelPending={props.cancelPending}
|
|
350
|
+
editPending={props.editPending}
|
|
173
351
|
message={props.message}
|
|
174
352
|
onCancel={
|
|
175
353
|
props.onCancel ? () => props.onCancel?.(props.message) : undefined
|
|
176
354
|
}
|
|
355
|
+
onEdit={
|
|
356
|
+
props.canEdit && props.onEdit
|
|
357
|
+
? () => props.onEdit?.(props.message)
|
|
358
|
+
: undefined
|
|
359
|
+
}
|
|
360
|
+
onSteer={
|
|
361
|
+
props.canSteer && props.onSteer
|
|
362
|
+
? () => props.onSteer?.(props.message)
|
|
363
|
+
: undefined
|
|
364
|
+
}
|
|
177
365
|
showSlack={showSlack}
|
|
366
|
+
steerPending={props.steerPending}
|
|
178
367
|
/>
|
|
179
368
|
}
|
|
180
369
|
/>
|
|
181
|
-
{redacted ? (
|
|
182
|
-
<p className="m-0 font-sans text-sm leading-snug text-dashboard-text-muted">
|
|
183
|
-
[redacted]
|
|
184
|
-
</p>
|
|
185
|
-
) : (
|
|
186
|
-
<p className="m-0 line-clamp-3 font-sans text-sm leading-snug text-dashboard-text-subtle">
|
|
187
|
-
{text}
|
|
188
|
-
</p>
|
|
189
|
-
)}
|
|
190
370
|
{canRetry ? (
|
|
191
371
|
<div className="flex min-w-0 items-center gap-2">
|
|
192
372
|
<p className="m-0 font-sans text-xs text-red-300/80">
|
|
@@ -203,7 +383,7 @@ function PendingRow(props: {
|
|
|
203
383
|
) : null}
|
|
204
384
|
{props.cancelError ? (
|
|
205
385
|
<p className="m-0 font-sans text-xs text-amber-100/75">
|
|
206
|
-
Could not
|
|
386
|
+
Could not delete. Try again.
|
|
207
387
|
</p>
|
|
208
388
|
) : null}
|
|
209
389
|
</article>
|
|
@@ -247,6 +427,12 @@ function ExpandQueuedMessagesButton(props: {
|
|
|
247
427
|
);
|
|
248
428
|
}
|
|
249
429
|
|
|
430
|
+
function sameEmail(left: string | undefined, right: string | undefined) {
|
|
431
|
+
return Boolean(
|
|
432
|
+
left && right && left.trim().toLowerCase() === right.trim().toLowerCase(),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
250
436
|
/** Render accepted mailbox rows as a compact stack attached above the composer. */
|
|
251
437
|
export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
252
438
|
cancelError?: boolean;
|
|
@@ -254,11 +440,23 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
254
440
|
cancelTargetInboundMessageId?: string;
|
|
255
441
|
/** Message ids already present in committed history. */
|
|
256
442
|
committedMessageIds?: readonly string[];
|
|
443
|
+
/** Conversation that owns stored attachment previews. */
|
|
444
|
+
conversationId?: string;
|
|
445
|
+
/** Inbound id of the row that an edit is taking out of the queue. */
|
|
446
|
+
editTargetInboundMessageId?: string;
|
|
257
447
|
messages: readonly ConversationMailboxMessage[];
|
|
258
448
|
onCancelMessage?: (message: ConversationMailboxMessage) => void;
|
|
449
|
+
/** Take the viewer's own queued web message back into the composer. */
|
|
450
|
+
onEditMessage?: (message: ConversationMailboxMessage) => void;
|
|
259
451
|
/** Fires after expand/collapse changes the stack height above the composer. */
|
|
260
452
|
onLayoutChange?: () => void;
|
|
261
453
|
onRetry?(message: ConversationMailboxMessage): void;
|
|
454
|
+
/** Ask Junior to read the viewer's own queued web message now. */
|
|
455
|
+
onSteerMessage?: (message: ConversationMailboxMessage) => void;
|
|
456
|
+
/** Inbound id of the row that a steer is sending now. */
|
|
457
|
+
steerTargetInboundMessageId?: string;
|
|
458
|
+
/** Email of the signed-in reader. Only their own web rows can be edited. */
|
|
459
|
+
viewerEmail?: string;
|
|
262
460
|
}): ReactNode {
|
|
263
461
|
const [expanded, setExpanded] = useState(false);
|
|
264
462
|
const onLayoutChangeRef = useRef(props.onLayoutChange);
|
|
@@ -288,6 +486,43 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
288
486
|
const visibleRows = showCollapsed ? previewRows : rows;
|
|
289
487
|
const hiddenCount = Math.max(0, rows.length - COLLAPSED_PENDING_ROW_COUNT);
|
|
290
488
|
const toggleExpanded = () => setExpanded((value) => !value);
|
|
489
|
+
const actionsDisabled =
|
|
490
|
+
Boolean(props.cancelPending) ||
|
|
491
|
+
Boolean(props.editTargetInboundMessageId) ||
|
|
492
|
+
Boolean(props.steerTargetInboundMessageId);
|
|
493
|
+
const renderRow = (message: ConversationMailboxMessage) => {
|
|
494
|
+
const isTarget =
|
|
495
|
+
props.cancelTargetInboundMessageId === message.inboundMessageId;
|
|
496
|
+
// Only accepted server rows can leave the queue. Local rows are still sending.
|
|
497
|
+
const accepted = message.clientStatus === undefined;
|
|
498
|
+
const ownWebRow =
|
|
499
|
+
accepted &&
|
|
500
|
+
message.source === "web" &&
|
|
501
|
+
!message.redacted &&
|
|
502
|
+
sameEmail(message.actorIdentity?.email, props.viewerEmail);
|
|
503
|
+
return (
|
|
504
|
+
<PendingRow
|
|
505
|
+
actionsDisabled={actionsDisabled}
|
|
506
|
+
cancelError={Boolean(props.cancelError && isTarget)}
|
|
507
|
+
cancelPending={Boolean(props.cancelPending && isTarget)}
|
|
508
|
+
canEdit={ownWebRow}
|
|
509
|
+
canSteer={ownWebRow && message.delivery === "defer"}
|
|
510
|
+
conversationId={props.conversationId}
|
|
511
|
+
editPending={
|
|
512
|
+
props.editTargetInboundMessageId === message.inboundMessageId
|
|
513
|
+
}
|
|
514
|
+
key={message.messageId}
|
|
515
|
+
message={message}
|
|
516
|
+
onCancel={accepted ? props.onCancelMessage : undefined}
|
|
517
|
+
onEdit={props.onEditMessage}
|
|
518
|
+
onRetry={props.onRetry}
|
|
519
|
+
onSteer={props.onSteerMessage}
|
|
520
|
+
steerPending={
|
|
521
|
+
props.steerTargetInboundMessageId === message.inboundMessageId
|
|
522
|
+
}
|
|
523
|
+
/>
|
|
524
|
+
);
|
|
525
|
+
};
|
|
291
526
|
|
|
292
527
|
return (
|
|
293
528
|
<div
|
|
@@ -295,52 +530,10 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
295
530
|
className="mx-2 overflow-hidden rounded-t-lg bg-amber-300/[0.055] md:mx-3"
|
|
296
531
|
>
|
|
297
532
|
{showCollapsed ? (
|
|
298
|
-
// Desktop keeps a
|
|
299
|
-
<div className="hidden md:block">
|
|
300
|
-
{previewRows.map((message) => (
|
|
301
|
-
<PendingRow
|
|
302
|
-
cancelDisabled={Boolean(props.cancelPending)}
|
|
303
|
-
cancelError={Boolean(
|
|
304
|
-
props.cancelError &&
|
|
305
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
306
|
-
)}
|
|
307
|
-
cancelPending={Boolean(
|
|
308
|
-
props.cancelPending &&
|
|
309
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
310
|
-
)}
|
|
311
|
-
key={message.messageId}
|
|
312
|
-
message={message}
|
|
313
|
-
onCancel={
|
|
314
|
-
message.clientStatus === undefined
|
|
315
|
-
? props.onCancelMessage
|
|
316
|
-
: undefined
|
|
317
|
-
}
|
|
318
|
-
onRetry={props.onRetry}
|
|
319
|
-
/>
|
|
320
|
-
))}
|
|
321
|
-
</div>
|
|
533
|
+
// Desktop keeps a short preview; mobile collapses to the control only.
|
|
534
|
+
<div className="hidden md:block">{previewRows.map(renderRow)}</div>
|
|
322
535
|
) : (
|
|
323
|
-
visibleRows.map(
|
|
324
|
-
<PendingRow
|
|
325
|
-
cancelDisabled={Boolean(props.cancelPending)}
|
|
326
|
-
cancelError={Boolean(
|
|
327
|
-
props.cancelError &&
|
|
328
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
329
|
-
)}
|
|
330
|
-
cancelPending={Boolean(
|
|
331
|
-
props.cancelPending &&
|
|
332
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
333
|
-
)}
|
|
334
|
-
key={message.messageId}
|
|
335
|
-
message={message}
|
|
336
|
-
onCancel={
|
|
337
|
-
message.clientStatus === undefined
|
|
338
|
-
? props.onCancelMessage
|
|
339
|
-
: undefined
|
|
340
|
-
}
|
|
341
|
-
onRetry={props.onRetry}
|
|
342
|
-
/>
|
|
343
|
-
))
|
|
536
|
+
visibleRows.map(renderRow)
|
|
344
537
|
)}
|
|
345
538
|
{canCollapse ? (
|
|
346
539
|
<ExpandQueuedMessagesButton
|
|
@@ -12,10 +12,12 @@ shared controls and overlays.
|
|
|
12
12
|
`TranscriptMessageHeading` aligns authors, timestamps, and the context action.
|
|
13
13
|
The action stays visible on touch devices and while open. Mouse users see it
|
|
14
14
|
on message hover; keyboard users see it on focus.
|
|
15
|
-
- `
|
|
16
|
-
`flush` when the text already sits
|
|
17
|
-
|
|
18
|
-
The caller owns the
|
|
15
|
+
- `TranscriptSummaryTrigger` owns disclosure padding, hover, and keyboard
|
|
16
|
+
focus for a `ui/collapsible` trigger. Use `flush` when the text already sits
|
|
17
|
+
on the transcript edge. It extends the padded control around that text. Do
|
|
18
|
+
not repeat negative margins at call sites. The caller owns the `Collapsible`
|
|
19
|
+
and its content. Transcript search holds event and context rows open, and
|
|
20
|
+
shows activity group rows without the group.
|
|
19
21
|
- Activity rows (tools, reasoning, and grouped rail events) share one
|
|
20
22
|
layout: a 1rem icon column, an 8px gap, then the title and metadata.
|
|
21
23
|
Tool and reasoning rows use `ui/trace`. Transcript search forces them open.
|
|
@@ -111,9 +113,12 @@ The timezone toggle uses `useEventTime` to share a UTC/local preference through
|
|
|
111
113
|
`@uidotdev/usehooks` and browser local storage. It applies to event row clocks
|
|
112
114
|
and detail timestamps. Local mode follows the browser timezone. The button uses
|
|
113
115
|
a generic zone name because a log can span standard and daylight-saving time.
|
|
114
|
-
Detail timestamps use the zone name for each
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
Detail timestamps use a readable date to the second and the zone name for each
|
|
117
|
+
event date, in both modes. The header keeps them on one line beside the
|
|
118
|
+
actions. Their tooltip adds milliseconds, and the `time` element keeps the
|
|
119
|
+
original ISO value. The mode, not the timezone identifier, persists across
|
|
120
|
+
visits and tabs. This hook belongs to the client-rendered event viewer. Raw
|
|
121
|
+
JSON retains the original ISO values.
|
|
117
122
|
|
|
118
123
|
## Event details
|
|
119
124
|
|
|
@@ -122,10 +127,14 @@ places `ConversationEventDetails` beside the log in the page panel. Selecting
|
|
|
122
127
|
another row changes the event and resets its scroll position and disclosures.
|
|
123
128
|
Closing returns focus to that row. Switching Conversations removes the panel.
|
|
124
129
|
|
|
125
|
-
The previous and next controls select the adjacent visible row. They
|
|
130
|
+
The previous and next controls select the adjacent visible row. They form one
|
|
131
|
+
`ui/button-group` named "Event navigation". They follow
|
|
126
132
|
the event search, like Up and Down. They are necessary below 1280px, where the
|
|
127
133
|
sheet covers the log. At the ends of the list, the control is disabled but
|
|
128
|
-
keeps focus. The sheet title is the event type.
|
|
134
|
+
keeps focus. The sheet title is the event type. The header shows the event
|
|
135
|
+
type, then the sequence number and timestamp. The body groups fields in the
|
|
136
|
+
same `DetailsCard` that Conversation details use. Groups inside a card, such
|
|
137
|
+
as tool input and output, use a divider, not another card.
|
|
129
138
|
|
|
130
139
|
A linked event can be older than the loaded pages. While the event loads, the
|
|
131
140
|
panel stays open. Only its header shows placeholders, so it does not compete
|
|
@@ -134,6 +143,17 @@ the log toolbar, columns, and row skeletons in the event log view.
|
|
|
134
143
|
|
|
135
144
|
Standalone transcripts, such as a subagent drawer, keep selection in
|
|
136
145
|
`ConversationEventLog`. Their event details use a nested drawer on desktop and
|
|
137
|
-
`BottomSheet` on smaller screens.
|
|
146
|
+
`BottomSheet` on smaller screens. The nested drawer stacks in front of the
|
|
147
|
+
subagent drawer. The subagent drawer shows its event log without a body inset,
|
|
148
|
+
so rows meet the drawer edges and the toolbar sticks to the top. This keeps the main log free of modal behavior
|
|
138
149
|
while a nested transcript retains its parent drawer's focus rules. These event
|
|
139
150
|
details have the same previous and next controls.
|
|
151
|
+
|
|
152
|
+
`SubagentTranscriptDrawer` uses the wide `Drawer` at all widths. Below 768px
|
|
153
|
+
the drawer fills the screen, so the long transcript gets the most room. Its
|
|
154
|
+
header holds search, view, and copy actions. `BottomSheet` has no place for
|
|
155
|
+
them.
|
|
156
|
+
|
|
157
|
+
`ForkConversationButton` opens a `ui/dialog` form, not a drawer. The dialog
|
|
158
|
+
stays above event details in a docked panel or a sheet. Cancel returns focus
|
|
159
|
+
to the fork button. A pending fork blocks dismissal until the request ends.
|
|
@@ -96,15 +96,14 @@ export function ShareConversationButton(props: {
|
|
|
96
96
|
</TooltipContent>
|
|
97
97
|
</Tooltip>
|
|
98
98
|
) : props.layout === "menu" ? (
|
|
99
|
-
<
|
|
99
|
+
<Button
|
|
100
100
|
aria-label={label}
|
|
101
|
-
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-hover focus-visible:bg-dashboard-fill-hover focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
102
101
|
disabled={status === "sharing"}
|
|
103
102
|
onClick={() => void shareLink()}
|
|
104
|
-
|
|
103
|
+
size="menu"
|
|
105
104
|
>
|
|
106
105
|
{content}
|
|
107
|
-
</
|
|
106
|
+
</Button>
|
|
108
107
|
) : (
|
|
109
108
|
<Button
|
|
110
109
|
aria-label={label}
|