@sentry/junior-dashboard 0.266.0 → 0.268.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +85 -12
- package/dist/client.js +94 -64
- package/dist/index.js +85 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +55 -53
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/ImageAttachment.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +6 -2
- package/src/client/components/README.md +15 -2
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +11 -1
- package/src/client/components/ui/dialog.tsx +29 -35
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +30 -1
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +160 -2
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- package/src/client/conversations/DetailsList.tsx +33 -0
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +310 -112
- package/src/client/conversations/README.md +30 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +27 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +10 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +174 -161
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +39 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/turnContext.ts +15 -0
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +31 -8
- package/src/client/pages/SettingsPage.tsx +103 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/pages/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +62 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +277 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +115 -0
- package/src/client/review-desk/queries.ts +164 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +54 -2
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
normalizeUserInputAnswer,
|
|
4
|
+
type ReviewItemActionBody,
|
|
5
|
+
type ReviewItemPayload,
|
|
6
|
+
} from "@sentry/junior/api/schema";
|
|
7
|
+
|
|
8
|
+
import { InlineError } from "../components/InlineError";
|
|
9
|
+
import { UserInputForm } from "../components/UserInputForm";
|
|
10
|
+
import { Button } from "../components/ui/button";
|
|
11
|
+
import { TranscriptMarkdown } from "../conversations/TranscriptMarkdown";
|
|
12
|
+
import { formatRelativeTime } from "../format";
|
|
13
|
+
import { DashboardApiError } from "../http";
|
|
14
|
+
import { useReviewItemAction } from "./queries";
|
|
15
|
+
|
|
16
|
+
function personLabel(person: { displayName?: string; email: string }) {
|
|
17
|
+
return person.displayName ?? person.email;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function actionError(error: unknown): string {
|
|
21
|
+
if (error instanceof DashboardApiError && error.apiError) {
|
|
22
|
+
return error.apiError;
|
|
23
|
+
}
|
|
24
|
+
return "Could not save this action. Try again.";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Show the submitted answer as one line for each answered field. */
|
|
28
|
+
function AnswerSummary(props: {
|
|
29
|
+
answer: NonNullable<ReviewItemPayload["answer"]>;
|
|
30
|
+
request: NonNullable<ReviewItemPayload["request"]>;
|
|
31
|
+
}) {
|
|
32
|
+
const answer = normalizeUserInputAnswer(props.request, props.answer);
|
|
33
|
+
const rows = props.request.fields.flatMap((field) => {
|
|
34
|
+
const values = answer.values[field.id];
|
|
35
|
+
return values?.length
|
|
36
|
+
? [{ label: field.label, value: values.join(", ") }]
|
|
37
|
+
: [];
|
|
38
|
+
});
|
|
39
|
+
if (answer.other) rows.push({ label: "Other", value: answer.other });
|
|
40
|
+
return (
|
|
41
|
+
<dl className="m-0 grid gap-2 rounded-md bg-dashboard-fill-soft p-3">
|
|
42
|
+
{rows.map((row) => (
|
|
43
|
+
<div className="grid gap-0.5" key={row.label}>
|
|
44
|
+
<dt className="text-xs font-medium text-dashboard-text-muted">
|
|
45
|
+
{row.label}
|
|
46
|
+
</dt>
|
|
47
|
+
<dd className="m-0 whitespace-pre-wrap break-words">{row.value}</dd>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</dl>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Show who closed an item, when, and the submitted answer. */
|
|
55
|
+
function ClosedSummary(props: { item: ReviewItemPayload }) {
|
|
56
|
+
const { item } = props;
|
|
57
|
+
const who = item.closedBy ? personLabel(item.closedBy) : "An agent";
|
|
58
|
+
const what = item.status === "dismissed" ? "dismissed" : "closed";
|
|
59
|
+
return (
|
|
60
|
+
<div className="grid gap-2 text-sm">
|
|
61
|
+
<p className="m-0 text-dashboard-text-muted">
|
|
62
|
+
{who} {what} this {formatRelativeTime(item.closedAt)}.
|
|
63
|
+
</p>
|
|
64
|
+
{item.closeReason ? (
|
|
65
|
+
<p className="m-0 text-dashboard-text-muted">{item.closeReason}</p>
|
|
66
|
+
) : null}
|
|
67
|
+
{item.request && item.answer ? (
|
|
68
|
+
<AnswerSummary answer={item.answer} request={item.request} />
|
|
69
|
+
) : null}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Show the actions that the viewer can take on one Review item, or how it
|
|
76
|
+
* closed. The review desk, the transcript card, and the side panel use it.
|
|
77
|
+
*/
|
|
78
|
+
export function ReviewItemActions(props: { item: ReviewItemPayload }) {
|
|
79
|
+
const { item } = props;
|
|
80
|
+
const action = useReviewItemAction();
|
|
81
|
+
const [error, setError] = useState<string>();
|
|
82
|
+
|
|
83
|
+
if (item.status !== "open") return <ClosedSummary item={item} />;
|
|
84
|
+
if (!item.canAct) {
|
|
85
|
+
return (
|
|
86
|
+
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
87
|
+
Only an assignee can act on this item.
|
|
88
|
+
</p>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function run(body: ReviewItemActionBody) {
|
|
93
|
+
setError(undefined);
|
|
94
|
+
try {
|
|
95
|
+
await action.mutateAsync({ body, id: item.id });
|
|
96
|
+
} catch (failure) {
|
|
97
|
+
setError(actionError(failure));
|
|
98
|
+
throw failure;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Take and Dismiss are quiet, so the answer or Done stays the main action.
|
|
103
|
+
const secondaryActions = (
|
|
104
|
+
<>
|
|
105
|
+
{item.canTake ? (
|
|
106
|
+
<Button
|
|
107
|
+
disabled={action.isPending}
|
|
108
|
+
onClick={() => void run({ action: "take" }).catch(() => {})}
|
|
109
|
+
variant="quiet"
|
|
110
|
+
>
|
|
111
|
+
Take
|
|
112
|
+
</Button>
|
|
113
|
+
) : null}
|
|
114
|
+
<Button
|
|
115
|
+
disabled={action.isPending}
|
|
116
|
+
onClick={() => void run({ action: "dismiss" }).catch(() => {})}
|
|
117
|
+
variant="quiet"
|
|
118
|
+
>
|
|
119
|
+
Dismiss
|
|
120
|
+
</Button>
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="grid gap-3">
|
|
126
|
+
{item.kind === "action" && item.request ? (
|
|
127
|
+
<div className="text-sm">
|
|
128
|
+
<div className="font-medium">
|
|
129
|
+
<TranscriptMarkdown text={item.request.question} />
|
|
130
|
+
</div>
|
|
131
|
+
<UserInputForm
|
|
132
|
+
onSubmit={(answer) =>
|
|
133
|
+
run({
|
|
134
|
+
action: "submit",
|
|
135
|
+
answer: {
|
|
136
|
+
values: answer.values,
|
|
137
|
+
...(answer.other ? { other: answer.other } : undefined),
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
request={item.request}
|
|
142
|
+
requestId={item.id}
|
|
143
|
+
secondaryActions={secondaryActions}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
148
|
+
{item.kind === "notice" ? (
|
|
149
|
+
<Button
|
|
150
|
+
disabled={action.isPending}
|
|
151
|
+
onClick={() => void run({ action: "complete" }).catch(() => {})}
|
|
152
|
+
variant="primary"
|
|
153
|
+
>
|
|
154
|
+
Done
|
|
155
|
+
</Button>
|
|
156
|
+
) : null}
|
|
157
|
+
<div className="flex flex-wrap gap-1 sm:ml-auto">
|
|
158
|
+
{secondaryActions}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
{error ? <InlineError>{error}</InlineError> : null}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
3
|
+
|
|
4
|
+
import { StatusChip } from "../components/StatusChip";
|
|
5
|
+
import { TranscriptMarkdown } from "../conversations/TranscriptMarkdown";
|
|
6
|
+
import { ReviewItemActions } from "./ReviewItemActions";
|
|
7
|
+
import { useReviewItemData } from "./queries";
|
|
8
|
+
import { reviewItemPath, reviewItemStatusLabel } from "./reviewItemFormat";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Show one Review item as a card: its title, status, description, and the
|
|
12
|
+
* viewer's actions, with a link to the review desk.
|
|
13
|
+
*/
|
|
14
|
+
export function ReviewItemCard(props: { item: ReviewItemPayload }) {
|
|
15
|
+
const { item } = props;
|
|
16
|
+
return (
|
|
17
|
+
<section
|
|
18
|
+
aria-label={`Review item: ${item.title}`}
|
|
19
|
+
className="grid gap-3 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] p-3 font-sans text-sm text-dashboard-text"
|
|
20
|
+
>
|
|
21
|
+
<header className="flex min-w-0 items-baseline justify-between gap-3">
|
|
22
|
+
<span className="min-w-0 truncate font-medium">{item.title}</span>
|
|
23
|
+
<StatusChip
|
|
24
|
+
className="shrink-0"
|
|
25
|
+
size="compact"
|
|
26
|
+
tone={item.status === "open" ? "accent" : "neutral"}
|
|
27
|
+
>
|
|
28
|
+
{reviewItemStatusLabel(item)}
|
|
29
|
+
</StatusChip>
|
|
30
|
+
</header>
|
|
31
|
+
<TranscriptMarkdown text={item.body} />
|
|
32
|
+
<ReviewItemActions item={item} />
|
|
33
|
+
<Link className="w-fit text-xs" to={reviewItemPath(item.id)}>
|
|
34
|
+
Open on the review desk
|
|
35
|
+
</Link>
|
|
36
|
+
</section>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Show the card of the Review item that a tool call added. It shows nothing
|
|
42
|
+
* when the viewer cannot see the item.
|
|
43
|
+
*/
|
|
44
|
+
export function TranscriptReviewItemCard(props: { itemId: string }) {
|
|
45
|
+
const item = useReviewItemData(props.itemId).data;
|
|
46
|
+
return item ? <ReviewItemCard item={item} /> : null;
|
|
47
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
4
|
+
|
|
5
|
+
import { StatusChip } from "../components/StatusChip";
|
|
6
|
+
import { TranscriptMarkdown } from "../conversations/TranscriptMarkdown";
|
|
7
|
+
import { conversationPath } from "../conversations/conversationRoutes";
|
|
8
|
+
import { automationPath, formatRelativeTime } from "../format";
|
|
9
|
+
import { ReviewItemActions } from "./ReviewItemActions";
|
|
10
|
+
import { useLinkedConversationTitles } from "./queries";
|
|
11
|
+
import { assigneeLabel, reviewItemStatusLabel } from "./reviewItemFormat";
|
|
12
|
+
|
|
13
|
+
function Fact(props: { children: ReactNode; label: string }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="grid content-start gap-0.5">
|
|
16
|
+
<dt className="text-xs font-medium text-dashboard-text-muted">
|
|
17
|
+
{props.label}
|
|
18
|
+
</dt>
|
|
19
|
+
<dd className="m-0 min-w-0 break-words text-sm">{props.children}</dd>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Show one Review item with its facts and the viewer's actions. */
|
|
25
|
+
export function ReviewItemDetail(props: { item: ReviewItemPayload }) {
|
|
26
|
+
const { item } = props;
|
|
27
|
+
const subject = item.subject;
|
|
28
|
+
const conversationTitles = useLinkedConversationTitles(item.conversationIds);
|
|
29
|
+
return (
|
|
30
|
+
<article className="grid min-w-0 content-start gap-5">
|
|
31
|
+
<header className="grid gap-1.5">
|
|
32
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
33
|
+
<h2 className="m-0 min-w-0 break-words font-display text-xl font-medium leading-snug">
|
|
34
|
+
{item.title}
|
|
35
|
+
</h2>
|
|
36
|
+
<StatusChip
|
|
37
|
+
className="mt-1 shrink-0"
|
|
38
|
+
size="compact"
|
|
39
|
+
tone={item.status === "open" ? "accent" : "neutral"}
|
|
40
|
+
>
|
|
41
|
+
{reviewItemStatusLabel(item)}
|
|
42
|
+
</StatusChip>
|
|
43
|
+
</div>
|
|
44
|
+
<p className="m-0 text-xs text-dashboard-text-muted">
|
|
45
|
+
Added {formatRelativeTime(item.createdAt)}
|
|
46
|
+
{item.visibility === "private" ? " · Private" : null}
|
|
47
|
+
</p>
|
|
48
|
+
</header>
|
|
49
|
+
<TranscriptMarkdown text={item.body} />
|
|
50
|
+
<section
|
|
51
|
+
aria-label={item.status === "open" ? "Actions" : "Outcome"}
|
|
52
|
+
className="border-t border-dashboard-border-subtle pt-5"
|
|
53
|
+
>
|
|
54
|
+
<ReviewItemActions item={item} />
|
|
55
|
+
</section>
|
|
56
|
+
<dl className="m-0 grid gap-4 border-t border-dashboard-border-subtle pt-5 sm:grid-cols-2">
|
|
57
|
+
<Fact label="Assignees">
|
|
58
|
+
{item.assignees.length > 0 || item.assignedToEveryone ? (
|
|
59
|
+
assigneeLabel(item)
|
|
60
|
+
) : (
|
|
61
|
+
<StatusChip size="compact" tone="warning">
|
|
62
|
+
Unassigned
|
|
63
|
+
</StatusChip>
|
|
64
|
+
)}
|
|
65
|
+
</Fact>
|
|
66
|
+
{subject ? (
|
|
67
|
+
<Fact label="Subject">
|
|
68
|
+
{subject.url ? (
|
|
69
|
+
<a href={subject.url} rel="noreferrer" target="_blank">
|
|
70
|
+
{subject.title ?? subject.url}
|
|
71
|
+
</a>
|
|
72
|
+
) : (
|
|
73
|
+
(subject.title ?? subject.id)
|
|
74
|
+
)}
|
|
75
|
+
</Fact>
|
|
76
|
+
) : null}
|
|
77
|
+
<Fact label="Added by">
|
|
78
|
+
<Link to={conversationPath(item.addedByConversationId)}>
|
|
79
|
+
Conversation
|
|
80
|
+
</Link>
|
|
81
|
+
{item.automationId ? (
|
|
82
|
+
<>
|
|
83
|
+
{" · "}
|
|
84
|
+
<Link to={automationPath(item.automationId)}>Automation</Link>
|
|
85
|
+
</>
|
|
86
|
+
) : null}
|
|
87
|
+
</Fact>
|
|
88
|
+
{item.conversationIds.length > 0 ? (
|
|
89
|
+
<Fact label="Linked Conversations">
|
|
90
|
+
<ul className="m-0 grid list-none gap-0.5 p-0">
|
|
91
|
+
{item.conversationIds.map((conversationId, index) => {
|
|
92
|
+
const title = conversationTitles[index];
|
|
93
|
+
return (
|
|
94
|
+
<li
|
|
95
|
+
className={
|
|
96
|
+
title ? "truncate" : "truncate font-mono text-xs"
|
|
97
|
+
}
|
|
98
|
+
key={conversationId}
|
|
99
|
+
>
|
|
100
|
+
<Link
|
|
101
|
+
title={conversationId}
|
|
102
|
+
to={conversationPath(conversationId)}
|
|
103
|
+
>
|
|
104
|
+
{title ?? conversationId}
|
|
105
|
+
</Link>
|
|
106
|
+
</li>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</ul>
|
|
110
|
+
</Fact>
|
|
111
|
+
) : null}
|
|
112
|
+
</dl>
|
|
113
|
+
</article>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
keepPreviousData,
|
|
4
|
+
useMutation,
|
|
5
|
+
useQueries,
|
|
6
|
+
useQuery,
|
|
7
|
+
useQueryClient,
|
|
8
|
+
} from "@tanstack/react-query";
|
|
9
|
+
import {
|
|
10
|
+
automationListSchema,
|
|
11
|
+
conversationDetailReportSchema,
|
|
12
|
+
reviewItemCountSchema,
|
|
13
|
+
reviewItemListSchema,
|
|
14
|
+
reviewItemSchema,
|
|
15
|
+
reviewItemWaitingConversationsSchema,
|
|
16
|
+
type ReviewItemActionBody,
|
|
17
|
+
} from "@sentry/junior/api/schema";
|
|
18
|
+
|
|
19
|
+
import { conversationDetailTitle } from "../format";
|
|
20
|
+
import { fetchDashboardJson, post } from "../http";
|
|
21
|
+
|
|
22
|
+
/** Review desk views. Each shows open items, or archived ones. */
|
|
23
|
+
export const REVIEW_DESK_VIEWS = ["mine", "everyone", "all"] as const;
|
|
24
|
+
export type ReviewDeskView = (typeof REVIEW_DESK_VIEWS)[number];
|
|
25
|
+
|
|
26
|
+
const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
|
|
27
|
+
const COUNT_REFRESH_MS = 60_000;
|
|
28
|
+
|
|
29
|
+
/** List the Review items that the viewer can see in one view. */
|
|
30
|
+
export function useReviewItemsData(filters: {
|
|
31
|
+
archived: boolean;
|
|
32
|
+
conversationId?: string;
|
|
33
|
+
q?: string;
|
|
34
|
+
view: ReviewDeskView;
|
|
35
|
+
}) {
|
|
36
|
+
const params = new URLSearchParams({ view: filters.view });
|
|
37
|
+
if (filters.archived) params.set("archived", "true");
|
|
38
|
+
if (filters.conversationId) {
|
|
39
|
+
params.set("conversationId", filters.conversationId);
|
|
40
|
+
}
|
|
41
|
+
if (filters.q) params.set("q", filters.q);
|
|
42
|
+
const search = params.toString();
|
|
43
|
+
return useQuery({
|
|
44
|
+
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "list", search],
|
|
45
|
+
queryFn: ({ signal }) =>
|
|
46
|
+
fetchDashboardJson(
|
|
47
|
+
reviewItemListSchema,
|
|
48
|
+
`/api/review-items?${search}`,
|
|
49
|
+
signal,
|
|
50
|
+
),
|
|
51
|
+
placeholderData: keepPreviousData,
|
|
52
|
+
retry: false,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read one Review item, for a link to an item outside the current list. */
|
|
57
|
+
export function useReviewItemData(id: string | undefined) {
|
|
58
|
+
return useQuery({
|
|
59
|
+
enabled: Boolean(id),
|
|
60
|
+
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "detail", id],
|
|
61
|
+
queryFn: ({ signal }) =>
|
|
62
|
+
fetchDashboardJson(
|
|
63
|
+
reviewItemSchema,
|
|
64
|
+
`/api/review-items/${encodeURIComponent(id!)}`,
|
|
65
|
+
signal,
|
|
66
|
+
),
|
|
67
|
+
retry: false,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the titles of the Conversations linked to an item. Each read asks for
|
|
73
|
+
* one event only. A Conversation that the viewer cannot read has no title.
|
|
74
|
+
*/
|
|
75
|
+
export function useLinkedConversationTitles(
|
|
76
|
+
conversationIds: readonly string[],
|
|
77
|
+
): (string | undefined)[] {
|
|
78
|
+
return useQueries({
|
|
79
|
+
queries: conversationIds.map((conversationId) => ({
|
|
80
|
+
queryKey: ["dashboard", "conversation-title", conversationId],
|
|
81
|
+
queryFn: async ({ signal }: { signal: AbortSignal }) =>
|
|
82
|
+
conversationDetailTitle(
|
|
83
|
+
await fetchDashboardJson(
|
|
84
|
+
conversationDetailReportSchema,
|
|
85
|
+
`/api/conversations/${encodeURIComponent(conversationId)}?limit=1`,
|
|
86
|
+
signal,
|
|
87
|
+
),
|
|
88
|
+
) ?? null,
|
|
89
|
+
retry: false,
|
|
90
|
+
staleTime: 5 * 60_000,
|
|
91
|
+
})),
|
|
92
|
+
}).map((query) => query.data ?? undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Count the open Review items assigned to the viewer, for the nav. */
|
|
96
|
+
export function useReviewItemCountData(enabled: boolean) {
|
|
97
|
+
return useQuery({
|
|
98
|
+
enabled,
|
|
99
|
+
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "count"],
|
|
100
|
+
queryFn: ({ signal }) =>
|
|
101
|
+
fetchDashboardJson(
|
|
102
|
+
reviewItemCountSchema,
|
|
103
|
+
"/api/review-items/count",
|
|
104
|
+
signal,
|
|
105
|
+
),
|
|
106
|
+
refetchInterval: COUNT_REFRESH_MS,
|
|
107
|
+
retry: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List the viewer's blocked Automations. They show on the review desk, but
|
|
113
|
+
* their state stays derived from Automation status.
|
|
114
|
+
*/
|
|
115
|
+
export function useBlockedAutomationsData(enabled: boolean) {
|
|
116
|
+
return useQuery({
|
|
117
|
+
enabled,
|
|
118
|
+
queryKey: ["dashboard", "automations", "scope=mine&state=blocked"],
|
|
119
|
+
queryFn: ({ signal }) =>
|
|
120
|
+
fetchDashboardJson(
|
|
121
|
+
automationListSchema,
|
|
122
|
+
"/api/automations?scope=mine&state=blocked&pageSize=100",
|
|
123
|
+
signal,
|
|
124
|
+
),
|
|
125
|
+
retry: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Take, submit, complete, or dismiss one Review item. */
|
|
130
|
+
export function useReviewItemAction() {
|
|
131
|
+
const queryClient = useQueryClient();
|
|
132
|
+
return useMutation({
|
|
133
|
+
mutationFn: (args: { body: ReviewItemActionBody; id: string }) =>
|
|
134
|
+
post(
|
|
135
|
+
reviewItemSchema,
|
|
136
|
+
`/api/review-items/${encodeURIComponent(args.id)}/actions`,
|
|
137
|
+
args.body,
|
|
138
|
+
),
|
|
139
|
+
onSettled: () =>
|
|
140
|
+
queryClient.invalidateQueries({ queryKey: REVIEW_ITEMS_QUERY_KEY }),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const NO_CONVERSATIONS: ReadonlySet<string> = new Set();
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Return the Conversations that wait on the viewer. Today only the review
|
|
148
|
+
* desk sets this: an open linked item assigned to the viewer or Everyone.
|
|
149
|
+
*/
|
|
150
|
+
export function useWaitingConversationIds(): ReadonlySet<string> {
|
|
151
|
+
const query = useQuery({
|
|
152
|
+
queryKey: [...REVIEW_ITEMS_QUERY_KEY, "waiting-conversations"],
|
|
153
|
+
queryFn: ({ signal }) =>
|
|
154
|
+
fetchDashboardJson(
|
|
155
|
+
reviewItemWaitingConversationsSchema,
|
|
156
|
+
"/api/review-items/waiting-conversations",
|
|
157
|
+
signal,
|
|
158
|
+
),
|
|
159
|
+
refetchInterval: COUNT_REFRESH_MS,
|
|
160
|
+
retry: false,
|
|
161
|
+
});
|
|
162
|
+
const ids = query.data?.conversationIds;
|
|
163
|
+
return useMemo(() => (ids ? new Set(ids) : NO_CONVERSATIONS), [ids]);
|
|
164
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
3
|
+
|
|
4
|
+
/** Return the review desk route that selects one item. */
|
|
5
|
+
export function reviewItemPath(id: string): string {
|
|
6
|
+
return `/review?item=${encodeURIComponent(id)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Name the state of an item: what it needs while open, or how it closed. */
|
|
10
|
+
export function reviewItemStatusLabel(item: ReviewItemPayload): string {
|
|
11
|
+
if (item.status === "open") {
|
|
12
|
+
return item.kind === "action" ? "Needs an answer" : "Notice";
|
|
13
|
+
}
|
|
14
|
+
return item.status === "done" ? "Done" : "Dismissed";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Name the assignees of an item in one short line. */
|
|
18
|
+
export function assigneeLabel(item: ReviewItemPayload): string {
|
|
19
|
+
const names = item.assignees.map(
|
|
20
|
+
(person) => person.displayName ?? person.email,
|
|
21
|
+
);
|
|
22
|
+
if (item.assignedToEveryone) names.unshift("Everyone");
|
|
23
|
+
return names.length > 0 ? names.join(", ") : "Unassigned";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ADD_REVIEW_ITEM_TOOL_NAME = "addReviewItem";
|
|
27
|
+
const executeToolInputSchema = z.object({ tool_name: z.string() });
|
|
28
|
+
const addedItemSchema = z.object({ item: z.object({ id: z.string().min(1) }) });
|
|
29
|
+
|
|
30
|
+
/** Read the item id from a tool result, which can be JSON text. */
|
|
31
|
+
function addedItemId(output: unknown): string | undefined {
|
|
32
|
+
let value = output;
|
|
33
|
+
if (typeof output === "string") {
|
|
34
|
+
try {
|
|
35
|
+
value = JSON.parse(output);
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return addedItemSchema.safeParse(value).data?.item.id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return the id of the Review item that one completed tool call added. The
|
|
45
|
+
* agent calls `addReviewItem` directly or through `executeTool`. Returns
|
|
46
|
+
* undefined when the transcript hides the tool result.
|
|
47
|
+
*/
|
|
48
|
+
export function reviewItemIdFromToolCall(call: {
|
|
49
|
+
input?: unknown;
|
|
50
|
+
name: string;
|
|
51
|
+
output?: unknown;
|
|
52
|
+
status: string;
|
|
53
|
+
}): string | undefined {
|
|
54
|
+
if (call.status !== "completed") return undefined;
|
|
55
|
+
const toolName =
|
|
56
|
+
call.name === "executeTool"
|
|
57
|
+
? executeToolInputSchema.safeParse(call.input).data?.tool_name
|
|
58
|
+
: call.name;
|
|
59
|
+
if (toolName !== ADD_REVIEW_ITEM_TOOL_NAME) return undefined;
|
|
60
|
+
return addedItemId(call.output);
|
|
61
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ObjectType } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { BundledLanguage } from "shiki/bundle/web";
|
|
3
3
|
import type {
|
|
4
|
+
InputImage,
|
|
4
5
|
MessageAttachment,
|
|
5
6
|
MessageCard,
|
|
6
7
|
PluginOperationalReportFeed,
|
|
@@ -151,6 +152,8 @@ export type TranscriptViewMessage = {
|
|
|
151
152
|
failureReason?: ConversationTurnFailureReason;
|
|
152
153
|
sentryEventUrl?: string;
|
|
153
154
|
attachments?: MessageAttachment[];
|
|
155
|
+
/** Local images of a pending send that the server has not stored yet. */
|
|
156
|
+
images?: InputImage[];
|
|
154
157
|
parts: TranscriptViewPart[];
|
|
155
158
|
role: "assistant" | "system" | "tool" | "user";
|
|
156
159
|
source?: "slack" | "web";
|
|
@@ -572,7 +572,16 @@ function activeConversation(nowMs: number): ConversationDetailReport {
|
|
|
572
572
|
type: "message",
|
|
573
573
|
messageId: "active-user-followup",
|
|
574
574
|
role: "user",
|
|
575
|
-
|
|
575
|
+
// Quoted reply keeps blockquote and nested-list rendering in visual QA.
|
|
576
|
+
text: [
|
|
577
|
+
"> error volume is flat, so this looks like slow path not hard fail",
|
|
578
|
+
"",
|
|
579
|
+
"Agreed. Compare the pre/post deploy spans next:",
|
|
580
|
+
"1. `checkout.complete` p95",
|
|
581
|
+
" - stable hosts",
|
|
582
|
+
" - canary hosts",
|
|
583
|
+
"2. ~~error rate~~ serializer span time",
|
|
584
|
+
].join("\n"),
|
|
576
585
|
}),
|
|
577
586
|
reportEvent(8, iso(Date.parse(startedAt), 42_000), {
|
|
578
587
|
type: "turn_lifecycle",
|
|
@@ -593,6 +602,16 @@ function activeConversation(nowMs: number): ConversationDetailReport {
|
|
|
593
602
|
},
|
|
594
603
|
],
|
|
595
604
|
}),
|
|
605
|
+
reportEvent(10, iso(Date.parse(startedAt), 46_000), {
|
|
606
|
+
type: "turn_context",
|
|
607
|
+
turnId: "active-turn",
|
|
608
|
+
pluginName: "junior",
|
|
609
|
+
kind: "user_customization",
|
|
610
|
+
version: 1,
|
|
611
|
+
content: {
|
|
612
|
+
text: "Keep replies short and simple.\nLead with the answer, then the evidence.",
|
|
613
|
+
},
|
|
614
|
+
}),
|
|
596
615
|
],
|
|
597
616
|
});
|
|
598
617
|
}
|
|
@@ -1451,7 +1470,7 @@ function eventAutomationRunConversation(
|
|
|
1451
1470
|
"This is a task, not a message from a person.",
|
|
1452
1471
|
"",
|
|
1453
1472
|
"About: Issue · ACME-42",
|
|
1454
|
-
'Created by: <@U123>. "
|
|
1473
|
+
'Created by: <@U123>. This person is the Automation creator. "The Automation creator", "me", and "my" in the instructions mean this person. Where the instructions refer to this person, write this mention in the message. If they do not, do not mention this person.',
|
|
1455
1474
|
"Instructions: Summarize the closed issue",
|
|
1456
1475
|
"",
|
|
1457
1476
|
"Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.",
|
|
@@ -1944,6 +1963,14 @@ export function readMockConversationPendingMessages(
|
|
|
1944
1963
|
role: "user" as const,
|
|
1945
1964
|
source: "web" as const,
|
|
1946
1965
|
text: "Keep the reply in Junior. I will paste the dashboard link next.",
|
|
1966
|
+
attachments: [
|
|
1967
|
+
{
|
|
1968
|
+
id: "qa-chart-png",
|
|
1969
|
+
filename: "queued-chart.png",
|
|
1970
|
+
contentType: "image/png",
|
|
1971
|
+
bytes: 18211,
|
|
1972
|
+
},
|
|
1973
|
+
],
|
|
1947
1974
|
},
|
|
1948
1975
|
{
|
|
1949
1976
|
actorIdentity: actor("dev@example.com", "Morgan Lee", "morgan"),
|
|
@@ -1978,6 +2005,28 @@ export function readMockConversationPendingMessages(
|
|
|
1978
2005
|
source: "web" as const,
|
|
1979
2006
|
text: "Fifth queued message.",
|
|
1980
2007
|
},
|
|
2008
|
+
{
|
|
2009
|
+
actorIdentity: actor("dev@example.com", "Morgan Lee", "morgan"),
|
|
2010
|
+
createdAt: iso(nowMs, -800),
|
|
2011
|
+
delivery: "defer" as const,
|
|
2012
|
+
inboundMessageId: `${conversationId}:pending-sixth`,
|
|
2013
|
+
messageId: `${conversationId}:pending-sixth`,
|
|
2014
|
+
receivedAt: iso(nowMs, -400),
|
|
2015
|
+
role: "user" as const,
|
|
2016
|
+
source: "web" as const,
|
|
2017
|
+
text: "Sixth queued message.",
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
actorIdentity: actor("dev@example.com", "Morgan Lee", "morgan"),
|
|
2021
|
+
createdAt: iso(nowMs, -600),
|
|
2022
|
+
delivery: "defer" as const,
|
|
2023
|
+
inboundMessageId: `${conversationId}:pending-seventh`,
|
|
2024
|
+
messageId: `${conversationId}:pending-seventh`,
|
|
2025
|
+
receivedAt: iso(nowMs, -300),
|
|
2026
|
+
role: "user" as const,
|
|
2027
|
+
source: "web" as const,
|
|
2028
|
+
text: "Seventh queued message.",
|
|
2029
|
+
},
|
|
1981
2030
|
]
|
|
1982
2031
|
: [];
|
|
1983
2032
|
|
|
@@ -2949,6 +2998,7 @@ function mockTasks(): AutomationSummary[] {
|
|
|
2949
2998
|
nextRunAt: "2026-08-10T16:00:00.000Z",
|
|
2950
2999
|
credentialMode: "creator",
|
|
2951
3000
|
ownedByViewer: true,
|
|
3001
|
+
costUsd: { 1: 0.42, 7: 1.02, 30: 4.18, 90: 16.4 },
|
|
2952
3002
|
runs: { 1: 1, 7: 3, 30: 12, 90: 48 },
|
|
2953
3003
|
timezone: "America/Los_Angeles",
|
|
2954
3004
|
schedule: "Every Monday at 9:00 AM",
|
|
@@ -2980,6 +3030,7 @@ function mockTasks(): AutomationSummary[] {
|
|
|
2980
3030
|
credentialMode: "creator",
|
|
2981
3031
|
ownedByViewer: true,
|
|
2982
3032
|
resource: "Issue · ACME-42",
|
|
3033
|
+
costUsd: { 1: 0, 7: 0.12, 30: 0.48, 90: 0.84 },
|
|
2983
3034
|
runs: { 1: 0, 7: 1, 30: 4, 90: 7 },
|
|
2984
3035
|
source: "github",
|
|
2985
3036
|
outcomes: [],
|
|
@@ -3008,6 +3059,7 @@ function mockTasks(): AutomationSummary[] {
|
|
|
3008
3059
|
credentialMode: "creator",
|
|
3009
3060
|
ownedByViewer: false,
|
|
3010
3061
|
resource: "Incident · INC-17",
|
|
3062
|
+
costUsd: { 1: 0, 7: 0, 30: 0, 90: 0 },
|
|
3011
3063
|
runs: { 1: 0, 7: 0, 30: 0, 90: 0 },
|
|
3012
3064
|
source: "pagerduty",
|
|
3013
3065
|
outcomes: [
|