@sentry/junior-dashboard 0.266.0 → 0.267.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/ImageAttachment.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +4 -1
- package/src/client/components/README.md +11 -2
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/dialog.tsx +29 -35
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +30 -1
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/DetailsList.tsx +33 -0
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +310 -112
- package/src/client/conversations/README.md +30 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptTurnContextView.tsx +33 -18
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
|
@@ -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,
|
|
13
|
+
FileText,
|
|
14
|
+
Pencil,
|
|
12
15
|
SkipForward,
|
|
13
|
-
|
|
16
|
+
Trash2,
|
|
17
|
+
Zap,
|
|
14
18
|
type LucideIcon,
|
|
15
19
|
} from "lucide-react";
|
|
16
20
|
|
|
21
|
+
import { Button } from "../components/ui/button";
|
|
17
22
|
import {
|
|
18
23
|
Tooltip,
|
|
19
24
|
TooltipContent,
|
|
20
25
|
TooltipTrigger,
|
|
21
26
|
} from "../components/ui/tooltip";
|
|
27
|
+
import { ActorAvatar } from "../components/ActorAvatar";
|
|
28
|
+
import { ImageAttachment } from "../components/ImageAttachment";
|
|
29
|
+
import { ParticipantAvatar } from "../components/ParticipantAvatarStack";
|
|
22
30
|
import { Spinner } from "../components/ui/spinner";
|
|
23
|
-
import { actorLabel } from "../format";
|
|
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: Spinner, label: "Sending" };
|
|
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,65 +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
184
|
className="size-3.25"
|
|
96
185
|
strokeWidth={2.2}
|
|
97
186
|
/>
|
|
98
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}
|
|
99
208
|
{props.onCancel ? (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<X aria-hidden="true" size={14} strokeWidth={2.2} />
|
|
118
|
-
)}
|
|
119
|
-
</button>
|
|
120
|
-
}
|
|
121
|
-
/>
|
|
122
|
-
<TooltipContent>Remove queued message</TooltipContent>
|
|
123
|
-
</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
|
+
/>
|
|
124
226
|
) : null}
|
|
125
227
|
</TranscriptHeadingMeta>
|
|
126
228
|
);
|
|
127
229
|
}
|
|
128
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
|
+
|
|
129
293
|
function PendingRow(props: {
|
|
130
|
-
|
|
294
|
+
actionsDisabled: boolean;
|
|
131
295
|
cancelError: boolean;
|
|
132
296
|
cancelPending: boolean;
|
|
297
|
+
canEdit: boolean;
|
|
298
|
+
canSteer: boolean;
|
|
299
|
+
conversationId?: string;
|
|
300
|
+
editPending: boolean;
|
|
133
301
|
message: ConversationMailboxMessage;
|
|
134
302
|
onCancel?(message: ConversationMailboxMessage): void;
|
|
303
|
+
onEdit?(message: ConversationMailboxMessage): void;
|
|
135
304
|
onRetry?(message: ConversationMailboxMessage): void;
|
|
305
|
+
onSteer?(message: ConversationMailboxMessage): void;
|
|
306
|
+
steerPending: boolean;
|
|
136
307
|
}) {
|
|
137
|
-
const
|
|
138
|
-
...(props.message.images ?? []),
|
|
139
|
-
...(props.message.attachments ?? []),
|
|
140
|
-
]
|
|
141
|
-
.map((attachment) => attachment.filename)
|
|
142
|
-
.join(", ");
|
|
143
|
-
const text = [props.message.text, filenames].filter(Boolean).join(" · ");
|
|
308
|
+
const text = props.message.text ?? "";
|
|
144
309
|
const redacted = Boolean(props.message.redacted);
|
|
145
|
-
// Pending rows can come from web or Slack. Avoid rebuilding a full
|
|
146
|
-
// transcript projection just to label the stack above the composer.
|
|
147
|
-
const roleLabel = actorLabel(props.message.actorIdentity) ?? "User";
|
|
148
310
|
const showSlack = props.message.source === "slack";
|
|
149
311
|
const canRetry =
|
|
150
312
|
props.message.clientStatus === "failed" &&
|
|
@@ -152,36 +314,59 @@ function PendingRow(props: {
|
|
|
152
314
|
Boolean(props.onRetry);
|
|
153
315
|
|
|
154
316
|
return (
|
|
155
|
-
<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">
|
|
156
318
|
<TranscriptHeadingRow
|
|
319
|
+
className="items-center"
|
|
157
320
|
left={
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
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
|
+
</>
|
|
161
343
|
}
|
|
162
|
-
leftClassName="text-sm leading-snug text-dashboard-text"
|
|
344
|
+
leftClassName="flex-1 text-sm leading-snug text-dashboard-text"
|
|
163
345
|
right={
|
|
164
346
|
<PendingMetaIcons
|
|
165
|
-
|
|
347
|
+
actionsDisabled={props.actionsDisabled}
|
|
166
348
|
cancelError={props.cancelError}
|
|
167
349
|
cancelPending={props.cancelPending}
|
|
350
|
+
editPending={props.editPending}
|
|
168
351
|
message={props.message}
|
|
169
352
|
onCancel={
|
|
170
353
|
props.onCancel ? () => props.onCancel?.(props.message) : undefined
|
|
171
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
|
+
}
|
|
172
365
|
showSlack={showSlack}
|
|
366
|
+
steerPending={props.steerPending}
|
|
173
367
|
/>
|
|
174
368
|
}
|
|
175
369
|
/>
|
|
176
|
-
{redacted ? (
|
|
177
|
-
<p className="m-0 font-sans text-sm leading-snug text-dashboard-text-muted">
|
|
178
|
-
[redacted]
|
|
179
|
-
</p>
|
|
180
|
-
) : (
|
|
181
|
-
<p className="m-0 line-clamp-3 font-sans text-sm leading-snug text-dashboard-text-subtle">
|
|
182
|
-
{text}
|
|
183
|
-
</p>
|
|
184
|
-
)}
|
|
185
370
|
{canRetry ? (
|
|
186
371
|
<div className="flex min-w-0 items-center gap-2">
|
|
187
372
|
<p className="m-0 font-sans text-xs text-red-300/80">
|
|
@@ -198,7 +383,7 @@ function PendingRow(props: {
|
|
|
198
383
|
) : null}
|
|
199
384
|
{props.cancelError ? (
|
|
200
385
|
<p className="m-0 font-sans text-xs text-amber-100/75">
|
|
201
|
-
Could not
|
|
386
|
+
Could not delete. Try again.
|
|
202
387
|
</p>
|
|
203
388
|
) : null}
|
|
204
389
|
</article>
|
|
@@ -242,6 +427,12 @@ function ExpandQueuedMessagesButton(props: {
|
|
|
242
427
|
);
|
|
243
428
|
}
|
|
244
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
|
+
|
|
245
436
|
/** Render accepted mailbox rows as a compact stack attached above the composer. */
|
|
246
437
|
export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
247
438
|
cancelError?: boolean;
|
|
@@ -249,11 +440,23 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
249
440
|
cancelTargetInboundMessageId?: string;
|
|
250
441
|
/** Message ids already present in committed history. */
|
|
251
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;
|
|
252
447
|
messages: readonly ConversationMailboxMessage[];
|
|
253
448
|
onCancelMessage?: (message: ConversationMailboxMessage) => void;
|
|
449
|
+
/** Take the viewer's own queued web message back into the composer. */
|
|
450
|
+
onEditMessage?: (message: ConversationMailboxMessage) => void;
|
|
254
451
|
/** Fires after expand/collapse changes the stack height above the composer. */
|
|
255
452
|
onLayoutChange?: () => void;
|
|
256
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;
|
|
257
460
|
}): ReactNode {
|
|
258
461
|
const [expanded, setExpanded] = useState(false);
|
|
259
462
|
const onLayoutChangeRef = useRef(props.onLayoutChange);
|
|
@@ -283,6 +486,43 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
283
486
|
const visibleRows = showCollapsed ? previewRows : rows;
|
|
284
487
|
const hiddenCount = Math.max(0, rows.length - COLLAPSED_PENDING_ROW_COUNT);
|
|
285
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
|
+
};
|
|
286
526
|
|
|
287
527
|
return (
|
|
288
528
|
<div
|
|
@@ -290,52 +530,10 @@ export const PendingMailboxStack = memo(function PendingMailboxStack(props: {
|
|
|
290
530
|
className="mx-2 overflow-hidden rounded-t-lg bg-amber-300/[0.055] md:mx-3"
|
|
291
531
|
>
|
|
292
532
|
{showCollapsed ? (
|
|
293
|
-
// Desktop keeps a
|
|
294
|
-
<div className="hidden md:block">
|
|
295
|
-
{previewRows.map((message) => (
|
|
296
|
-
<PendingRow
|
|
297
|
-
cancelDisabled={Boolean(props.cancelPending)}
|
|
298
|
-
cancelError={Boolean(
|
|
299
|
-
props.cancelError &&
|
|
300
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
301
|
-
)}
|
|
302
|
-
cancelPending={Boolean(
|
|
303
|
-
props.cancelPending &&
|
|
304
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
305
|
-
)}
|
|
306
|
-
key={message.messageId}
|
|
307
|
-
message={message}
|
|
308
|
-
onCancel={
|
|
309
|
-
message.clientStatus === undefined
|
|
310
|
-
? props.onCancelMessage
|
|
311
|
-
: undefined
|
|
312
|
-
}
|
|
313
|
-
onRetry={props.onRetry}
|
|
314
|
-
/>
|
|
315
|
-
))}
|
|
316
|
-
</div>
|
|
533
|
+
// Desktop keeps a short preview; mobile collapses to the control only.
|
|
534
|
+
<div className="hidden md:block">{previewRows.map(renderRow)}</div>
|
|
317
535
|
) : (
|
|
318
|
-
visibleRows.map(
|
|
319
|
-
<PendingRow
|
|
320
|
-
cancelDisabled={Boolean(props.cancelPending)}
|
|
321
|
-
cancelError={Boolean(
|
|
322
|
-
props.cancelError &&
|
|
323
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
324
|
-
)}
|
|
325
|
-
cancelPending={Boolean(
|
|
326
|
-
props.cancelPending &&
|
|
327
|
-
props.cancelTargetInboundMessageId === message.inboundMessageId,
|
|
328
|
-
)}
|
|
329
|
-
key={message.messageId}
|
|
330
|
-
message={message}
|
|
331
|
-
onCancel={
|
|
332
|
-
message.clientStatus === undefined
|
|
333
|
-
? props.onCancelMessage
|
|
334
|
-
: undefined
|
|
335
|
-
}
|
|
336
|
-
onRetry={props.onRetry}
|
|
337
|
-
/>
|
|
338
|
-
))
|
|
536
|
+
visibleRows.map(renderRow)
|
|
339
537
|
)}
|
|
340
538
|
{canCollapse ? (
|
|
341
539
|
<ExpandQueuedMessagesButton
|
|
@@ -12,13 +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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
event and context rows open.
|
|
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.
|
|
22
21
|
- Activity rows (tools, reasoning, and grouped rail events) share one
|
|
23
22
|
layout: a 1rem icon column, an 8px gap, then the title and metadata.
|
|
24
23
|
Tool and reasoning rows use `ui/trace`. Transcript search forces them open.
|
|
@@ -114,9 +113,12 @@ The timezone toggle uses `useEventTime` to share a UTC/local preference through
|
|
|
114
113
|
`@uidotdev/usehooks` and browser local storage. It applies to event row clocks
|
|
115
114
|
and detail timestamps. Local mode follows the browser timezone. The button uses
|
|
116
115
|
a generic zone name because a log can span standard and daylight-saving time.
|
|
117
|
-
Detail timestamps use the zone name for each
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
120
122
|
|
|
121
123
|
## Event details
|
|
122
124
|
|
|
@@ -125,10 +127,14 @@ places `ConversationEventDetails` beside the log in the page panel. Selecting
|
|
|
125
127
|
another row changes the event and resets its scroll position and disclosures.
|
|
126
128
|
Closing returns focus to that row. Switching Conversations removes the panel.
|
|
127
129
|
|
|
128
|
-
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
|
|
129
132
|
the event search, like Up and Down. They are necessary below 1280px, where the
|
|
130
133
|
sheet covers the log. At the ends of the list, the control is disabled but
|
|
131
|
-
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.
|
|
132
138
|
|
|
133
139
|
A linked event can be older than the loaded pages. While the event loads, the
|
|
134
140
|
panel stays open. Only its header shows placeholders, so it does not compete
|
|
@@ -137,6 +143,17 @@ the log toolbar, columns, and row skeletons in the event log view.
|
|
|
137
143
|
|
|
138
144
|
Standalone transcripts, such as a subagent drawer, keep selection in
|
|
139
145
|
`ConversationEventLog`. Their event details use a nested drawer on desktop and
|
|
140
|
-
`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
|
|
141
149
|
while a nested transcript retains its parent drawer's focus rules. These event
|
|
142
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.
|