@sentry/junior-dashboard 0.234.0 → 0.236.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 +100 -18
- package/dist/client.js +47 -46
- package/dist/index.d.ts +3 -1
- package/dist/index.js +100 -18
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/app.ts +7 -1
- package/src/assets/junior-avatar.png +0 -0
- package/src/assets/junior-favicon.png +0 -0
- package/src/assets.ts +1 -0
- package/src/client/App.tsx +35 -0
- package/src/client/api.ts +63 -0
- package/src/client/components/README.md +8 -4
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/conversations/ConversationEventLog.tsx +45 -5
- package/src/client/conversations/ConversationForkLinks.tsx +51 -0
- package/src/client/conversations/ConversationHeader.tsx +12 -1
- package/src/client/conversations/ConversationPage.tsx +72 -2
- package/src/client/conversations/ForkConversationButton.tsx +125 -0
- package/src/client/conversations/TranscriptMessageView.tsx +28 -7
- package/src/client/conversations/TranscriptView.tsx +4 -0
- package/src/client/conversations/conversationRoutes.ts +10 -0
- package/src/client/conversations/queries.ts +45 -0
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +28 -27
- package/src/client/pages/automations/AutomationEditPage.tsx +23 -24
- package/src/client/pages/automations/AutomationEditor.tsx +66 -13
- package/src/client/pages/automations/AutomationFilters.tsx +22 -14
- package/src/client/pages/automations/AutomationLifecycle.tsx +6 -4
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +18 -0
- package/src/client/pages/automations/AutomationRow.tsx +172 -187
- package/src/client/pages/automations/AutomationVersionsPage.tsx +356 -0
- package/src/client/pages/automations/AutomationsPage.tsx +40 -42
- package/src/client/pages/automations/AutomationsPageLayout.tsx +6 -5
- package/src/client/pages/automations/README.md +14 -3
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/code/CodeChangeRow.tsx +41 -0
- package/src/client/pages/code/CodePage.tsx +18 -93
- package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
- package/src/client/pages/code/RepositoryPage.tsx +227 -0
- package/src/client/pages/code/RepositorySections.tsx +108 -0
- package/src/client/pages/code/RepositoryTabs.tsx +146 -0
- package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
- package/src/client/pages/dev/ComponentsPage.tsx +5 -22
- package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
- package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
- package/src/client/pages/system/SystemNavigation.tsx +6 -2
- package/src/client/pages/system/SystemPageLayout.tsx +4 -6
- package/src/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +8 -2
- package/src/mock-reporting/routes.ts +25 -8
- package/src/shell.ts +21 -5
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { GitFork } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { conversationPath } from "./conversationRoutes";
|
|
4
|
+
|
|
5
|
+
const linkClassName =
|
|
6
|
+
"font-medium text-dashboard-text no-underline underline-offset-2 hover:underline";
|
|
7
|
+
|
|
8
|
+
/** Link a conversation to its fork source and to its forks. */
|
|
9
|
+
export function ConversationForkLinks(props: {
|
|
10
|
+
forkedFromConversationId?: string;
|
|
11
|
+
forkedFromTitle?: string;
|
|
12
|
+
forks?: string[];
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<nav
|
|
16
|
+
aria-label="Conversation forks"
|
|
17
|
+
className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-xs leading-snug text-dashboard-text-muted"
|
|
18
|
+
>
|
|
19
|
+
<GitFork aria-hidden="true" size={12} className="shrink-0" />
|
|
20
|
+
{props.forkedFromConversationId ? (
|
|
21
|
+
<span>
|
|
22
|
+
Forked from{" "}
|
|
23
|
+
<Link
|
|
24
|
+
className={linkClassName}
|
|
25
|
+
to={conversationPath(props.forkedFromConversationId)}
|
|
26
|
+
>
|
|
27
|
+
{props.forkedFromTitle ?? "source conversation"}
|
|
28
|
+
</Link>
|
|
29
|
+
</span>
|
|
30
|
+
) : null}
|
|
31
|
+
{props.forkedFromConversationId && props.forks?.length ? (
|
|
32
|
+
<span>·</span>
|
|
33
|
+
) : null}
|
|
34
|
+
{props.forks?.length ? (
|
|
35
|
+
<span className="inline-flex flex-wrap items-center gap-x-1.5">
|
|
36
|
+
Forks:
|
|
37
|
+
{props.forks.map((id, index) => (
|
|
38
|
+
<Link
|
|
39
|
+
key={id}
|
|
40
|
+
aria-label={`Fork ${index + 1}`}
|
|
41
|
+
className={linkClassName}
|
|
42
|
+
to={conversationPath(id)}
|
|
43
|
+
>
|
|
44
|
+
{index + 1}
|
|
45
|
+
</Link>
|
|
46
|
+
))}
|
|
47
|
+
</span>
|
|
48
|
+
) : null}
|
|
49
|
+
</nav>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -32,6 +32,8 @@ export function ConversationHeader(props: {
|
|
|
32
32
|
archive: ConversationArchiveAction;
|
|
33
33
|
brief?: ReactNode;
|
|
34
34
|
conversationId: string;
|
|
35
|
+
/** Links to the fork source and to forks of this conversation. */
|
|
36
|
+
forks?: ReactNode;
|
|
35
37
|
identity: ReactNode;
|
|
36
38
|
linkedWork?: ReactNode;
|
|
37
39
|
lastActivityAt?: string;
|
|
@@ -149,7 +151,10 @@ export function ConversationHeader(props: {
|
|
|
149
151
|
);
|
|
150
152
|
|
|
151
153
|
const showMobileHeader =
|
|
152
|
-
searchOpenVisible ||
|
|
154
|
+
searchOpenVisible ||
|
|
155
|
+
props.archive.error ||
|
|
156
|
+
Boolean(props.linkedWork) ||
|
|
157
|
+
Boolean(props.forks);
|
|
153
158
|
|
|
154
159
|
const liveIndicator = props.live ? (
|
|
155
160
|
<span
|
|
@@ -183,6 +188,9 @@ export function ConversationHeader(props: {
|
|
|
183
188
|
{props.meta}
|
|
184
189
|
</div>
|
|
185
190
|
) : null}
|
|
191
|
+
{props.forks ? (
|
|
192
|
+
<div className="mt-1.5 min-w-0">{props.forks}</div>
|
|
193
|
+
) : null}
|
|
186
194
|
</div>
|
|
187
195
|
<ConversationHeaderActions
|
|
188
196
|
archive={props.archive}
|
|
@@ -196,6 +204,9 @@ export function ConversationHeader(props: {
|
|
|
196
204
|
view={props.view}
|
|
197
205
|
/>
|
|
198
206
|
</div>
|
|
207
|
+
{props.forks ? (
|
|
208
|
+
<div className="px-4 py-2 md:hidden">{props.forks}</div>
|
|
209
|
+
) : null}
|
|
199
210
|
{props.linkedWork ? (
|
|
200
211
|
<div
|
|
201
212
|
aria-label="Linked work"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
2
3
|
import type {
|
|
3
4
|
ConversationDetailReport,
|
|
4
5
|
ConversationFeed,
|
|
@@ -18,8 +19,13 @@ import type { ConversationMailboxMessage } from "./conversationOutbox";
|
|
|
18
19
|
import { buildConversationMarkdown } from "../markdownExport";
|
|
19
20
|
import { CopyMarkdownButton } from "./CopyMarkdownButton";
|
|
20
21
|
import { ConversationBrief } from "./ConversationBrief";
|
|
22
|
+
import {
|
|
23
|
+
CONVERSATION_EVENT_PARAM,
|
|
24
|
+
parseConversationEventSeq,
|
|
25
|
+
} from "./conversationRoutes";
|
|
21
26
|
import { ConversationComposer } from "./ConversationComposer";
|
|
22
27
|
import { ConversationHeader } from "./ConversationHeader";
|
|
28
|
+
import { ConversationForkLinks } from "./ConversationForkLinks";
|
|
23
29
|
import { ConversationHeaderMeta } from "./ConversationHeaderMeta";
|
|
24
30
|
import {
|
|
25
31
|
ConversationAnnotations,
|
|
@@ -67,7 +73,15 @@ export function ConversationPage(props: {
|
|
|
67
73
|
}) {
|
|
68
74
|
const [subagentTarget, setSubagentTarget] =
|
|
69
75
|
useState<SubagentTranscriptTarget>();
|
|
70
|
-
const [
|
|
76
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
77
|
+
// The URL owns the open event log entry so readers can share that link.
|
|
78
|
+
const selectedEventSeq = parseConversationEventSeq(
|
|
79
|
+
searchParams.get(CONVERSATION_EVENT_PARAM),
|
|
80
|
+
);
|
|
81
|
+
const [viewState, setView] = useState<TranscriptViewMode>(() =>
|
|
82
|
+
selectedEventSeq === undefined ? "rich" : "raw",
|
|
83
|
+
);
|
|
84
|
+
const view = selectedEventSeq === undefined ? viewState : "raw";
|
|
71
85
|
const [search, setSearch] = useState("");
|
|
72
86
|
const [pinRequestVersion, setPinRequestVersion] = useState(0);
|
|
73
87
|
const conversationId = props.conversationId;
|
|
@@ -109,6 +123,48 @@ export function ConversationPage(props: {
|
|
|
109
123
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
110
124
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
111
125
|
pendingGeneratedAtRef.current = detail.pendingGeneratedAt;
|
|
126
|
+
const setSelectedEventSeq = useCallback(
|
|
127
|
+
(seq: number | undefined) => {
|
|
128
|
+
// Keep the event log after close, even when a link opened the page.
|
|
129
|
+
setView("raw");
|
|
130
|
+
setSearchParams(
|
|
131
|
+
(current) => {
|
|
132
|
+
const next = new URLSearchParams(current);
|
|
133
|
+
if (seq === undefined) next.delete(CONVERSATION_EVENT_PARAM);
|
|
134
|
+
else next.set(CONVERSATION_EVENT_PARAM, String(seq));
|
|
135
|
+
return next;
|
|
136
|
+
},
|
|
137
|
+
{ replace: true },
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
[setSearchParams],
|
|
141
|
+
);
|
|
142
|
+
const onViewChange = useCallback(
|
|
143
|
+
(value: TranscriptViewMode) => {
|
|
144
|
+
if (value !== "raw" && selectedEventSeq !== undefined) {
|
|
145
|
+
setSelectedEventSeq(undefined);
|
|
146
|
+
}
|
|
147
|
+
setView(value);
|
|
148
|
+
},
|
|
149
|
+
[selectedEventSeq, setSelectedEventSeq],
|
|
150
|
+
);
|
|
151
|
+
// A linked event can be older than the first page. Load earlier pages until
|
|
152
|
+
// it is present or history ends.
|
|
153
|
+
const loadPreviousPageRef = useRef(detail.loadPreviousPage);
|
|
154
|
+
loadPreviousPageRef.current = detail.loadPreviousPage;
|
|
155
|
+
const oldestLoadedSeq = transcript?.events[0]?.seq;
|
|
156
|
+
const linkedEventMissing =
|
|
157
|
+
selectedEventSeq !== undefined &&
|
|
158
|
+
oldestLoadedSeq !== undefined &&
|
|
159
|
+
selectedEventSeq < oldestLoadedSeq;
|
|
160
|
+
const loadLinkedEvent =
|
|
161
|
+
linkedEventMissing &&
|
|
162
|
+
detail.hasPreviousPage &&
|
|
163
|
+
!detail.isLoadingPreviousPage &&
|
|
164
|
+
!detail.historyError;
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (loadLinkedEvent) loadPreviousPageRef.current();
|
|
167
|
+
}, [loadLinkedEvent, oldestLoadedSeq]);
|
|
112
168
|
const requestPin = useCallback(() => {
|
|
113
169
|
setPinRequestVersion((version) => version + 1);
|
|
114
170
|
}, []);
|
|
@@ -137,6 +193,18 @@ export function ConversationPage(props: {
|
|
|
137
193
|
<section className="min-w-0">
|
|
138
194
|
<ConversationHeader
|
|
139
195
|
conversationId={conversationId}
|
|
196
|
+
forks={
|
|
197
|
+
detail.data?.forkedFromConversationId ||
|
|
198
|
+
detail.data?.forks?.length ? (
|
|
199
|
+
<ConversationForkLinks
|
|
200
|
+
forkedFromConversationId={
|
|
201
|
+
detail.data.forkedFromConversationId
|
|
202
|
+
}
|
|
203
|
+
forkedFromTitle={detail.data.forkedFromTitle}
|
|
204
|
+
forks={detail.data.forks}
|
|
205
|
+
/>
|
|
206
|
+
) : null
|
|
207
|
+
}
|
|
140
208
|
lastActivityAt={conversation?.lastSeenAt}
|
|
141
209
|
sentryConversationUrl={detail.data?.sentryConversationUrl}
|
|
142
210
|
copyAction={
|
|
@@ -206,7 +274,7 @@ export function ConversationPage(props: {
|
|
|
206
274
|
/>
|
|
207
275
|
}
|
|
208
276
|
onSearchChange={setSearch}
|
|
209
|
-
onViewChange={
|
|
277
|
+
onViewChange={onViewChange}
|
|
210
278
|
privacy={
|
|
211
279
|
<ConversationPrivacyChip
|
|
212
280
|
visibility={conversation?.visibility}
|
|
@@ -256,12 +324,14 @@ export function ConversationPage(props: {
|
|
|
256
324
|
live={live}
|
|
257
325
|
loadingPreviousPage={detail.isLoadingPreviousPage}
|
|
258
326
|
onLoadPreviousPage={detail.loadPreviousPage}
|
|
327
|
+
onSelectedEventSeqChange={setSelectedEventSeq}
|
|
259
328
|
pinRequestVersion={pinRequestVersion}
|
|
260
329
|
responding={
|
|
261
330
|
!detail.error && conversationIsResponding(transcript)
|
|
262
331
|
}
|
|
263
332
|
onOpenSubagentTranscript={onOpenSubagentTranscript}
|
|
264
333
|
search={search}
|
|
334
|
+
selectedEventSeq={selectedEventSeq}
|
|
265
335
|
transcript={transcript}
|
|
266
336
|
view={view}
|
|
267
337
|
/>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { GitFork } from "lucide-react";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useNavigate } from "react-router";
|
|
5
|
+
import { Button } from "../components/Button";
|
|
6
|
+
import { Drawer } from "../components/Drawer";
|
|
7
|
+
import { Field } from "../components/Field";
|
|
8
|
+
import { TextArea } from "../components/TextInput";
|
|
9
|
+
import { DashboardApiError } from "../http";
|
|
10
|
+
import { conversationPath } from "./conversationRoutes";
|
|
11
|
+
import { useForkConversation } from "./queries";
|
|
12
|
+
|
|
13
|
+
type ForkTarget = { conversationId: string; messageId: string };
|
|
14
|
+
|
|
15
|
+
/** Open the fork dialog for one assistant reply. */
|
|
16
|
+
export function ForkConversationButton(props: ForkTarget) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Button
|
|
21
|
+
size="icon"
|
|
22
|
+
aria-label="Fork after this message"
|
|
23
|
+
title="Fork after this message"
|
|
24
|
+
onClick={() => setOpen(true)}
|
|
25
|
+
>
|
|
26
|
+
<GitFork size={14} aria-hidden="true" />
|
|
27
|
+
</Button>
|
|
28
|
+
{open
|
|
29
|
+
? createPortal(
|
|
30
|
+
<ForkConversationDialog
|
|
31
|
+
{...props}
|
|
32
|
+
onClose={() => setOpen(false)}
|
|
33
|
+
/>,
|
|
34
|
+
document.body,
|
|
35
|
+
)
|
|
36
|
+
: null}
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ForkConversationDialog(props: ForkTarget & { onClose(): void }) {
|
|
42
|
+
const titleId = useId();
|
|
43
|
+
const inputId = useId();
|
|
44
|
+
const [idempotencyKey] = useState(() => crypto.randomUUID());
|
|
45
|
+
const [message, setMessage] = useState("");
|
|
46
|
+
const navigate = useNavigate();
|
|
47
|
+
const fork = useForkConversation(props.conversationId);
|
|
48
|
+
const submit = () => {
|
|
49
|
+
if (!message.trim() || fork.isPending) return;
|
|
50
|
+
fork.mutate(
|
|
51
|
+
{ idempotencyKey, message, messageId: props.messageId },
|
|
52
|
+
{
|
|
53
|
+
onSuccess: (accepted) => {
|
|
54
|
+
props.onClose();
|
|
55
|
+
navigate(conversationPath(accepted.conversationId));
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
return (
|
|
61
|
+
<Drawer
|
|
62
|
+
closeLabel="Close fork dialog"
|
|
63
|
+
dismissLabel="Dismiss fork dialog"
|
|
64
|
+
header={
|
|
65
|
+
<h2 id={titleId} className="m-0 text-lg font-semibold">
|
|
66
|
+
Fork Conversation
|
|
67
|
+
</h2>
|
|
68
|
+
}
|
|
69
|
+
titleId={titleId}
|
|
70
|
+
openKey={idempotencyKey}
|
|
71
|
+
onClose={() => {
|
|
72
|
+
if (!fork.isPending) props.onClose();
|
|
73
|
+
}}
|
|
74
|
+
width="narrow"
|
|
75
|
+
>
|
|
76
|
+
<form
|
|
77
|
+
className="grid gap-4 text-sm"
|
|
78
|
+
onSubmit={(event) => {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
submit();
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<p className="m-0">
|
|
84
|
+
Creates a fork of the conversation from this point.
|
|
85
|
+
</p>
|
|
86
|
+
<Field htmlFor={inputId} label="Message">
|
|
87
|
+
<TextArea
|
|
88
|
+
id={inputId}
|
|
89
|
+
prose
|
|
90
|
+
autoFocus
|
|
91
|
+
className="min-h-28"
|
|
92
|
+
placeholder="Message Junior…"
|
|
93
|
+
value={message}
|
|
94
|
+
disabled={fork.isPending}
|
|
95
|
+
onChange={(event) => setMessage(event.target.value)}
|
|
96
|
+
onKeyDown={(event) => {
|
|
97
|
+
// Same keys as the composer: Enter sends, Shift+Enter adds a line.
|
|
98
|
+
if (
|
|
99
|
+
event.key === "Enter" &&
|
|
100
|
+
!event.shiftKey &&
|
|
101
|
+
!event.nativeEvent.isComposing
|
|
102
|
+
) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
submit();
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</Field>
|
|
109
|
+
{fork.error ? (
|
|
110
|
+
<p role="alert" className="m-0 text-red-300">
|
|
111
|
+
{(fork.error instanceof DashboardApiError && fork.error.apiError) ||
|
|
112
|
+
"Could not fork the conversation. Try again."}
|
|
113
|
+
</p>
|
|
114
|
+
) : null}
|
|
115
|
+
<Button
|
|
116
|
+
tone="primary"
|
|
117
|
+
type="submit"
|
|
118
|
+
disabled={!message.trim() || fork.isPending}
|
|
119
|
+
>
|
|
120
|
+
{fork.isPending ? "Forking…" : "Fork and send"}
|
|
121
|
+
</Button>
|
|
122
|
+
</form>
|
|
123
|
+
</Drawer>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ForkConversationButton } from "./ForkConversationButton";
|
|
1
2
|
import { TranscriptAttachments } from "./TranscriptAttachment";
|
|
2
3
|
import { ObjectCard } from "./ObjectCard";
|
|
3
4
|
import { AutomationCard } from "../components/AutomationCard";
|
|
@@ -46,11 +47,7 @@ export const TranscriptMessageView = memo(
|
|
|
46
47
|
}}
|
|
47
48
|
>
|
|
48
49
|
<TranscriptMessageHeader
|
|
49
|
-
|
|
50
|
-
props.message.role === "user" && props.message.contexts?.length ? (
|
|
51
|
-
<TranscriptTurnContextView contexts={props.message.contexts} />
|
|
52
|
-
) : undefined
|
|
53
|
-
}
|
|
50
|
+
action={messageAction(props.message, props.conversation)}
|
|
54
51
|
message={props.message}
|
|
55
52
|
conversation={props.conversation}
|
|
56
53
|
/>
|
|
@@ -86,6 +83,7 @@ export const TranscriptMessageView = memo(
|
|
|
86
83
|
previous.message === next.message &&
|
|
87
84
|
previous.conversation.conversationId === next.conversation.conversationId &&
|
|
88
85
|
previous.conversation.surface === next.conversation.surface &&
|
|
86
|
+
previous.conversation.canFork === next.conversation.canFork &&
|
|
89
87
|
previous.conversation.actorIdentity === next.conversation.actorIdentity,
|
|
90
88
|
);
|
|
91
89
|
|
|
@@ -114,8 +112,31 @@ export function RedactedMessageView(props: {
|
|
|
114
112
|
);
|
|
115
113
|
}
|
|
116
114
|
|
|
115
|
+
function messageAction(
|
|
116
|
+
message: TranscriptViewMessage,
|
|
117
|
+
conversation: ConversationTranscript,
|
|
118
|
+
): ReactNode {
|
|
119
|
+
if (message.role === "user" && message.contexts?.length) {
|
|
120
|
+
return <TranscriptTurnContextView contexts={message.contexts} />;
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
conversation.canFork &&
|
|
124
|
+
message.role === "assistant" &&
|
|
125
|
+
message.messageId &&
|
|
126
|
+
!message.pending
|
|
127
|
+
) {
|
|
128
|
+
return (
|
|
129
|
+
<ForkConversationButton
|
|
130
|
+
conversationId={conversation.conversationId}
|
|
131
|
+
messageId={message.messageId}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
function TranscriptMessageHeader(props: {
|
|
118
|
-
|
|
139
|
+
action?: ReactNode;
|
|
119
140
|
message: TranscriptViewMessage;
|
|
120
141
|
conversation: ConversationTranscript;
|
|
121
142
|
}) {
|
|
@@ -127,7 +148,7 @@ function TranscriptMessageHeader(props: {
|
|
|
127
148
|
);
|
|
128
149
|
|
|
129
150
|
return (
|
|
130
|
-
<TranscriptMessageHeading action={props.
|
|
151
|
+
<TranscriptMessageHeading action={props.action}>
|
|
131
152
|
<span className={transcriptRoleLabelClass(props.message.role)}>
|
|
132
153
|
{roleLabel}
|
|
133
154
|
</span>
|
|
@@ -25,8 +25,10 @@ export function Transcript(props: {
|
|
|
25
25
|
loadingPreviousPage?: boolean;
|
|
26
26
|
pinRequestVersion?: number;
|
|
27
27
|
onLoadPreviousPage?: () => void;
|
|
28
|
+
onSelectedEventSeqChange?(seq: number | undefined): void;
|
|
28
29
|
responding?: boolean;
|
|
29
30
|
search?: string;
|
|
31
|
+
selectedEventSeq?: number;
|
|
30
32
|
onOpenSubagentTranscript?: (args: {
|
|
31
33
|
part: TranscriptViewSubagentPart;
|
|
32
34
|
conversation: ConversationTranscript;
|
|
@@ -101,6 +103,8 @@ export function Transcript(props: {
|
|
|
101
103
|
<ConversationEventLog
|
|
102
104
|
key={props.transcript.conversationId}
|
|
103
105
|
conversation={props.transcript}
|
|
106
|
+
onSelectedSeqChange={props.onSelectedEventSeqChange}
|
|
107
|
+
selectedSeq={props.selectedEventSeq}
|
|
104
108
|
/>
|
|
105
109
|
) : (
|
|
106
110
|
<ConversationTranscriptView
|
|
@@ -10,3 +10,13 @@ export const NEW_CONVERSATION_PATH = "/";
|
|
|
10
10
|
export function isNewConversationPath(pathname: string): boolean {
|
|
11
11
|
return pathname === "/" || pathname === "/conversations/new";
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/** Query param that opens one event log entry by its sequence number. */
|
|
15
|
+
export const CONVERSATION_EVENT_PARAM = "event";
|
|
16
|
+
|
|
17
|
+
/** Read a linked event sequence number. Ignore values that are not one. */
|
|
18
|
+
export function parseConversationEventSeq(
|
|
19
|
+
value: string | null | undefined,
|
|
20
|
+
): number | undefined {
|
|
21
|
+
return value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
22
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
conversationDetailReportSchema,
|
|
26
26
|
conversationEventPageSchema,
|
|
27
27
|
conversationPendingMessagesReportSchema,
|
|
28
|
+
forkConversationResponseSchema,
|
|
28
29
|
} from "@sentry/junior/api/schema";
|
|
29
30
|
|
|
30
31
|
import type { DashboardCoreData } from "../types";
|
|
@@ -227,6 +228,50 @@ export function useCreateConversation() {
|
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Fork a conversation at one assistant reply and send the first message to
|
|
233
|
+
* the fork. A retry with the same key returns the same fork and message.
|
|
234
|
+
*/
|
|
235
|
+
export function useForkConversation(sourceConversationId: string) {
|
|
236
|
+
const queryClient = useQueryClient();
|
|
237
|
+
return useMutation({
|
|
238
|
+
mutationFn: async (args: {
|
|
239
|
+
idempotencyKey: string;
|
|
240
|
+
message: string;
|
|
241
|
+
messageId: string;
|
|
242
|
+
}) => {
|
|
243
|
+
const fork = await post(
|
|
244
|
+
forkConversationResponseSchema,
|
|
245
|
+
`/api/conversations/${encodeURIComponent(sourceConversationId)}/forks`,
|
|
246
|
+
{ messageId: args.messageId, idempotencyKey: args.idempotencyKey },
|
|
247
|
+
);
|
|
248
|
+
return post(
|
|
249
|
+
acceptedConversationMessageSchema,
|
|
250
|
+
`/api/conversations/${encodeURIComponent(fork.conversationId)}/messages`,
|
|
251
|
+
{ idempotencyKey: args.idempotencyKey, message: args.message },
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
onSuccess: (accepted, args) => {
|
|
255
|
+
queryClient.setQueryData<ConversationOutboxMessage[]>(
|
|
256
|
+
conversationOutboxQueryKey(accepted.conversationId),
|
|
257
|
+
(current) =>
|
|
258
|
+
upsertConversationOutboxMessage(current, {
|
|
259
|
+
...conversationOutboxMessageForSubmit({
|
|
260
|
+
idempotencyKey: args.idempotencyKey,
|
|
261
|
+
message: args.message,
|
|
262
|
+
actorIdentity: outboxActorIdentity(queryClient),
|
|
263
|
+
messageId: accepted.messageId,
|
|
264
|
+
}),
|
|
265
|
+
status: "accepted",
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
void queryClient.invalidateQueries({
|
|
269
|
+
queryKey: ["dashboard", "conversations"],
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
230
275
|
/** Add one dashboard message and refresh the shared transcript. */
|
|
231
276
|
export function useAppendConversationMessage(conversationId: string) {
|
|
232
277
|
const queryClient = useQueryClient();
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { AutomationLifecycle } from "./AutomationLifecycle";
|
|
2
2
|
import { automationOutcomeLabel } from "./automationOutcomes";
|
|
3
|
+
import {
|
|
4
|
+
automationEventConditions,
|
|
5
|
+
automationEventNames,
|
|
6
|
+
automationScheduleLabel,
|
|
7
|
+
formatAutomationRunTime,
|
|
8
|
+
} from "./automationFormat";
|
|
3
9
|
import type { AutomationSummary } from "@sentry/junior/api/schema";
|
|
4
10
|
import { Link, useLocation } from "react-router";
|
|
5
11
|
import { Detail, DetailList } from "../../components/DetailList";
|
|
@@ -34,26 +40,24 @@ export function AutomationDetailsDrawer(props: {
|
|
|
34
40
|
) : (
|
|
35
41
|
automation.createdBy
|
|
36
42
|
);
|
|
43
|
+
const conditions =
|
|
44
|
+
automation.kind === "event" ? automationEventConditions(automation) : "";
|
|
37
45
|
const details =
|
|
38
46
|
automation.kind === "scheduled"
|
|
39
47
|
? [
|
|
40
|
-
{ label: "Schedule", value: automation
|
|
48
|
+
{ label: "Schedule", value: automationScheduleLabel(automation) },
|
|
41
49
|
{
|
|
42
50
|
label: "Next run",
|
|
43
51
|
value:
|
|
44
52
|
automation.status === "active" && automation.nextRunAt
|
|
45
|
-
?
|
|
53
|
+
? formatAutomationRunTime(automation.nextRunAt)
|
|
46
54
|
: "None",
|
|
47
55
|
},
|
|
48
56
|
]
|
|
49
57
|
: [
|
|
50
58
|
{ label: "Watching", value: automation.resource },
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
value: automation.events
|
|
54
|
-
.map((event) => event.replaceAll(/[._]/g, " "))
|
|
55
|
-
.join(", "),
|
|
56
|
-
},
|
|
59
|
+
{ label: "Events", value: automationEventNames(automation) },
|
|
60
|
+
...(conditions ? [{ label: "Conditions", value: conditions }] : []),
|
|
57
61
|
];
|
|
58
62
|
const titleId = "automation-details-drawer-title";
|
|
59
63
|
|
|
@@ -110,16 +114,24 @@ export function AutomationDetailsDrawer(props: {
|
|
|
110
114
|
)}
|
|
111
115
|
</Detail>
|
|
112
116
|
</DetailList>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
<div className="flex flex-wrap gap-x-5">
|
|
118
|
+
{automation.totalRuns > 0 &&
|
|
119
|
+
automation.lastRunStatus !== "failed" &&
|
|
120
|
+
automation.lastRunStatus !== "blocked" ? (
|
|
121
|
+
<Link
|
|
122
|
+
className="w-fit py-2 text-sm text-dashboard-text underline underline-offset-2"
|
|
123
|
+
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
|
|
124
|
+
>
|
|
125
|
+
View all runs
|
|
126
|
+
</Link>
|
|
127
|
+
) : null}
|
|
116
128
|
<Link
|
|
117
129
|
className="w-fit py-2 text-sm text-dashboard-text underline underline-offset-2"
|
|
118
|
-
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/
|
|
130
|
+
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/versions${location.search}`}
|
|
119
131
|
>
|
|
120
|
-
|
|
132
|
+
Version history
|
|
121
133
|
</Link>
|
|
122
|
-
|
|
134
|
+
</div>
|
|
123
135
|
<details>
|
|
124
136
|
<summary className="cursor-pointer py-3 text-sm text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
|
|
125
137
|
More details
|
|
@@ -192,10 +204,10 @@ function AutomationExecutionSummary(props: {
|
|
|
192
204
|
className="text-dashboard-text underline decoration-white/20 underline-offset-2 hover:decoration-white/60"
|
|
193
205
|
to={conversationPath(automation.lastConversationId)}
|
|
194
206
|
>
|
|
195
|
-
{
|
|
207
|
+
{formatAutomationRunTime(automation.lastRunAt)}
|
|
196
208
|
</Link>
|
|
197
209
|
) : automation.lastRunAt ? (
|
|
198
|
-
|
|
210
|
+
formatAutomationRunTime(automation.lastRunAt)
|
|
199
211
|
) : null}
|
|
200
212
|
</span>
|
|
201
213
|
</div>
|
|
@@ -208,14 +220,3 @@ function formatDate(value: string): string {
|
|
|
208
220
|
timeStyle: "short",
|
|
209
221
|
});
|
|
210
222
|
}
|
|
211
|
-
|
|
212
|
-
function formatRunDate(value: string): string {
|
|
213
|
-
return formatTime(value, {
|
|
214
|
-
day: "numeric",
|
|
215
|
-
hour: "numeric",
|
|
216
|
-
minute: "2-digit",
|
|
217
|
-
month: "short",
|
|
218
|
-
timeZoneName: "short",
|
|
219
|
-
year: "numeric",
|
|
220
|
-
});
|
|
221
|
-
}
|