@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
|
@@ -1,331 +1,11 @@
|
|
|
1
1
|
export const TRANSCRIPT_ANCHOR_CLASS =
|
|
2
2
|
"font-medium text-dashboard-accent-text underline decoration-dashboard-accent/35 underline-offset-2 transition-colors hover:text-dashboard-text hover:decoration-dashboard-text-solid";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
type TextRange = {
|
|
12
|
-
end: number;
|
|
13
|
-
start: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/** Find safe markdown and bare links that should become transcript anchors. */
|
|
17
|
-
export function findTranscriptMarkdownLinks(
|
|
18
|
-
text: string,
|
|
19
|
-
): TranscriptMarkdownLink[] {
|
|
20
|
-
const literalRanges = findLiteralRanges(text);
|
|
21
|
-
const markdownSyntaxRanges = findMarkdownLinkSyntaxRanges(
|
|
22
|
-
text,
|
|
23
|
-
literalRanges,
|
|
24
|
-
);
|
|
25
|
-
const markdownLinks = findMarkdownLinks(text, literalRanges);
|
|
26
|
-
const autolinks = findCommonMarkAutolinks(
|
|
27
|
-
text,
|
|
28
|
-
mergeRanges([...literalRanges, ...markdownSyntaxRanges]),
|
|
29
|
-
);
|
|
30
|
-
const bareRanges = mergeRanges([
|
|
31
|
-
...literalRanges,
|
|
32
|
-
...markdownSyntaxRanges,
|
|
33
|
-
...autolinks,
|
|
34
|
-
]);
|
|
35
|
-
return [
|
|
36
|
-
...markdownLinks,
|
|
37
|
-
...autolinks,
|
|
38
|
-
...findBareLinks(text, bareRanges),
|
|
39
|
-
].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function findMarkdownLinks(
|
|
43
|
-
text: string,
|
|
44
|
-
ignoredRanges: TextRange[],
|
|
45
|
-
): TranscriptMarkdownLink[] {
|
|
46
|
-
const links: TranscriptMarkdownLink[] = [];
|
|
47
|
-
let cursor = 0;
|
|
48
|
-
while (cursor < text.length) {
|
|
49
|
-
const link = findNextMarkdownLink(text, cursor, ignoredRanges);
|
|
50
|
-
if (!link) break;
|
|
51
|
-
links.push(link);
|
|
52
|
-
cursor = link.end;
|
|
53
|
-
}
|
|
54
|
-
return links;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function findNextMarkdownLink(
|
|
58
|
-
text: string,
|
|
59
|
-
start: number,
|
|
60
|
-
ignoredRanges: TextRange[],
|
|
61
|
-
): TranscriptMarkdownLink | undefined {
|
|
62
|
-
for (
|
|
63
|
-
let linkStart = text.indexOf("[", start);
|
|
64
|
-
linkStart >= 0;
|
|
65
|
-
linkStart = text.indexOf("[", linkStart + 1)
|
|
66
|
-
) {
|
|
67
|
-
if (isInRange(linkStart, ignoredRanges) || isEscaped(text, linkStart)) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const labelEnd = text.indexOf("]", linkStart + 1);
|
|
72
|
-
if (labelEnd < 0) return undefined;
|
|
73
|
-
if (text[labelEnd + 1] !== "(") continue;
|
|
74
|
-
|
|
75
|
-
const label = text.slice(linkStart + 1, labelEnd);
|
|
76
|
-
if (!isMarkdownLinkLabel(label)) continue;
|
|
77
|
-
|
|
78
|
-
const destination = readMarkdownDestination(text, labelEnd + 2);
|
|
79
|
-
if (!destination) continue;
|
|
80
|
-
|
|
81
|
-
const href = safeMarkdownDestinationHref(destination.href);
|
|
82
|
-
if (!href) continue;
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
end: destination.end,
|
|
86
|
-
href,
|
|
87
|
-
label,
|
|
88
|
-
start: linkStart,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function findMarkdownLinkSyntaxRanges(
|
|
95
|
-
text: string,
|
|
96
|
-
ignoredRanges: TextRange[],
|
|
97
|
-
): TextRange[] {
|
|
98
|
-
const ranges: TextRange[] = [];
|
|
99
|
-
for (
|
|
100
|
-
let linkStart = text.indexOf("[");
|
|
101
|
-
linkStart >= 0;
|
|
102
|
-
linkStart = text.indexOf("[", linkStart + 1)
|
|
103
|
-
) {
|
|
104
|
-
if (isInRange(linkStart, ignoredRanges) || isEscaped(text, linkStart)) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const labelEnd = text.indexOf("]", linkStart + 1);
|
|
109
|
-
if (labelEnd < 0) continue;
|
|
110
|
-
if (text[labelEnd + 1] !== "(") continue;
|
|
111
|
-
|
|
112
|
-
const label = text.slice(linkStart + 1, labelEnd);
|
|
113
|
-
if (!isMarkdownLinkLabel(label)) continue;
|
|
114
|
-
|
|
115
|
-
const destination = readMarkdownDestination(text, labelEnd + 2);
|
|
116
|
-
if (!destination) continue;
|
|
117
|
-
|
|
118
|
-
ranges.push({ start: linkStart, end: destination.end });
|
|
119
|
-
}
|
|
120
|
-
return ranges;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isMarkdownLinkLabel(label: string): boolean {
|
|
124
|
-
return Boolean(label) && !label.includes("\n") && !label.includes("[");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function readMarkdownDestination(
|
|
128
|
-
text: string,
|
|
129
|
-
start: number,
|
|
130
|
-
): { end: number; href: string } | undefined {
|
|
131
|
-
let depth = 0;
|
|
132
|
-
for (let index = start; index < text.length; index += 1) {
|
|
133
|
-
const char = text[index];
|
|
134
|
-
if (char === "\\") {
|
|
135
|
-
index += 1;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (char === "(") {
|
|
139
|
-
depth += 1;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (char !== ")") continue;
|
|
143
|
-
|
|
144
|
-
if (depth > 0) {
|
|
145
|
-
depth -= 1;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const href = text.slice(start, index);
|
|
150
|
-
return href ? { end: index + 1, href } : undefined;
|
|
151
|
-
}
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function findCommonMarkAutolinks(
|
|
156
|
-
text: string,
|
|
157
|
-
ignoredRanges: TextRange[],
|
|
158
|
-
): TranscriptMarkdownLink[] {
|
|
159
|
-
const links: TranscriptMarkdownLink[] = [];
|
|
160
|
-
const pattern = /<(https?:\/\/[^<>\s]+|mailto:[^<>\s]+)>/gi;
|
|
161
|
-
let match: RegExpExecArray | null;
|
|
162
|
-
|
|
163
|
-
while ((match = pattern.exec(text))) {
|
|
164
|
-
if (isInRange(match.index, ignoredRanges) || isEscaped(text, match.index)) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const href = safeLinkHref(match[1] ?? "");
|
|
168
|
-
if (!href) continue;
|
|
169
|
-
links.push({
|
|
170
|
-
end: match.index + match[0].length,
|
|
171
|
-
href,
|
|
172
|
-
label: href,
|
|
173
|
-
start: match.index,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
return links;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function findNextBareLink(
|
|
180
|
-
text: string,
|
|
181
|
-
start: number,
|
|
182
|
-
ignoredRanges: TextRange[],
|
|
183
|
-
): TranscriptMarkdownLink | undefined {
|
|
184
|
-
// Stop at * so emphasis markers stay outside the URL, matching Slack outbound
|
|
185
|
-
// bare-URL wrapping (`readBareUrl` in packages/junior/src/chat/slack/mrkdwn.ts).
|
|
186
|
-
const bareUrlPattern = /(https?:\/\/[^\s<>"'*]+|mailto:[^\s<>"'*]+)/gi;
|
|
187
|
-
bareUrlPattern.lastIndex = start;
|
|
188
|
-
|
|
189
|
-
let match: RegExpExecArray | null;
|
|
190
|
-
while ((match = bareUrlPattern.exec(text))) {
|
|
191
|
-
if (isInRange(match.index, ignoredRanges)) continue;
|
|
192
|
-
|
|
193
|
-
const bareLink = trimBareUrl(match[0]);
|
|
194
|
-
const href = safeLinkHref(bareLink);
|
|
195
|
-
if (!href) continue;
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
end: match.index + bareLink.length,
|
|
199
|
-
href,
|
|
200
|
-
label: href,
|
|
201
|
-
start: match.index,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function findBareLinks(
|
|
208
|
-
text: string,
|
|
209
|
-
ignoredRanges: TextRange[],
|
|
210
|
-
): TranscriptMarkdownLink[] {
|
|
211
|
-
const links: TranscriptMarkdownLink[] = [];
|
|
212
|
-
let cursor = 0;
|
|
213
|
-
while (cursor < text.length) {
|
|
214
|
-
const link = findNextBareLink(text, cursor, ignoredRanges);
|
|
215
|
-
if (!link) break;
|
|
216
|
-
links.push(link);
|
|
217
|
-
cursor = link.end;
|
|
218
|
-
}
|
|
219
|
-
return links;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function findLiteralRanges(text: string): TextRange[] {
|
|
223
|
-
return [...findInlineCodeRanges(text), ...findEscapedMarkdownLinkRanges(text)]
|
|
224
|
-
.filter((range) => range.end > range.start)
|
|
225
|
-
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function findInlineCodeRanges(text: string): TextRange[] {
|
|
229
|
-
const ranges: TextRange[] = [];
|
|
230
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
231
|
-
if (text[index] !== "`" || isEscaped(text, index)) continue;
|
|
232
|
-
|
|
233
|
-
const ticks = countRun(text, index, "`");
|
|
234
|
-
const closing = findClosingBacktickRun(text, index + ticks, ticks);
|
|
235
|
-
if (closing < 0) {
|
|
236
|
-
index += ticks - 1;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
ranges.push({ start: index, end: closing + ticks });
|
|
241
|
-
index = closing + ticks - 1;
|
|
242
|
-
}
|
|
243
|
-
return ranges;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function findClosingBacktickRun(
|
|
247
|
-
text: string,
|
|
248
|
-
start: number,
|
|
249
|
-
ticks: number,
|
|
250
|
-
): number {
|
|
251
|
-
for (let index = start; index < text.length; index += 1) {
|
|
252
|
-
if (text[index] !== "`" || isEscaped(text, index)) continue;
|
|
253
|
-
|
|
254
|
-
const run = countRun(text, index, "`");
|
|
255
|
-
if (run === ticks) return index;
|
|
256
|
-
index += run - 1;
|
|
257
|
-
}
|
|
258
|
-
return -1;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function findEscapedMarkdownLinkRanges(text: string): TextRange[] {
|
|
262
|
-
const ranges: TextRange[] = [];
|
|
263
|
-
for (
|
|
264
|
-
let linkStart = text.indexOf("[");
|
|
265
|
-
linkStart >= 0;
|
|
266
|
-
linkStart = text.indexOf("[", linkStart + 1)
|
|
267
|
-
) {
|
|
268
|
-
if (!isEscaped(text, linkStart)) continue;
|
|
269
|
-
|
|
270
|
-
const labelEnd = text.indexOf("]", linkStart + 1);
|
|
271
|
-
if (labelEnd < 0) continue;
|
|
272
|
-
if (text[labelEnd + 1] !== "(") continue;
|
|
273
|
-
|
|
274
|
-
const destination = readMarkdownDestination(text, labelEnd + 2);
|
|
275
|
-
if (!destination) continue;
|
|
276
|
-
|
|
277
|
-
ranges.push({ start: linkStart, end: destination.end });
|
|
278
|
-
}
|
|
279
|
-
return ranges;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function isInRange(index: number, ranges: TextRange[]): boolean {
|
|
283
|
-
for (const range of ranges) {
|
|
284
|
-
if (index < range.start) return false;
|
|
285
|
-
if (index >= range.start && index < range.end) return true;
|
|
286
|
-
}
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function isEscaped(text: string, index: number): boolean {
|
|
291
|
-
let backslashes = 0;
|
|
292
|
-
for (
|
|
293
|
-
let cursor = index - 1;
|
|
294
|
-
cursor >= 0 && text[cursor] === "\\";
|
|
295
|
-
cursor -= 1
|
|
296
|
-
) {
|
|
297
|
-
backslashes += 1;
|
|
298
|
-
}
|
|
299
|
-
return backslashes % 2 === 1;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function countRun(text: string, start: number, char: string): number {
|
|
303
|
-
let count = 0;
|
|
304
|
-
for (
|
|
305
|
-
let index = start;
|
|
306
|
-
index < text.length && text[index] === char;
|
|
307
|
-
index += 1
|
|
308
|
-
) {
|
|
309
|
-
count += 1;
|
|
310
|
-
}
|
|
311
|
-
return count;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function safeMarkdownDestinationHref(destination: string): string | undefined {
|
|
315
|
-
const href = readMarkdownHref(destination);
|
|
316
|
-
return href ? safeLinkHref(href) : undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function readMarkdownHref(destination: string): string | undefined {
|
|
320
|
-
const trimmed = destination.trim();
|
|
321
|
-
const match =
|
|
322
|
-
/^(<[^>\n]+>|[^\s]+)(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?$/.exec(trimmed);
|
|
323
|
-
const href = match?.[1];
|
|
324
|
-
if (!href) return undefined;
|
|
325
|
-
return href.startsWith("<") && href.endsWith(">") ? href.slice(1, -1) : href;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function safeLinkHref(href: string): string | undefined {
|
|
4
|
+
/**
|
|
5
|
+
* Keep a transcript link destination only when it is an absolute `http:`,
|
|
6
|
+
* `https:`, or `mailto:` URL. Return undefined to drop the link.
|
|
7
|
+
*/
|
|
8
|
+
export function safeTranscriptHref(href: string): string | undefined {
|
|
329
9
|
const trimmed = href.trim();
|
|
330
10
|
const hasControlCharacter = [...trimmed].some((character) => {
|
|
331
11
|
const code = character.charCodeAt(0);
|
|
@@ -343,45 +23,3 @@ function safeLinkHref(href: string): string | undefined {
|
|
|
343
23
|
return undefined;
|
|
344
24
|
}
|
|
345
25
|
}
|
|
346
|
-
|
|
347
|
-
function trimBareUrl(href: string): string {
|
|
348
|
-
let trimmed = href;
|
|
349
|
-
|
|
350
|
-
while (shouldTrimBareUrlSuffix(trimmed)) {
|
|
351
|
-
trimmed = trimmed.slice(0, -1);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return trimmed;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function shouldTrimBareUrlSuffix(href: string): boolean {
|
|
358
|
-
const last = href.at(-1);
|
|
359
|
-
if (!last) return false;
|
|
360
|
-
if (/[.,;:!?]/.test(last)) return true;
|
|
361
|
-
return last === ")" && closingParensExceedOpening(href);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function closingParensExceedOpening(value: string): boolean {
|
|
365
|
-
let balance = 0;
|
|
366
|
-
for (const char of value) {
|
|
367
|
-
if (char === "(") balance += 1;
|
|
368
|
-
if (char === ")") balance -= 1;
|
|
369
|
-
}
|
|
370
|
-
return balance < 0;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function mergeRanges(ranges: TextRange[]): TextRange[] {
|
|
374
|
-
const sorted = ranges
|
|
375
|
-
.filter((range) => range.end > range.start)
|
|
376
|
-
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
377
|
-
const merged: TextRange[] = [];
|
|
378
|
-
for (const range of sorted) {
|
|
379
|
-
const previous = merged.at(-1);
|
|
380
|
-
if (!previous || range.start > previous.end) {
|
|
381
|
-
merged.push({ ...range });
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
previous.end = Math.max(previous.end, range.end);
|
|
385
|
-
}
|
|
386
|
-
return merged;
|
|
387
|
-
}
|
|
@@ -33,3 +33,18 @@ export function memoryRecallContent(context: TranscriptViewTurnContext) {
|
|
|
33
33
|
const parsed = memoryRecallContentSchema.safeParse(context.content);
|
|
34
34
|
return parsed.success ? parsed.data : undefined;
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
const userCustomizationContentSchema = z.object({ text: z.string() }).strict();
|
|
38
|
+
|
|
39
|
+
/** Parse the saved user customization that the runtime added to one Turn. */
|
|
40
|
+
export function userCustomizationContent(context: TranscriptViewTurnContext) {
|
|
41
|
+
if (
|
|
42
|
+
context.pluginName !== "junior" ||
|
|
43
|
+
context.kind !== "user_customization" ||
|
|
44
|
+
context.version !== 1
|
|
45
|
+
) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const parsed = userCustomizationContentSchema.safeParse(context.content);
|
|
49
|
+
return parsed.success ? parsed.data : undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
userInputRequestSchema,
|
|
3
|
+
type ConversationReportEvent,
|
|
4
|
+
type UserInputRequest,
|
|
5
|
+
} from "@sentry/junior/api/schema";
|
|
6
|
+
|
|
7
|
+
/** Model-facing name of Junior's tool that asks a person for input. */
|
|
8
|
+
const REQUEST_USER_INPUT_TOOL_NAME = "requestUserInput";
|
|
9
|
+
|
|
10
|
+
/** A request from Junior that still waits for an answer. */
|
|
11
|
+
export type OpenUserInputRequest = UserInputRequest & { toolCallId: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find the latest `requestUserInput` call that no user message answers yet.
|
|
15
|
+
* Any later user message is the answer, whether it is a submitted form or
|
|
16
|
+
* typed text.
|
|
17
|
+
*
|
|
18
|
+
* Reporting splits one call over events: the assistant event has the input
|
|
19
|
+
* and status `running`, and the later result event has status `completed`
|
|
20
|
+
* without the input. Keep the input by tool call id until the result.
|
|
21
|
+
*/
|
|
22
|
+
export function openUserInputRequest(
|
|
23
|
+
events: readonly ConversationReportEvent[] | undefined,
|
|
24
|
+
): OpenUserInputRequest | undefined {
|
|
25
|
+
const inputs = new Map<string, unknown>();
|
|
26
|
+
const completed = new Set<string>();
|
|
27
|
+
let open: OpenUserInputRequest | undefined;
|
|
28
|
+
for (const event of events ?? []) {
|
|
29
|
+
const data = event.data;
|
|
30
|
+
if (data.type === "message" && data.role === "user") {
|
|
31
|
+
open = undefined;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (data.type !== "tool_calls") continue;
|
|
35
|
+
for (const call of data.calls) {
|
|
36
|
+
if (call.name !== REQUEST_USER_INPUT_TOOL_NAME) continue;
|
|
37
|
+
if (call.input !== undefined) inputs.set(call.toolCallId, call.input);
|
|
38
|
+
if (call.status === "error" && open?.toolCallId === call.toolCallId) {
|
|
39
|
+
open = undefined;
|
|
40
|
+
}
|
|
41
|
+
// Open a request once, when its result first arrives. A repeated
|
|
42
|
+
// result must not reopen a request that a user message answered.
|
|
43
|
+
if (call.status !== "completed" || completed.has(call.toolCallId)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
completed.add(call.toolCallId);
|
|
47
|
+
const request = userInputRequestSchema.safeParse(
|
|
48
|
+
inputs.get(call.toolCallId),
|
|
49
|
+
);
|
|
50
|
+
if (request.success) {
|
|
51
|
+
open = { ...request.data, toolCallId: call.toolCallId };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return open;
|
|
56
|
+
}
|
package/src/client/format.ts
CHANGED
|
@@ -360,6 +360,21 @@ export type MessageSummary = {
|
|
|
360
360
|
total: number;
|
|
361
361
|
};
|
|
362
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Return the person who wrote one transcript user message. Child agent input
|
|
365
|
+
* has no person. Other messages fall back to the conversation actor.
|
|
366
|
+
*/
|
|
367
|
+
export function transcriptMessageActor(
|
|
368
|
+
conversation: ConversationTranscript,
|
|
369
|
+
message: TranscriptViewMessage,
|
|
370
|
+
): ActorIdentity | undefined {
|
|
371
|
+
if (transcriptRoleKind(message.role) !== "user") return undefined;
|
|
372
|
+
if (message.agentInvocationId) return undefined;
|
|
373
|
+
if (actorLabel(message.actorIdentity)) return message.actorIdentity;
|
|
374
|
+
if (actorLabel(conversation.actorIdentity)) return conversation.actorIdentity;
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
363
378
|
/** Return the best available actor label for one transcript message. */
|
|
364
379
|
export function transcriptMessageActorLabel(
|
|
365
380
|
conversation: ConversationTranscript,
|
|
@@ -371,11 +386,7 @@ export function transcriptMessageActorLabel(
|
|
|
371
386
|
// The conversation actor started the child agent but did not write its
|
|
372
387
|
// input, so do not fall back to that actor here.
|
|
373
388
|
if (message.agentInvocationId) return "Agent invocation";
|
|
374
|
-
return (
|
|
375
|
-
actorLabel(message.actorIdentity) ??
|
|
376
|
-
actorLabel(conversation.actorIdentity) ??
|
|
377
|
-
"User"
|
|
378
|
-
);
|
|
389
|
+
return actorLabel(transcriptMessageActor(conversation, message)) ?? "User";
|
|
379
390
|
}
|
|
380
391
|
if (kind === "system") return "System";
|
|
381
392
|
if (kind === "tool") return "Tool";
|
|
@@ -786,6 +797,11 @@ export function peoplePath(email: string): string {
|
|
|
786
797
|
return `/people/${encodeURIComponent(email)}`;
|
|
787
798
|
}
|
|
788
799
|
|
|
800
|
+
/** Build the dashboard route for one memory's details. */
|
|
801
|
+
export function memoryPath(memoryId: string): string {
|
|
802
|
+
return `/memories/${encodeURIComponent(memoryId)}`;
|
|
803
|
+
}
|
|
804
|
+
|
|
789
805
|
/** Build the canonical detail route for a persisted public location. */
|
|
790
806
|
export function locationPath(locationId: string): string {
|
|
791
807
|
return `/system/locations/${encodeURIComponent(locationId)}`;
|
|
@@ -796,9 +812,16 @@ export function workspacePath(workspaceId: string): string {
|
|
|
796
812
|
return `/system/workspaces/${encodeURIComponent(workspaceId)}`;
|
|
797
813
|
}
|
|
798
814
|
|
|
799
|
-
/**
|
|
800
|
-
export
|
|
801
|
-
|
|
815
|
+
/** Automation page tabs. Overview is the bare Automation path. */
|
|
816
|
+
export type AutomationTab = "overview" | "runs" | "versions" | "edit";
|
|
817
|
+
|
|
818
|
+
/** Build the route for one Automation page tab, or its edit mode. */
|
|
819
|
+
export function automationPath(
|
|
820
|
+
automationId: string,
|
|
821
|
+
tab: AutomationTab = "overview",
|
|
822
|
+
): string {
|
|
823
|
+
const base = `/automations/${encodeURIComponent(automationId)}`;
|
|
824
|
+
return tab === "overview" ? base : `${base}/${tab}`;
|
|
802
825
|
}
|
|
803
826
|
|
|
804
827
|
function normalizeLanguage(language: string | undefined): BundledLanguage {
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { useEffect, useState, type FormEvent } from "react";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
USER_CUSTOMIZATION_MAX_CHARS,
|
|
6
|
+
userCustomizationSchema,
|
|
7
|
+
} from "@sentry/junior/api/schema";
|
|
4
8
|
import {
|
|
5
9
|
dashboardIdentitySchema,
|
|
6
10
|
type DashboardIdentity,
|
|
7
11
|
} from "../../api/schema";
|
|
8
12
|
import { Button } from "../components/ui/button";
|
|
9
13
|
import { Input } from "../components/ui/input";
|
|
14
|
+
import { Textarea } from "../components/ui/textarea";
|
|
10
15
|
import { Field, FieldDescription, FieldLabel } from "../components/ui/field";
|
|
11
16
|
import { InlineError } from "../components/InlineError";
|
|
12
17
|
import { Card } from "../components/layout/Card";
|
|
13
18
|
import { useDocumentTitle } from "../documentTitle";
|
|
14
|
-
import { patch } from "../http";
|
|
19
|
+
import { DashboardApiError, fetchDashboardJson, patch, put } from "../http";
|
|
15
20
|
import { dashboardContainerClass } from "../styles";
|
|
16
21
|
import type { DashboardCoreData } from "../types";
|
|
17
22
|
|
|
18
23
|
const dashboardCoreQueryKey = ["dashboard", "core"] as const;
|
|
24
|
+
const userCustomizationQueryKey = ["dashboard", "user-customization"] as const;
|
|
25
|
+
const USER_CUSTOMIZATION_PATH = "/api/user-customization";
|
|
19
26
|
|
|
20
27
|
type SettingsPageProps = {
|
|
21
28
|
identity: DashboardIdentity;
|
|
@@ -58,7 +65,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
|
|
|
58
65
|
<section className="mx-auto w-full max-w-3xl">
|
|
59
66
|
<h1 className="m-0 text-2xl font-bold">Settings</h1>
|
|
60
67
|
<p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
|
|
61
|
-
Manage
|
|
68
|
+
Manage your profile and preferences.
|
|
62
69
|
</p>
|
|
63
70
|
|
|
64
71
|
<form className="mt-6" onSubmit={submit}>
|
|
@@ -97,7 +104,100 @@ export function SettingsPage({ identity }: SettingsPageProps) {
|
|
|
97
104
|
</div>
|
|
98
105
|
</Card>
|
|
99
106
|
</form>
|
|
107
|
+
<CustomizationForm />
|
|
100
108
|
</section>
|
|
101
109
|
</div>
|
|
102
110
|
);
|
|
103
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Let the user save text that changes how the agent replies to them. The
|
|
115
|
+
* server returns 404 when the app does not enable this experiment.
|
|
116
|
+
*/
|
|
117
|
+
function CustomizationForm() {
|
|
118
|
+
const queryClient = useQueryClient();
|
|
119
|
+
const [text, setText] = useState("");
|
|
120
|
+
const customizationQuery = useQuery({
|
|
121
|
+
queryKey: userCustomizationQueryKey,
|
|
122
|
+
queryFn: ({ signal }) =>
|
|
123
|
+
fetchDashboardJson(
|
|
124
|
+
userCustomizationSchema,
|
|
125
|
+
USER_CUSTOMIZATION_PATH,
|
|
126
|
+
signal,
|
|
127
|
+
),
|
|
128
|
+
retry: false,
|
|
129
|
+
});
|
|
130
|
+
const savedText = customizationQuery.data?.customization;
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (savedText !== undefined) setText(savedText);
|
|
133
|
+
}, [savedText]);
|
|
134
|
+
const save = useMutation({
|
|
135
|
+
mutationFn: (customization: string) =>
|
|
136
|
+
put(userCustomizationSchema, USER_CUSTOMIZATION_PATH, { customization }),
|
|
137
|
+
onSuccess: (updated) => {
|
|
138
|
+
queryClient.setQueryData(userCustomizationQueryKey, updated);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
if (
|
|
142
|
+
customizationQuery.error instanceof DashboardApiError &&
|
|
143
|
+
customizationQuery.error.status === 404
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (savedText === undefined) {
|
|
148
|
+
return customizationQuery.isError ? (
|
|
149
|
+
<InlineError className="mt-6">
|
|
150
|
+
Could not load your customization.
|
|
151
|
+
</InlineError>
|
|
152
|
+
) : null;
|
|
153
|
+
}
|
|
154
|
+
const canSave =
|
|
155
|
+
text.trim() !== savedText &&
|
|
156
|
+
text.trim().length <= USER_CUSTOMIZATION_MAX_CHARS &&
|
|
157
|
+
!save.isPending;
|
|
158
|
+
|
|
159
|
+
function submit(event: FormEvent<HTMLFormElement>) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
if (canSave) save.mutate(text.trim());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<form className="mt-6" onSubmit={submit}>
|
|
166
|
+
<Card className="mb-0" padding="md" variant="raised">
|
|
167
|
+
<h2 className="m-0 text-lg font-bold">Customization</h2>
|
|
168
|
+
<Field className="mt-5">
|
|
169
|
+
<FieldLabel htmlFor="user-customization">
|
|
170
|
+
How should replies to you change?
|
|
171
|
+
</FieldLabel>
|
|
172
|
+
<Textarea
|
|
173
|
+
id="user-customization"
|
|
174
|
+
maxLength={USER_CUSTOMIZATION_MAX_CHARS}
|
|
175
|
+
onChange={(event) => {
|
|
176
|
+
setText(event.target.value);
|
|
177
|
+
if (save.isError || save.isSuccess) save.reset();
|
|
178
|
+
}}
|
|
179
|
+
placeholder="Keep replies short and simple."
|
|
180
|
+
value={text}
|
|
181
|
+
/>
|
|
182
|
+
<FieldDescription>
|
|
183
|
+
Added to each message you send. Do not put secrets here. (
|
|
184
|
+
{text.trim().length}/{USER_CUSTOMIZATION_MAX_CHARS})
|
|
185
|
+
</FieldDescription>
|
|
186
|
+
</Field>
|
|
187
|
+
<div className="mt-5 flex items-center gap-3">
|
|
188
|
+
<Button disabled={!canSave} type="submit">
|
|
189
|
+
{save.isPending ? "Saving…" : "Save customization"}
|
|
190
|
+
</Button>
|
|
191
|
+
{save.isSuccess ? (
|
|
192
|
+
<p className="m-0 text-sm text-emerald-300">Changes saved.</p>
|
|
193
|
+
) : null}
|
|
194
|
+
{save.isError ? (
|
|
195
|
+
<InlineError>
|
|
196
|
+
Could not save your customization. Try again.
|
|
197
|
+
</InlineError>
|
|
198
|
+
) : null}
|
|
199
|
+
</div>
|
|
200
|
+
</Card>
|
|
201
|
+
</form>
|
|
202
|
+
);
|
|
203
|
+
}
|