@sentry/junior-dashboard 0.269.0 → 0.271.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 +2 -2
- package/dist/client.js +78 -75
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/client/App.tsx +103 -65
- package/src/client/components/AGENTS.md +30 -59
- package/src/client/components/AutomationCard.tsx +2 -1
- package/src/client/components/ProfileMenu.tsx +0 -24
- package/src/client/components/UserInputForm.tsx +143 -39
- package/src/client/components/layout/PageHeader.tsx +25 -10
- package/src/client/conversations/AGENTS.md +12 -37
- package/src/client/conversations/ConversationBrief.tsx +55 -36
- package/src/client/conversations/ConversationComposer.tsx +27 -8
- package/src/client/conversations/ConversationEventLog.tsx +1 -0
- package/src/client/conversations/ConversationHomeList.tsx +43 -26
- package/src/client/conversations/ConversationPage.tsx +137 -40
- package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
- package/src/client/conversations/ConversationSidebar.tsx +55 -25
- package/src/client/conversations/ConversationTranscript.tsx +3 -2
- package/src/client/conversations/ConversationWatches.tsx +4 -1
- package/src/client/conversations/ConversationWorkspace.tsx +43 -25
- package/src/client/conversations/EventDetails.tsx +6 -9
- package/src/client/conversations/PendingMailboxStack.tsx +5 -2
- package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +9 -3
- package/src/client/conversations/TranscriptMessageView.tsx +6 -5
- package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
- package/src/client/conversations/TranscriptRows.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
- package/src/client/conversations/TranscriptToolView.tsx +2 -1
- package/src/client/conversations/TranscriptView.tsx +6 -6
- package/src/client/conversations/conversationOutbox.ts +13 -3
- package/src/client/conversations/conversationRoutes.ts +30 -3
- package/src/client/conversations/conversationSections.ts +36 -8
- package/src/client/conversations/eventTranscript.ts +1 -0
- package/src/client/conversations/transcriptBottomPinning.ts +171 -81
- package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
- package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
- package/src/client/display-text/DisplayText.tsx +68 -0
- package/src/client/display-text/emoji.ts +54 -0
- package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
- package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
- package/src/client/display-text/mentions.tsx +148 -0
- package/src/client/display-text/plainText.ts +29 -0
- package/src/client/pages/automations/AGENTS.md +6 -25
- package/src/client/pages/automations/AutomationFilters.tsx +6 -6
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
- package/src/client/pages/code/RepositoryTabs.tsx +6 -1
- package/src/client/pages/memory/MemoryDetails.tsx +5 -13
- package/src/client/pages/settings/ConnectionsPage.tsx +198 -0
- package/src/client/pages/settings/McpSettingsPage.tsx +77 -0
- package/src/client/pages/settings/PersonalTokensPage.tsx +198 -0
- package/src/client/pages/{SettingsPage.tsx → settings/SettingsPage.tsx} +54 -62
- package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
- package/src/client/primaryNav.ts +1 -1
- package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
- package/src/client/review-desk/ReviewDeskPage.tsx +202 -241
- package/src/client/review-desk/ReviewDeskSidebar.tsx +253 -0
- package/src/client/review-desk/ReviewItemActions.tsx +6 -2
- package/src/client/review-desk/ReviewItemCard.tsx +58 -17
- package/src/client/review-desk/ReviewItemContext.tsx +108 -0
- package/src/client/review-desk/ReviewItemDetails.tsx +153 -0
- package/src/client/review-desk/ReviewItemView.tsx +159 -0
- package/src/client/review-desk/UnsentAnswers.tsx +148 -0
- package/src/client/review-desk/queries.ts +4 -2
- package/src/client/review-desk/reviewItemFormat.ts +83 -1
- package/src/client/components/charts/AGENTS.md +0 -15
- package/src/client/conversations/TranscriptText.tsx +0 -47
- package/src/client/conversations/conversationActivityPreview.ts +0 -16
- package/src/client/conversations/transcriptMentions.tsx +0 -134
- package/src/client/pages/ConnectionsPage.tsx +0 -207
- package/src/client/pages/PersonalTokensPage.tsx +0 -205
- package/src/client/pages/system/AGENTS.md +0 -12
- package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const 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
4
|
/**
|
|
5
|
-
* Keep a
|
|
5
|
+
* Keep a link destination only when it is an absolute `http:`,
|
|
6
6
|
* `https:`, or `mailto:` URL. Return undefined to drop the link.
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
8
|
+
export function safeHref(href: string): string | undefined {
|
|
9
9
|
const trimmed = href.trim();
|
|
10
10
|
const hasControlCharacter = [...trimmed].some((character) => {
|
|
11
11
|
const code = character.charCodeAt(0);
|
|
@@ -23,3 +23,16 @@ export function safeTranscriptHref(href: string): string | undefined {
|
|
|
23
23
|
return undefined;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/** Slack writes links as `<https://example.com>` or `<https://example.com|label>`. */
|
|
28
|
+
export const SLACK_LINK_RE = /<((?:https?|mailto):[^\s|<>]+)(?:\|([^<>]*))?>/g;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Rewrite labeled Slack links as Markdown links. Markdown already reads a
|
|
32
|
+
* bare `<https://example.com>` as a link.
|
|
33
|
+
*/
|
|
34
|
+
export function slackLinksToMarkdown(text: string): string {
|
|
35
|
+
return text.replace(SLACK_LINK_RE, (match, url: string, label?: string) =>
|
|
36
|
+
label?.trim() ? `[${label.trim()}](${url})` : match,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -11,37 +11,53 @@ import {
|
|
|
11
11
|
TableHeader,
|
|
12
12
|
TableRow,
|
|
13
13
|
} from "../components/ui/table";
|
|
14
|
-
import { cn } from "../styles";
|
|
15
14
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
15
|
+
HighlightText,
|
|
16
|
+
useTranscriptSearch,
|
|
17
|
+
} from "../conversations/transcriptSearch";
|
|
18
|
+
import { cn } from "../styles";
|
|
19
|
+
import { ANCHOR_CLASS, safeHref, slackLinksToMarkdown } from "./links";
|
|
20
|
+
import { renderTextTokens } from "./mentions";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Render
|
|
23
|
+
* Render display text Markdown as readable prose.
|
|
24
24
|
*
|
|
25
25
|
* The text is CommonMark plus GFM, with a hard break for each newline. Agent
|
|
26
|
-
* replies also use Slack-style `•` bullets
|
|
27
|
-
* those render as lists and headings
|
|
26
|
+
* replies also use Slack-style `•` bullets, `<url|label>` links, and
|
|
27
|
+
* bold-only section labels, so those render as lists, links, and headings
|
|
28
|
+
* too. Inline mode keeps only inline formatting, so the text fits in one
|
|
29
|
+
* line or in a button.
|
|
28
30
|
*/
|
|
29
|
-
export function
|
|
30
|
-
|
|
31
|
+
export function MarkdownText(props: {
|
|
32
|
+
chat?: boolean;
|
|
33
|
+
inline?: boolean;
|
|
31
34
|
text: string;
|
|
32
35
|
}): ReactNode {
|
|
33
36
|
if (!props.text.trim()) return null;
|
|
37
|
+
const source = normalizeSlackBullets(slackLinksToMarkdown(props.text));
|
|
38
|
+
|
|
39
|
+
if (props.inline) {
|
|
40
|
+
return (
|
|
41
|
+
<Markdown
|
|
42
|
+
components={INLINE_COMPONENTS}
|
|
43
|
+
remarkPlugins={[remarkGfm]}
|
|
44
|
+
urlTransform={safeHref}
|
|
45
|
+
>
|
|
46
|
+
{source}
|
|
47
|
+
</Markdown>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
34
50
|
|
|
35
51
|
return (
|
|
36
52
|
<div
|
|
37
53
|
className={cn(
|
|
38
54
|
"min-w-0 break-words text-dashboard-text [overflow-wrap:anywhere]",
|
|
39
|
-
props.
|
|
55
|
+
props.chat
|
|
40
56
|
? "font-sans text-base leading-relaxed"
|
|
41
57
|
: "font-sans text-sm leading-6 md:leading-7",
|
|
42
58
|
)}
|
|
43
59
|
>
|
|
44
|
-
<div className={cn("grid min-w-0", props.
|
|
60
|
+
<div className={cn("grid min-w-0", props.chat ? "gap-3" : "gap-2")}>
|
|
45
61
|
<Markdown
|
|
46
62
|
components={COMPONENTS}
|
|
47
63
|
remarkPlugins={[
|
|
@@ -50,9 +66,9 @@ export function TranscriptMarkdown(props: {
|
|
|
50
66
|
remarkBareUrlStops,
|
|
51
67
|
remarkSectionLabels,
|
|
52
68
|
]}
|
|
53
|
-
urlTransform={
|
|
69
|
+
urlTransform={safeHref}
|
|
54
70
|
>
|
|
55
|
-
{
|
|
71
|
+
{source}
|
|
56
72
|
</Markdown>
|
|
57
73
|
</div>
|
|
58
74
|
</div>
|
|
@@ -255,13 +271,61 @@ const COMPONENTS: Components = {
|
|
|
255
271
|
),
|
|
256
272
|
};
|
|
257
273
|
|
|
258
|
-
// Text nodes can hold Slack mention tokens and
|
|
274
|
+
// Text nodes can hold Slack mention tokens, emoji codes, and search matches.
|
|
259
275
|
function renderText(children: ReactNode): ReactNode {
|
|
260
276
|
return Children.map(children, (child, index) =>
|
|
261
|
-
typeof child === "string" ?
|
|
277
|
+
typeof child === "string" ? renderTextTokens(child, `t-${index}`) : child,
|
|
262
278
|
);
|
|
263
279
|
}
|
|
264
280
|
|
|
281
|
+
// Inline mode ends each block with a newline. A one-line container shows it
|
|
282
|
+
// as a space, and a `whitespace-pre-wrap` container keeps the line break.
|
|
283
|
+
function InlineBlock(props: { children?: ReactNode }) {
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
{renderText(props.children)}
|
|
287
|
+
{"\n"}
|
|
288
|
+
</>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function InlineCell(props: { children?: ReactNode }) {
|
|
293
|
+
return <>{renderText(props.children)} </>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function InlineContainer(props: { children?: ReactNode }) {
|
|
297
|
+
return <>{props.children}</>;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const INLINE_COMPONENTS: Components = {
|
|
301
|
+
// A link cannot sit in a button or in another link, so keep the label.
|
|
302
|
+
a: ({ children }) => <>{renderText(children)}</>,
|
|
303
|
+
blockquote: InlineContainer,
|
|
304
|
+
code: COMPONENTS.code,
|
|
305
|
+
del: COMPONENTS.del,
|
|
306
|
+
em: COMPONENTS.em,
|
|
307
|
+
h1: InlineBlock,
|
|
308
|
+
h2: InlineBlock,
|
|
309
|
+
h3: InlineBlock,
|
|
310
|
+
h4: InlineBlock,
|
|
311
|
+
h5: InlineBlock,
|
|
312
|
+
h6: InlineBlock,
|
|
313
|
+
hr: () => "\n",
|
|
314
|
+
img: ({ alt }) => alt ?? null,
|
|
315
|
+
li: InlineBlock,
|
|
316
|
+
ol: InlineContainer,
|
|
317
|
+
p: InlineBlock,
|
|
318
|
+
pre: InlineContainer,
|
|
319
|
+
strong: COMPONENTS.strong,
|
|
320
|
+
table: InlineContainer,
|
|
321
|
+
tbody: InlineContainer,
|
|
322
|
+
td: InlineCell,
|
|
323
|
+
th: InlineCell,
|
|
324
|
+
thead: InlineContainer,
|
|
325
|
+
tr: InlineBlock,
|
|
326
|
+
ul: InlineContainer,
|
|
327
|
+
};
|
|
328
|
+
|
|
265
329
|
function alignClass(align: unknown): string | undefined {
|
|
266
330
|
if (align === "center") return "text-center";
|
|
267
331
|
if (align === "right") return "text-right";
|
|
@@ -269,13 +333,13 @@ function alignClass(align: unknown): string | undefined {
|
|
|
269
333
|
}
|
|
270
334
|
|
|
271
335
|
function TranscriptAnchor(props: { children: ReactNode; href?: string }) {
|
|
272
|
-
// `
|
|
336
|
+
// `safeHref` removes unsafe destinations. Keep their label text.
|
|
273
337
|
if (!props.href) return <>{renderText(props.children)}</>;
|
|
274
338
|
|
|
275
339
|
const opensNewTab = /^https?:/i.test(props.href);
|
|
276
340
|
return (
|
|
277
341
|
<a
|
|
278
|
-
className={
|
|
342
|
+
className={ANCHOR_CLASS}
|
|
279
343
|
href={props.href}
|
|
280
344
|
rel={opensNewTab ? "noreferrer" : undefined}
|
|
281
345
|
target={opensNewTab ? "_blank" : undefined}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import type { ActorIdentity } from "@sentry/junior/api/schema";
|
|
4
|
+
|
|
5
|
+
import { HighlightText } from "../conversations/transcriptSearch";
|
|
6
|
+
import { peoplePath } from "../format";
|
|
7
|
+
import { cn } from "../styles";
|
|
8
|
+
import { replaceSlackEmoji } from "./emoji";
|
|
9
|
+
import { ANCHOR_CLASS } from "./links";
|
|
10
|
+
|
|
11
|
+
type Mention = {
|
|
12
|
+
end: number;
|
|
13
|
+
id: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
sigil: "@" | "#";
|
|
16
|
+
start: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type MentionContext = {
|
|
20
|
+
links: boolean;
|
|
21
|
+
people: ReadonlyMap<string, ActorIdentity>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Matches the user and channel tokens that core keeps in message text.
|
|
25
|
+
const SLACK_MENTION_RE = /<([@#])([A-Z0-9]+)(?:\|([^<>]*))?>/g;
|
|
26
|
+
|
|
27
|
+
const MentionPeople = createContext<MentionContext>({
|
|
28
|
+
links: true,
|
|
29
|
+
people: new Map(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Share the people that the API resolved for one display text. */
|
|
33
|
+
export function MentionPeopleProvider(props: {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
links: boolean;
|
|
36
|
+
mentions: readonly ActorIdentity[];
|
|
37
|
+
}) {
|
|
38
|
+
return (
|
|
39
|
+
<MentionPeople.Provider
|
|
40
|
+
value={{ links: props.links, people: mentionPeople(props.mentions) }}
|
|
41
|
+
>
|
|
42
|
+
{props.children}
|
|
43
|
+
</MentionPeople.Provider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render one text run with Slack `<@U123>` and `<#C123|name>` tokens as
|
|
49
|
+
* readable mentions, Slack `:name:` codes as emoji, and search matches
|
|
50
|
+
* highlighted. It reads people from the nearest `MentionPeopleProvider`.
|
|
51
|
+
*/
|
|
52
|
+
export function renderTextTokens(text: string, keyBase: string): ReactNode[] {
|
|
53
|
+
const nodes: ReactNode[] = [];
|
|
54
|
+
let cursor = 0;
|
|
55
|
+
for (const mention of findMentions(text)) {
|
|
56
|
+
if (mention.start > cursor) {
|
|
57
|
+
nodes.push(
|
|
58
|
+
<HighlightText
|
|
59
|
+
key={`${keyBase}-m-${cursor}`}
|
|
60
|
+
text={replaceSlackEmoji(text.slice(cursor, mention.start))}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
nodes.push(
|
|
65
|
+
<MentionView key={`${keyBase}-m-${mention.start}`} mention={mention} />,
|
|
66
|
+
);
|
|
67
|
+
cursor = mention.end;
|
|
68
|
+
}
|
|
69
|
+
if (cursor < text.length) {
|
|
70
|
+
nodes.push(
|
|
71
|
+
<HighlightText
|
|
72
|
+
key={`${keyBase}-m-${cursor}`}
|
|
73
|
+
text={replaceSlackEmoji(text.slice(cursor))}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return nodes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Return one text run as a string, with the names and emoji it renders. */
|
|
81
|
+
export function plainTextTokens(
|
|
82
|
+
text: string,
|
|
83
|
+
mentions: readonly ActorIdentity[],
|
|
84
|
+
): string {
|
|
85
|
+
const people = mentionPeople(mentions);
|
|
86
|
+
let result = "";
|
|
87
|
+
let cursor = 0;
|
|
88
|
+
for (const mention of findMentions(text)) {
|
|
89
|
+
result += replaceSlackEmoji(text.slice(cursor, mention.start));
|
|
90
|
+
result += mentionLabel(mention, people);
|
|
91
|
+
cursor = mention.end;
|
|
92
|
+
}
|
|
93
|
+
return result + replaceSlackEmoji(text.slice(cursor));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mentionPeople(
|
|
97
|
+
mentions: readonly ActorIdentity[],
|
|
98
|
+
): Map<string, ActorIdentity> {
|
|
99
|
+
const people = new Map<string, ActorIdentity>();
|
|
100
|
+
for (const person of mentions) {
|
|
101
|
+
if (person.slackUserId) people.set(person.slackUserId, person);
|
|
102
|
+
}
|
|
103
|
+
return people;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mentionLabel(
|
|
107
|
+
mention: Mention,
|
|
108
|
+
people: ReadonlyMap<string, ActorIdentity>,
|
|
109
|
+
): string {
|
|
110
|
+
const { id, label, sigil } = mention;
|
|
111
|
+
const person = sigil === "@" ? people.get(id) : undefined;
|
|
112
|
+
const name = person?.fullName ?? person?.slackUserName ?? label ?? id;
|
|
113
|
+
return `${sigil}${name.replace(/^[@#]/, "")}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findMentions(text: string): Mention[] {
|
|
117
|
+
return [...text.matchAll(SLACK_MENTION_RE)].map((match) => ({
|
|
118
|
+
end: match.index + match[0].length,
|
|
119
|
+
id: match[2]!,
|
|
120
|
+
label: match[3]?.trim() || undefined,
|
|
121
|
+
sigil: match[1] as "@" | "#",
|
|
122
|
+
start: match.index,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function MentionView(props: { mention: Mention }) {
|
|
127
|
+
const { links, people } = useContext(MentionPeople);
|
|
128
|
+
const { id, sigil } = props.mention;
|
|
129
|
+
const person = sigil === "@" ? people.get(id) : undefined;
|
|
130
|
+
const text = <HighlightText text={mentionLabel(props.mention, people)} />;
|
|
131
|
+
if (links && person?.email) {
|
|
132
|
+
return (
|
|
133
|
+
<Link
|
|
134
|
+
// Previews sit in cards that disable pointer events on their text.
|
|
135
|
+
className={cn("pointer-events-auto", ANCHOR_CLASS)}
|
|
136
|
+
title={id}
|
|
137
|
+
to={peoplePath(person.email)}
|
|
138
|
+
>
|
|
139
|
+
{text}
|
|
140
|
+
</Link>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
<span className="font-medium text-dashboard-accent-text" title={id}>
|
|
145
|
+
{text}
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ActorIdentity } from "@sentry/junior/api/schema";
|
|
2
|
+
|
|
3
|
+
import { plainTextTokens } from "./mentions";
|
|
4
|
+
import { SLACK_LINK_RE } from "./links";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Return display text as one plain string for places that cannot hold
|
|
8
|
+
* markup, such as a `title` attribute. It shows the same names and emoji as
|
|
9
|
+
* `DisplayText`, without the Markdown syntax.
|
|
10
|
+
*/
|
|
11
|
+
export function displayPlainText(
|
|
12
|
+
text: string,
|
|
13
|
+
mentions: readonly ActorIdentity[] = [],
|
|
14
|
+
): string {
|
|
15
|
+
return plainTextTokens(stripMarkdown(text), mentions);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripMarkdown(text: string): string {
|
|
19
|
+
return text
|
|
20
|
+
.replace(SLACK_LINK_RE, (_, url: string, label?: string) =>
|
|
21
|
+
label?.trim() ? label.trim() : url,
|
|
22
|
+
)
|
|
23
|
+
.replace(/\[([^\]]+)\]\([^\s)]+(?:\s+"[^"]*")?\)/g, "$1")
|
|
24
|
+
.replace(/(^|\s)([*_~`]{1,3})(?=\S)/g, "$1")
|
|
25
|
+
.replace(/([*_~`]{1,3})(?=\s|$|[.,!?;:])/g, "")
|
|
26
|
+
.replace(/^\s*(?:#{1,6}|[-+*]|\d+\.)\s+/gm, "")
|
|
27
|
+
.replace(/\s+/g, " ")
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
@@ -1,34 +1,15 @@
|
|
|
1
1
|
# Automation pages
|
|
2
2
|
|
|
3
|
-
## Boundaries
|
|
4
|
-
|
|
5
3
|
- Core owns validation, edit access, creator-only rules, revision checks,
|
|
6
|
-
Schedule previews, and the Event catalog.
|
|
7
|
-
|
|
4
|
+
Schedule previews, and the Event catalog. Show core errors. Do not repeat
|
|
5
|
+
these rules in the page.
|
|
8
6
|
- Preview requests never save or run work.
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- Send only changed fields. Keep unsupported triggers and stored
|
|
14
|
-
Destinations unless the user replaces them. Keep outcome order.
|
|
15
|
-
- New messages can target only the current Destination or the creator.
|
|
16
|
-
- A public reader sees outcomes as read-only and cannot turn on creator
|
|
17
|
-
credentials. Warn that an instruction or event selector change switches to
|
|
18
|
-
system credentials.
|
|
19
|
-
- Pause, Resume, and Delete are creator-only. Hide them from other viewers.
|
|
7
|
+
- Send only changed fields. Keep unsupported triggers and stored Destinations
|
|
8
|
+
unless the user replaces them. Keep outcome order.
|
|
9
|
+
- Warn that an instruction or event selector change switches to system
|
|
10
|
+
credentials.
|
|
20
11
|
- Query refreshes must not replace an unsaved draft. After a stale save, let
|
|
21
12
|
the user keep their changes or load the latest version.
|
|
22
|
-
- Block navigation with unsaved changes.
|
|
23
13
|
- Lifecycle actions and "Make active" read the latest revision first.
|
|
24
|
-
|
|
25
|
-
## Page rules
|
|
26
|
-
|
|
27
|
-
- Each Automation has one page at `/automations/:id`. Keep the list query
|
|
28
|
-
string on the way back. Drop tab-only params: `result`, `run`, `version`.
|
|
29
14
|
- `AutomationRecentRuns` shows terminal runs only. Do not infer queued or
|
|
30
15
|
running work from terminal records.
|
|
31
|
-
- Delete keeps errors in the dialog and blocks dismissal while pending.
|
|
32
|
-
|
|
33
|
-
Browser journeys: `e2e/automation-editor.spec.ts` and
|
|
34
|
-
`e2e/automations.spec.ts`.
|
|
@@ -57,13 +57,13 @@ export function AutomationFilters(props: {
|
|
|
57
57
|
key: "type",
|
|
58
58
|
label: "Type",
|
|
59
59
|
value: filters.type === "all" ? "" : filters.type,
|
|
60
|
-
|
|
60
|
+
valueLabel: filters.type === "scheduled" ? "Scheduled" : "Event",
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
key: "creator",
|
|
64
64
|
label: "Creator",
|
|
65
65
|
value: filters.creator,
|
|
66
|
-
|
|
66
|
+
valueLabel:
|
|
67
67
|
data?.creators.find((option) => option.value === filters.creator)
|
|
68
68
|
?.label ?? filters.creator,
|
|
69
69
|
},
|
|
@@ -71,7 +71,7 @@ export function AutomationFilters(props: {
|
|
|
71
71
|
key: "destination",
|
|
72
72
|
label: "Destination",
|
|
73
73
|
value: filters.destination,
|
|
74
|
-
|
|
74
|
+
valueLabel:
|
|
75
75
|
data?.destinations.find(
|
|
76
76
|
(option) => option.value === filters.destination,
|
|
77
77
|
)?.label ?? filters.destination,
|
|
@@ -80,7 +80,7 @@ export function AutomationFilters(props: {
|
|
|
80
80
|
key: "state",
|
|
81
81
|
label: "State",
|
|
82
82
|
value: filters.state === "all" ? "" : filters.state,
|
|
83
|
-
|
|
83
|
+
valueLabel: stateOptions.find((option) => option.value === filters.state)
|
|
84
84
|
?.label,
|
|
85
85
|
},
|
|
86
86
|
].filter((filter) => filter.value);
|
|
@@ -163,11 +163,11 @@ export function AutomationFilters(props: {
|
|
|
163
163
|
key={filter.key}
|
|
164
164
|
aria-label={`Remove ${filter.key} filter`}
|
|
165
165
|
className="min-h-9 max-w-full text-xs"
|
|
166
|
-
title={`${filter.label}: ${filter.
|
|
166
|
+
title={`${filter.label}: ${filter.valueLabel}`}
|
|
167
167
|
onClick={() => onChange(filter.key, "")}
|
|
168
168
|
>
|
|
169
169
|
<span className="truncate">
|
|
170
|
-
{filter.label}: {filter.
|
|
170
|
+
{filter.label}: {filter.valueLabel}
|
|
171
171
|
</span>
|
|
172
172
|
<X aria-hidden="true" className="shrink-0" size={12} />
|
|
173
173
|
</Button>
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type TimeRangeDays,
|
|
16
16
|
} from "../../components/controls/TimeRangeSelector";
|
|
17
17
|
import { Card } from "../../components/layout/Card";
|
|
18
|
-
import {
|
|
18
|
+
import { DisplayText } from "../../display-text/DisplayText";
|
|
19
19
|
import { automationPath, formatCostSummary, peoplePath } from "../../format";
|
|
20
20
|
import { pathWithSearch } from "../../searchParams";
|
|
21
21
|
import { dashboardInlineLinkClass } from "../../styles";
|
|
@@ -76,7 +76,7 @@ function AutomationSettings(props: { automation: AutomationSummary }) {
|
|
|
76
76
|
<div className="min-w-0 [&>section:first-child]:border-t-0 [&>section:first-child]:pt-0">
|
|
77
77
|
<AutomationFormSection title="What to do">
|
|
78
78
|
<div className="text-base leading-relaxed [overflow-wrap:anywhere]">
|
|
79
|
-
<
|
|
79
|
+
<DisplayText
|
|
80
80
|
mentions={automation.mentions ?? []}
|
|
81
81
|
text={automation.instruction}
|
|
82
82
|
/>
|
|
@@ -29,6 +29,7 @@ import { InlineError } from "../../components/InlineError";
|
|
|
29
29
|
import { PageContentSkeleton } from "../../components/PageContentSkeleton";
|
|
30
30
|
import { StatusChip } from "../../components/StatusChip";
|
|
31
31
|
import { Card } from "../../components/layout/Card";
|
|
32
|
+
import { DisplayText } from "../../display-text/DisplayText";
|
|
32
33
|
import { formatRelativeTime } from "../../format";
|
|
33
34
|
import { DashboardApiError, fetchDashboardJson, post } from "../../http";
|
|
34
35
|
import { cn } from "../../styles";
|
|
@@ -364,7 +365,10 @@ function fieldValue(
|
|
|
364
365
|
return definition.title ?? "First line of the instruction";
|
|
365
366
|
case "instruction":
|
|
366
367
|
return (
|
|
367
|
-
<
|
|
368
|
+
<DisplayText
|
|
369
|
+
mentions={automation.mentions}
|
|
370
|
+
text={definition.instruction}
|
|
371
|
+
/>
|
|
368
372
|
);
|
|
369
373
|
case "when":
|
|
370
374
|
if (version.kind === "scheduled") {
|
|
@@ -431,7 +435,7 @@ function LineDiff(props: { after: string; before: string }) {
|
|
|
431
435
|
{line.kind === "added" ? "Added: " : "Removed: "}
|
|
432
436
|
</span>
|
|
433
437
|
)}
|
|
434
|
-
{line.
|
|
438
|
+
{line.value || " "}
|
|
435
439
|
</span>
|
|
436
440
|
</div>
|
|
437
441
|
))}
|
|
@@ -439,7 +443,7 @@ function LineDiff(props: { after: string; before: string }) {
|
|
|
439
443
|
);
|
|
440
444
|
}
|
|
441
445
|
|
|
442
|
-
type DiffLine = { kind: "added" | "removed" | "same";
|
|
446
|
+
type DiffLine = { kind: "added" | "removed" | "same"; value: string };
|
|
443
447
|
|
|
444
448
|
/** Line diff from the longest common subsequence. Instructions stay short. */
|
|
445
449
|
function diffLines(before: string, after: string): DiffLine[] {
|
|
@@ -461,17 +465,17 @@ function diffLines(before: string, after: string): DiffLine[] {
|
|
|
461
465
|
let j = 0;
|
|
462
466
|
while (i < a.length && j < b.length) {
|
|
463
467
|
if (a[i] === b[j]) {
|
|
464
|
-
lines.push({ kind: "same",
|
|
468
|
+
lines.push({ kind: "same", value: a[i]! });
|
|
465
469
|
i++;
|
|
466
470
|
j++;
|
|
467
471
|
} else if (lengths[i + 1]![j]! >= lengths[i]![j + 1]!) {
|
|
468
|
-
lines.push({ kind: "removed",
|
|
472
|
+
lines.push({ kind: "removed", value: a[i++]! });
|
|
469
473
|
} else {
|
|
470
|
-
lines.push({ kind: "added",
|
|
474
|
+
lines.push({ kind: "added", value: b[j++]! });
|
|
471
475
|
}
|
|
472
476
|
}
|
|
473
|
-
while (i < a.length) lines.push({ kind: "removed",
|
|
474
|
-
while (j < b.length) lines.push({ kind: "added",
|
|
477
|
+
while (i < a.length) lines.push({ kind: "removed", value: a[i++]! });
|
|
478
|
+
while (j < b.length) lines.push({ kind: "added", value: b[j++]! });
|
|
475
479
|
return lines;
|
|
476
480
|
}
|
|
477
481
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../../components/controls/TimeRangeSelector";
|
|
17
17
|
import { Card } from "../../components/layout/Card";
|
|
18
18
|
import { ConversationHomeList } from "../../conversations/ConversationHomeList";
|
|
19
|
+
import { attentionFirst } from "../../conversations/conversationSections";
|
|
19
20
|
import { buildConversations, getDashboardTimeZone } from "../../format";
|
|
20
21
|
import { useDebouncedValue } from "../../debouncedValue";
|
|
21
22
|
import { useWaitingConversationIds } from "../../review-desk/queries";
|
|
@@ -52,7 +53,11 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
|
|
|
52
53
|
</EmptyTelemetry>
|
|
53
54
|
) : (
|
|
54
55
|
<ConversationHomeList
|
|
55
|
-
conversations={pageItems(
|
|
56
|
+
conversations={pageItems(
|
|
57
|
+
attentionFirst(conversations, waitingConversationIds),
|
|
58
|
+
page,
|
|
59
|
+
PAGE_SIZE,
|
|
60
|
+
)}
|
|
56
61
|
emptyLabel={
|
|
57
62
|
search
|
|
58
63
|
? "No conversations match this search."
|
|
@@ -18,16 +18,12 @@ import {
|
|
|
18
18
|
TooltipContent,
|
|
19
19
|
TooltipTrigger,
|
|
20
20
|
} from "../../components/ui/tooltip";
|
|
21
|
-
import {
|
|
21
|
+
import { DisplayText } from "../../display-text/DisplayText";
|
|
22
22
|
import type {
|
|
23
23
|
PluginUserPageRecord,
|
|
24
24
|
PluginUserPageRecordAction,
|
|
25
25
|
} from "../user/pluginUserPageData";
|
|
26
26
|
|
|
27
|
-
// The memory plugin API does not resolve mentions yet, so memory text shows
|
|
28
|
-
// Slack ids instead of names.
|
|
29
|
-
const NO_MEMORY_MENTIONS: readonly [] = [];
|
|
30
|
-
|
|
31
27
|
// The heading, origin, and scope rows already show these values.
|
|
32
28
|
const SUMMARIZED_METADATA = new Set([
|
|
33
29
|
"Learned",
|
|
@@ -81,15 +77,11 @@ export function MemoryDetails(props: {
|
|
|
81
77
|
{record ? (
|
|
82
78
|
<>
|
|
83
79
|
<div className="grid gap-3 text-sm leading-relaxed">
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/>
|
|
80
|
+
{/* The memory plugin API does not resolve mentions yet, so
|
|
81
|
+
memory text shows Slack ids instead of names. */}
|
|
82
|
+
<DisplayText text={record.title} />
|
|
88
83
|
{record.description ? (
|
|
89
|
-
<
|
|
90
|
-
mentions={NO_MEMORY_MENTIONS}
|
|
91
|
-
text={record.description}
|
|
92
|
-
/>
|
|
84
|
+
<DisplayText text={record.description} />
|
|
93
85
|
) : null}
|
|
94
86
|
</div>
|
|
95
87
|
<MemoryFacts record={record} />
|