@sentry/junior-dashboard 0.258.0 → 0.260.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/README.md +5 -3
- package/dist/app.js +88 -37
- package/dist/chunks/github-light-QHW3AEMR.js +1 -0
- package/dist/client.js +86 -50
- package/dist/index.js +88 -37
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +0 -1
- package/src/client/code.tsx +10 -4
- package/src/client/components/ProfileMenu.tsx +3 -0
- package/src/client/components/README.md +12 -2
- package/src/client/components/ThemeSwitcher.tsx +48 -0
- package/src/client/components/charts/ActivityChart.tsx +6 -7
- package/src/client/components/charts/InputCacheChart.tsx +1 -1
- package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
- package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
- package/src/client/components/charts/cache-input.ts +11 -3
- package/src/client/components/layout/DashboardHeader.tsx +2 -9
- package/src/client/conversations/ConversationBrief.tsx +35 -9
- package/src/client/conversations/ConversationHomeList.tsx +7 -1
- package/src/client/conversations/ConversationMeta.tsx +19 -7
- package/src/client/conversations/ConversationTranscript.tsx +8 -11
- package/src/client/conversations/EventDetails.tsx +9 -5
- package/src/client/conversations/README.md +6 -0
- package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
- package/src/client/conversations/TranscriptEventView.tsx +48 -9
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
- package/src/client/conversations/TranscriptText.tsx +28 -20
- package/src/client/conversations/conversationActivityPreview.ts +6 -0
- package/src/client/conversations/eventTranscript.ts +4 -1
- package/src/client/conversations/transcriptMentions.tsx +27 -10
- package/src/client/pages/admin/AdminPage.tsx +14 -5
- package/src/client/pages/admin/ServiceAccountsSection.tsx +231 -0
- package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
- package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
- package/src/client/pages/code/CodeActivityChart.tsx +4 -4
- package/src/client/pages/dev/ComponentsPage.tsx +7 -3
- package/src/client/pages/dev/ConversationFixture.tsx +29 -0
- package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
- package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
- package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
- package/src/client/pages/people/ContributionGrid.tsx +8 -8
- package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
- package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
- package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
- package/src/client/pages/system/PluginBarChart.tsx +12 -6
- package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
- package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
- package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
- package/src/client/pages/system/WorkspaceList.tsx +6 -1
- package/src/client/pages/system/WorkspacesPage.tsx +18 -12
- package/src/client/pages/system/workspaceDraft.ts +8 -0
- package/src/client/theme.ts +52 -0
- package/src/client/types.ts +8 -1
- package/src/mock-reporting/fixtures.ts +48 -35
- package/src/shell.ts +4 -1
- package/src/tailwind.css +109 -0
- package/src/theme.ts +49 -0
- package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { memo } from "react";
|
|
2
|
+
import type { ActorIdentity } from "@sentry/junior/api/schema";
|
|
2
3
|
|
|
3
4
|
import { HighlightedCode } from "../code";
|
|
4
5
|
import { parseMarkdownBlocks, transcriptRoleKind } from "../format";
|
|
5
6
|
import { TranscriptMarkdown } from "./TranscriptMarkdown";
|
|
7
|
+
import { MentionPeopleProvider } from "./transcriptMentions";
|
|
6
8
|
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Render transcript prose as Markdown with explicit fenced code blocks.
|
|
11
|
+
* `mentions` are the people that the API resolved for this text.
|
|
12
|
+
*/
|
|
8
13
|
export const TranscriptText = memo(function TranscriptText(props: {
|
|
14
|
+
mentions: readonly ActorIdentity[];
|
|
9
15
|
role?: string;
|
|
10
16
|
text: string;
|
|
11
17
|
}) {
|
|
@@ -13,27 +19,29 @@ export const TranscriptText = memo(function TranscriptText(props: {
|
|
|
13
19
|
const blocks = parseMarkdownBlocks(props.text);
|
|
14
20
|
|
|
15
21
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
<MentionPeopleProvider mentions={props.mentions}>
|
|
23
|
+
<div className="grid min-w-0 gap-2">
|
|
24
|
+
{blocks.map((block, index) => {
|
|
25
|
+
if (block.language === "markdown" && !block.fenced) {
|
|
26
|
+
return (
|
|
27
|
+
<TranscriptMarkdown
|
|
28
|
+
compact={roleKind === "assistant" || roleKind === "user"}
|
|
29
|
+
key={index}
|
|
30
|
+
text={block.code}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
36
|
+
<div
|
|
37
|
+
className="min-w-0 overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5"
|
|
22
38
|
key={index}
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
>
|
|
40
|
+
<HighlightedCode code={block.code} language={block.language} />
|
|
41
|
+
</div>
|
|
25
42
|
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<div
|
|
30
|
-
className="min-w-0 overflow-hidden rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5"
|
|
31
|
-
key={index}
|
|
32
|
-
>
|
|
33
|
-
<HighlightedCode code={block.code} language={block.language} />
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
})}
|
|
37
|
-
</div>
|
|
43
|
+
})}
|
|
44
|
+
</div>
|
|
45
|
+
</MentionPeopleProvider>
|
|
38
46
|
);
|
|
39
47
|
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
// Slack writes links as `<https://example.com>` or `<https://example.com|label>`.
|
|
2
|
+
const SLACK_LINK_RE = /<((?:https?|mailto):[^\s|<>]+)(?:\|([^<>]*))?>/g;
|
|
3
|
+
|
|
1
4
|
/** Reduce message Markdown to compact plain text for conversation previews. */
|
|
2
5
|
export function formatConversationActivityPreview(text: string): string {
|
|
3
6
|
return text
|
|
7
|
+
.replace(SLACK_LINK_RE, (_, url: string, label?: string) =>
|
|
8
|
+
label?.trim() ? label.trim() : url,
|
|
9
|
+
)
|
|
4
10
|
.replace(/\[([^\]]+)\]\([^\s)]+(?:\s+"[^"]*")?\)/g, "$1")
|
|
5
11
|
.replace(/(^|\s)([*_~`]{1,3})(?=\S)/g, "$1")
|
|
6
12
|
.replace(/([*_~`]{1,3})(?=\s|$|[.,!?;:])/g, "")
|
|
@@ -37,6 +37,7 @@ function eventMessage(
|
|
|
37
37
|
role,
|
|
38
38
|
sourceSeq: event.seq,
|
|
39
39
|
timestamp: eventTimestamp(event),
|
|
40
|
+
...(event.mentions ? { mentions: event.mentions } : undefined),
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -213,12 +214,12 @@ export function transcriptMessagesFromEvents(
|
|
|
213
214
|
...(data.actorIdentity
|
|
214
215
|
? { actorIdentity: data.actorIdentity }
|
|
215
216
|
: undefined),
|
|
216
|
-
...(data.mentions ? { mentions: data.mentions } : undefined),
|
|
217
217
|
...(data.eventType ? { eventType: data.eventType } : undefined),
|
|
218
218
|
eventObjectType: data.eventObjectType,
|
|
219
219
|
...(data.trustedSummary
|
|
220
220
|
? { trustedSummary: data.trustedSummary }
|
|
221
221
|
: undefined),
|
|
222
|
+
...(data.events ? { events: data.events } : undefined),
|
|
222
223
|
...(data.explicitMention !== undefined
|
|
223
224
|
? { explicitMention: data.explicitMention }
|
|
224
225
|
: undefined),
|
|
@@ -388,6 +389,7 @@ export function transcriptMessagesFromEvents(
|
|
|
388
389
|
eventMessage(event, "system", [
|
|
389
390
|
{
|
|
390
391
|
type: "structured_event",
|
|
392
|
+
...(event.mentions ? { mentions: event.mentions } : undefined),
|
|
391
393
|
namespace: data.namespace,
|
|
392
394
|
name: data.name,
|
|
393
395
|
version: data.version,
|
|
@@ -415,6 +417,7 @@ export function transcriptMessagesFromEvents(
|
|
|
415
417
|
eventMessage(event, "system", [
|
|
416
418
|
{
|
|
417
419
|
type: "context_event",
|
|
420
|
+
...(event.mentions ? { mentions: event.mentions } : undefined),
|
|
418
421
|
event:
|
|
419
422
|
data.type === "handoff"
|
|
420
423
|
? {
|
|
@@ -3,7 +3,7 @@ import { Link } from "react-router";
|
|
|
3
3
|
import type { ActorIdentity } from "@sentry/junior/api/schema";
|
|
4
4
|
|
|
5
5
|
import { peoplePath } from "../format";
|
|
6
|
-
import
|
|
6
|
+
import { cn } from "../styles";
|
|
7
7
|
import { TRANSCRIPT_ANCHOR_CLASS } from "./transcriptMarkdownLinks";
|
|
8
8
|
import { HighlightText } from "./transcriptSearch";
|
|
9
9
|
|
|
@@ -22,16 +22,17 @@ const TranscriptMentionPeople = createContext<
|
|
|
22
22
|
ReadonlyMap<string, ActorIdentity>
|
|
23
23
|
>(new Map());
|
|
24
24
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Share the people that the API resolved for one block of display text.
|
|
27
|
+
* Use `TranscriptText` or `MentionText`, which require the API mentions.
|
|
28
|
+
*/
|
|
29
|
+
export function MentionPeopleProvider(props: {
|
|
27
30
|
children: ReactNode;
|
|
28
|
-
|
|
31
|
+
mentions: readonly ActorIdentity[];
|
|
29
32
|
}) {
|
|
30
33
|
const people = new Map<string, ActorIdentity>();
|
|
31
|
-
for (const
|
|
32
|
-
|
|
33
|
-
if (person.slackUserId) people.set(person.slackUserId, person);
|
|
34
|
-
}
|
|
34
|
+
for (const person of props.mentions) {
|
|
35
|
+
if (person.slackUserId) people.set(person.slackUserId, person);
|
|
35
36
|
}
|
|
36
37
|
return (
|
|
37
38
|
<TranscriptMentionPeople.Provider value={people}>
|
|
@@ -40,7 +41,22 @@ export function TranscriptMentionPeopleProvider(props: {
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/** Render text with
|
|
44
|
+
/** Render one line of display text with the people the API resolved for it. */
|
|
45
|
+
export function MentionText(props: {
|
|
46
|
+
mentions: readonly ActorIdentity[];
|
|
47
|
+
text: string;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<MentionPeopleProvider mentions={props.mentions}>
|
|
51
|
+
{renderMentionText(props.text, "mention-text")}
|
|
52
|
+
</MentionPeopleProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render text with Slack `<@U123>` and `<#C123|name>` tokens as readable
|
|
58
|
+
* mentions. It reads people from the nearest `MentionPeopleProvider`.
|
|
59
|
+
*/
|
|
44
60
|
export function renderMentionText(text: string, keyBase: string): ReactNode[] {
|
|
45
61
|
const nodes: ReactNode[] = [];
|
|
46
62
|
let cursor = 0;
|
|
@@ -88,7 +104,8 @@ function MentionView(props: { mention: TranscriptMention }) {
|
|
|
88
104
|
if (person?.email) {
|
|
89
105
|
return (
|
|
90
106
|
<Link
|
|
91
|
-
|
|
107
|
+
// Previews sit in cards that disable pointer events on their text.
|
|
108
|
+
className={cn("pointer-events-auto", TRANSCRIPT_ANCHOR_CLASS)}
|
|
92
109
|
title={id}
|
|
93
110
|
to={peoplePath(person.email)}
|
|
94
111
|
>
|
|
@@ -6,15 +6,23 @@ import { Card } from "../../components/layout/Card";
|
|
|
6
6
|
import { PageHeader } from "../../components/layout/PageHeader";
|
|
7
7
|
import { dashboardContainerClass } from "../../styles";
|
|
8
8
|
import { pluginUserPagePath } from "../user/PluginUserPage";
|
|
9
|
+
import {
|
|
10
|
+
ServiceAccountsSection,
|
|
11
|
+
useServiceAccounts,
|
|
12
|
+
} from "./ServiceAccountsSection";
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
|
-
*
|
|
15
|
+
* Show service accounts and admin-only plugin pages to a Junior admin.
|
|
12
16
|
*
|
|
13
|
-
* The server hides admin pages from other viewers and
|
|
14
|
-
* so this page only arranges
|
|
17
|
+
* The server hides admin pages and service accounts from other viewers and
|
|
18
|
+
* refuses their requests, so this page only arranges them.
|
|
15
19
|
*/
|
|
16
20
|
export function AdminPage(props: { pages: PluginUserPageLink[] }) {
|
|
17
21
|
const adminPages = props.pages.filter((page) => page.navigation === "admin");
|
|
22
|
+
const serviceAccounts = useServiceAccounts();
|
|
23
|
+
const nothingToSet =
|
|
24
|
+
adminPages.length === 0 &&
|
|
25
|
+
serviceAccounts.data?.serviceAccounts.length === 0;
|
|
18
26
|
return (
|
|
19
27
|
<div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
|
|
20
28
|
<section className="mx-auto grid w-full max-w-3xl gap-6">
|
|
@@ -28,7 +36,8 @@ export function AdminPage(props: { pages: PluginUserPageLink[] }) {
|
|
|
28
36
|
}
|
|
29
37
|
title="Admin"
|
|
30
38
|
/>
|
|
31
|
-
{
|
|
39
|
+
<ServiceAccountsSection accountsQuery={serviceAccounts} />
|
|
40
|
+
{nothingToSet ? (
|
|
32
41
|
<Card padding="md">
|
|
33
42
|
<div className="flex items-center gap-4">
|
|
34
43
|
<div className="grid size-10 shrink-0 place-items-center rounded-md border border-dashboard-border bg-dashboard-fill-soft text-dashboard-text-muted">
|
|
@@ -39,7 +48,7 @@ export function AdminPage(props: { pages: PluginUserPageLink[] }) {
|
|
|
39
48
|
</p>
|
|
40
49
|
</div>
|
|
41
50
|
</Card>
|
|
42
|
-
) : (
|
|
51
|
+
) : adminPages.length === 0 ? null : (
|
|
43
52
|
<div className="grid gap-3">
|
|
44
53
|
{adminPages.map((page) => (
|
|
45
54
|
<Link
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutation,
|
|
3
|
+
useQuery,
|
|
4
|
+
useQueryClient,
|
|
5
|
+
type UseQueryResult,
|
|
6
|
+
} from "@tanstack/react-query";
|
|
7
|
+
import { KeyRound } from "lucide-react";
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import {
|
|
10
|
+
disconnectServiceAccountResponseSchema,
|
|
11
|
+
serviceAccountListSchema,
|
|
12
|
+
startServiceAccountSignInResponseSchema,
|
|
13
|
+
type serviceAccountSchema,
|
|
14
|
+
} from "@sentry/junior/api/schema";
|
|
15
|
+
import { getDashboardAgentName } from "../../agentName";
|
|
16
|
+
import { Button } from "../../components/Button";
|
|
17
|
+
import { InlineError } from "../../components/InlineError";
|
|
18
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
19
|
+
import { Card } from "../../components/layout/Card";
|
|
20
|
+
import { formatRelativeTime } from "../../format";
|
|
21
|
+
import { del, fetchDashboardJson, post } from "../../http";
|
|
22
|
+
|
|
23
|
+
type ServiceAccount = z.infer<typeof serviceAccountSchema>;
|
|
24
|
+
|
|
25
|
+
const serviceAccountsQueryKey = ["dashboard", "service-accounts"] as const;
|
|
26
|
+
|
|
27
|
+
function serviceAccountPath(provider: string): string {
|
|
28
|
+
return `/api/service-accounts/${encodeURIComponent(provider)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Load the plugins that support a service account, with their status. */
|
|
32
|
+
export function useServiceAccounts() {
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: serviceAccountsQueryKey,
|
|
35
|
+
queryFn: ({ signal }) =>
|
|
36
|
+
fetchDashboardJson(
|
|
37
|
+
serviceAccountListSchema,
|
|
38
|
+
"/api/service-accounts",
|
|
39
|
+
signal,
|
|
40
|
+
),
|
|
41
|
+
// OAuth finishes in another tab. Refresh when the admin returns.
|
|
42
|
+
refetchOnWindowFocus: true,
|
|
43
|
+
retry: false,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function statusText(account: ServiceAccount): string {
|
|
48
|
+
const usage = !account.userOAuth
|
|
49
|
+
? `Everyone uses this account. People cannot connect their own ${account.displayName} account.`
|
|
50
|
+
: `People who have not connected their own ${account.displayName} account use this account.`;
|
|
51
|
+
if (!account.connected) {
|
|
52
|
+
const blocked = !account.userOAuth
|
|
53
|
+
? ` ${account.displayName} is not available until you connect it.`
|
|
54
|
+
: "";
|
|
55
|
+
return `Not connected.${blocked} ${usage} Sign in as the shared account, not as yourself.`;
|
|
56
|
+
}
|
|
57
|
+
const by = account.connectedBy ?? "the CLI";
|
|
58
|
+
return `Connected by ${by} ${formatRelativeTime(account.connectedAt)}. ${usage}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Connect and disconnect the shared provider accounts that Junior uses for
|
|
63
|
+
* people who have not connected their own account.
|
|
64
|
+
*/
|
|
65
|
+
export function ServiceAccountsSection(props: {
|
|
66
|
+
accountsQuery: UseQueryResult<z.infer<typeof serviceAccountListSchema>>;
|
|
67
|
+
}) {
|
|
68
|
+
const { accountsQuery } = props;
|
|
69
|
+
const queryClient = useQueryClient();
|
|
70
|
+
const connectMutation = useMutation({
|
|
71
|
+
mutationFn: (input: { provider: string; popup: Window | null }) =>
|
|
72
|
+
post(
|
|
73
|
+
startServiceAccountSignInResponseSchema,
|
|
74
|
+
serviceAccountPath(input.provider),
|
|
75
|
+
{},
|
|
76
|
+
),
|
|
77
|
+
onSuccess: ({ authorizationUrl }, { popup }) => {
|
|
78
|
+
// A blocked popup returns null, so use this tab. A tab the admin
|
|
79
|
+
// closed means they cancelled; keep them on the dashboard.
|
|
80
|
+
if (!popup) {
|
|
81
|
+
window.location.assign(authorizationUrl);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (popup.closed) return;
|
|
85
|
+
popup.opener = null;
|
|
86
|
+
popup.location.href = authorizationUrl;
|
|
87
|
+
},
|
|
88
|
+
onError: (_error, { popup }) => {
|
|
89
|
+
popup?.close();
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const disconnectMutation = useMutation({
|
|
93
|
+
mutationFn: (account: ServiceAccount) =>
|
|
94
|
+
del(
|
|
95
|
+
disconnectServiceAccountResponseSchema,
|
|
96
|
+
serviceAccountPath(account.provider),
|
|
97
|
+
),
|
|
98
|
+
onSuccess: () =>
|
|
99
|
+
queryClient.invalidateQueries({ queryKey: serviceAccountsQueryKey }),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const accounts = accountsQuery.data?.serviceAccounts ?? [];
|
|
103
|
+
if (accountsQuery.isSuccess && accounts.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const busy = connectMutation.isPending || disconnectMutation.isPending;
|
|
107
|
+
const pendingProvider = connectMutation.isPending
|
|
108
|
+
? connectMutation.variables?.provider
|
|
109
|
+
: disconnectMutation.isPending
|
|
110
|
+
? disconnectMutation.variables?.provider
|
|
111
|
+
: undefined;
|
|
112
|
+
const listFailed = !accountsQuery.data && accountsQuery.isError;
|
|
113
|
+
const displayedError = connectMutation.isError
|
|
114
|
+
? "Could not start the sign-in. Try again."
|
|
115
|
+
: disconnectMutation.isError
|
|
116
|
+
? "Could not disconnect the account. Try again."
|
|
117
|
+
: listFailed
|
|
118
|
+
? "Could not load service accounts. Try again."
|
|
119
|
+
: undefined;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Card as="section" padding="md">
|
|
123
|
+
<h2 className="m-0 font-display text-base font-medium text-dashboard-text">
|
|
124
|
+
Service accounts
|
|
125
|
+
</h2>
|
|
126
|
+
<p className="mt-1 mb-4 text-sm text-dashboard-text-muted">
|
|
127
|
+
A shared account that Junior uses for a plugin. It can do only what its
|
|
128
|
+
plugin allows, usually read. Everyone who can talk to{" "}
|
|
129
|
+
{getDashboardAgentName()} can use it, so give it only access that all of
|
|
130
|
+
them may have.
|
|
131
|
+
</p>
|
|
132
|
+
{displayedError ? (
|
|
133
|
+
<InlineError className={listFailed ? undefined : "mb-4"}>
|
|
134
|
+
{displayedError}
|
|
135
|
+
</InlineError>
|
|
136
|
+
) : null}
|
|
137
|
+
{listFailed ? null : accountsQuery.isPending ? (
|
|
138
|
+
<p className="m-0 text-sm text-dashboard-text-muted">
|
|
139
|
+
Loading service accounts…
|
|
140
|
+
</p>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
{accounts.map((account) => (
|
|
144
|
+
<div
|
|
145
|
+
className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
|
|
146
|
+
key={account.provider}
|
|
147
|
+
>
|
|
148
|
+
<KeyRound
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
className="shrink-0 text-dashboard-accent"
|
|
151
|
+
size={16}
|
|
152
|
+
/>
|
|
153
|
+
<div className="min-w-0 flex-1">
|
|
154
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
155
|
+
<p className="m-0 truncate text-sm font-semibold">
|
|
156
|
+
{account.displayName}
|
|
157
|
+
</p>
|
|
158
|
+
<StatusChip
|
|
159
|
+
size="compact"
|
|
160
|
+
tone={
|
|
161
|
+
account.connected
|
|
162
|
+
? "success"
|
|
163
|
+
: !account.userOAuth
|
|
164
|
+
? "warning"
|
|
165
|
+
: "neutral"
|
|
166
|
+
}
|
|
167
|
+
>
|
|
168
|
+
{account.connected ? "connected" : "not connected"}
|
|
169
|
+
</StatusChip>
|
|
170
|
+
{!account.userOAuth ? (
|
|
171
|
+
<StatusChip size="compact" tone="neutral">
|
|
172
|
+
shared only
|
|
173
|
+
</StatusChip>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
<p className="mt-1 mb-0 break-words text-xs text-dashboard-text-muted">
|
|
177
|
+
{statusText(account)}
|
|
178
|
+
</p>
|
|
179
|
+
<p className="mt-1 mb-0 break-words font-mono text-xs text-dashboard-text-faint">
|
|
180
|
+
{account.tools.join(", ")}
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
{account.connected ? (
|
|
184
|
+
<Button
|
|
185
|
+
aria-busy={
|
|
186
|
+
disconnectMutation.isPending &&
|
|
187
|
+
pendingProvider === account.provider
|
|
188
|
+
}
|
|
189
|
+
aria-label={`Disconnect the ${account.displayName} service account`}
|
|
190
|
+
disabled={busy}
|
|
191
|
+
onClick={() => {
|
|
192
|
+
connectMutation.reset();
|
|
193
|
+
disconnectMutation.mutate(account);
|
|
194
|
+
}}
|
|
195
|
+
tone="danger"
|
|
196
|
+
>
|
|
197
|
+
Disconnect
|
|
198
|
+
</Button>
|
|
199
|
+
) : null}
|
|
200
|
+
<Button
|
|
201
|
+
aria-busy={
|
|
202
|
+
connectMutation.isPending &&
|
|
203
|
+
pendingProvider === account.provider
|
|
204
|
+
}
|
|
205
|
+
aria-label={`${account.connected ? "Reconnect" : "Connect"} the ${account.displayName} service account`}
|
|
206
|
+
disabled={busy}
|
|
207
|
+
onClick={() => {
|
|
208
|
+
disconnectMutation.reset();
|
|
209
|
+
// Open the tab during the click so popup blockers allow
|
|
210
|
+
// it, then point it at the provider.
|
|
211
|
+
const popup = window.open("about:blank", "_blank");
|
|
212
|
+
connectMutation.mutate({
|
|
213
|
+
provider: account.provider,
|
|
214
|
+
popup,
|
|
215
|
+
});
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{connectMutation.isPending &&
|
|
219
|
+
pendingProvider === account.provider
|
|
220
|
+
? "Connecting…"
|
|
221
|
+
: account.connected
|
|
222
|
+
? "Reconnect"
|
|
223
|
+
: "Connect"}
|
|
224
|
+
</Button>
|
|
225
|
+
</div>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</Card>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { Card } from "../../components/layout/Card";
|
|
19
19
|
import { formatCostSummary } from "../../format";
|
|
20
20
|
|
|
21
|
-
const COST_COLOR = "
|
|
21
|
+
const COST_COLOR = "var(--color-cyan-300)";
|
|
22
22
|
|
|
23
23
|
/** Render linked conversation spend for completed automation executions. */
|
|
24
24
|
export function AutomationCostChart(props: {
|
|
@@ -103,7 +103,7 @@ export function AutomationCostChart(props: {
|
|
|
103
103
|
format={(value) => formatCostSummary({ total: value })}
|
|
104
104
|
layout={layout}
|
|
105
105
|
maximum={maximum}
|
|
106
|
-
stroke="
|
|
106
|
+
stroke="var(--color-slate-200)"
|
|
107
107
|
/>
|
|
108
108
|
<ActivityChartDateLabels
|
|
109
109
|
dates={days.map((day) => day.date)}
|
|
@@ -88,7 +88,10 @@ export function AutomationDetailsDrawer(props: {
|
|
|
88
88
|
<h3 className="mt-0 mb-2 text-sm font-medium text-dashboard-text-muted">
|
|
89
89
|
What to do
|
|
90
90
|
</h3>
|
|
91
|
-
<TranscriptText
|
|
91
|
+
<TranscriptText
|
|
92
|
+
mentions={automation.mentions ?? []}
|
|
93
|
+
text={automation.instruction}
|
|
94
|
+
/>
|
|
92
95
|
</div>
|
|
93
96
|
<AutomationLifecycle
|
|
94
97
|
key={`${automation.kind}:${automation.id}`}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { Card } from "../../components/layout/Card";
|
|
19
19
|
import { formatActivityChartAverage } from "../../format";
|
|
20
20
|
|
|
21
|
-
const EXECUTION_COLOR = "
|
|
21
|
+
const EXECUTION_COLOR = "var(--color-amber-400)";
|
|
22
22
|
|
|
23
23
|
/** Render completed automation executions over a trailing window. */
|
|
24
24
|
export function AutomationExecutionChart(props: {
|
|
@@ -95,7 +95,7 @@ export function AutomationExecutionChart(props: {
|
|
|
95
95
|
format={formatActivityChartAverage}
|
|
96
96
|
layout={layout}
|
|
97
97
|
maximum={maximum}
|
|
98
|
-
stroke="
|
|
98
|
+
stroke="var(--color-slate-200)"
|
|
99
99
|
/>
|
|
100
100
|
<ActivityChartDateLabels
|
|
101
101
|
dates={days.map((day) => day.date)}
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
import { Card } from "../../components/layout/Card";
|
|
16
16
|
|
|
17
17
|
const series = [
|
|
18
|
-
{ color: "
|
|
19
|
-
{ color: "
|
|
20
|
-
{ color: "
|
|
18
|
+
{ color: "var(--color-emerald-300)", key: "completed", label: "Completed" },
|
|
19
|
+
{ color: "var(--color-rose-300)", key: "failed", label: "Failed" },
|
|
20
|
+
{ color: "var(--color-amber-300)", key: "blocked", label: "Blocked" },
|
|
21
21
|
] as const;
|
|
22
22
|
|
|
23
23
|
/** Render one automation's terminal executions by status over a trailing window. */
|
|
@@ -19,9 +19,9 @@ import { ChartLegend } from "../../components/charts/ChartLegend";
|
|
|
19
19
|
import { Card } from "../../components/layout/Card";
|
|
20
20
|
import { formatActivityChartAverage } from "../../format";
|
|
21
21
|
|
|
22
|
-
const CREATED_COLOR = "
|
|
23
|
-
const MERGED_COLOR = "
|
|
24
|
-
const CLOSED_COLOR = "
|
|
22
|
+
const CREATED_COLOR = "var(--color-cyan-300)";
|
|
23
|
+
const MERGED_COLOR = "var(--color-emerald-300)";
|
|
24
|
+
const CLOSED_COLOR = "var(--color-rose-400)";
|
|
25
25
|
|
|
26
26
|
/** Render created, merged, and closed code changes over a trailing window. */
|
|
27
27
|
export function CodeActivityChart(props: {
|
|
@@ -143,7 +143,7 @@ export function CodeActivityChart(props: {
|
|
|
143
143
|
format={formatActivityChartAverage}
|
|
144
144
|
layout={layout}
|
|
145
145
|
maximum={maximum}
|
|
146
|
-
stroke="
|
|
146
|
+
stroke="var(--color-slate-200)"
|
|
147
147
|
/>
|
|
148
148
|
<ActivityChartDateLabels
|
|
149
149
|
dates={days.map((day) => day.date)}
|
|
@@ -50,6 +50,7 @@ import { StatCard } from "../../components/metrics/StatCard";
|
|
|
50
50
|
import { StatusChip } from "../../components/StatusChip";
|
|
51
51
|
import { StatusDot } from "../../components/StatusDot";
|
|
52
52
|
import { TextArea, TextInput } from "../../components/TextInput";
|
|
53
|
+
import { ThemeSwitcher } from "../../components/ThemeSwitcher";
|
|
53
54
|
import { TranscriptMarkdown } from "../../conversations/TranscriptMarkdown";
|
|
54
55
|
import { TranscriptText } from "../../conversations/TranscriptText";
|
|
55
56
|
import { TranscriptMessageShell } from "../../conversations/TranscriptMessageShell";
|
|
@@ -231,7 +232,6 @@ function FoundationsGalleryPage() {
|
|
|
231
232
|
<Fixture title="Conversation navigation">
|
|
232
233
|
<DashboardChromeProvider>
|
|
233
234
|
<DashboardHeader
|
|
234
|
-
compact
|
|
235
235
|
mobileNavigationOpen={navigationOpen}
|
|
236
236
|
navItems={[
|
|
237
237
|
{ key: "code", label: "Code", to: "/code" },
|
|
@@ -301,6 +301,9 @@ function FoundationsGalleryPage() {
|
|
|
301
301
|
</Drawer>
|
|
302
302
|
) : null}
|
|
303
303
|
</Fixture>
|
|
304
|
+
<Fixture title="Theme">
|
|
305
|
+
<ThemeSwitcher className="max-w-64" />
|
|
306
|
+
</Fixture>
|
|
304
307
|
<Fixture title="Color tokens">
|
|
305
308
|
<div className="grid gap-4">
|
|
306
309
|
<TokenSwatchRow
|
|
@@ -733,6 +736,7 @@ function TranscriptsGalleryPage() {
|
|
|
733
736
|
Junior
|
|
734
737
|
</span>
|
|
735
738
|
<TranscriptText
|
|
739
|
+
mentions={[]}
|
|
736
740
|
role="assistant"
|
|
737
741
|
text={
|
|
738
742
|
"Your reminder is set.\n\nI'll remind you to drink water every 30 minutes until you ask me to stop."
|
|
@@ -793,10 +797,10 @@ function TranscriptsGalleryPage() {
|
|
|
793
797
|
<TranscriptMarkdown text={GFM_SAMPLE} />
|
|
794
798
|
</Fixture>
|
|
795
799
|
<Fixture title="TranscriptText assistant">
|
|
796
|
-
<TranscriptText role="assistant" text={MIXED_ASSISTANT} />
|
|
800
|
+
<TranscriptText mentions={[]} role="assistant" text={MIXED_ASSISTANT} />
|
|
797
801
|
</Fixture>
|
|
798
802
|
<Fixture title="TranscriptText user">
|
|
799
|
-
<TranscriptText role="user" text={EVENT_NOTIFICATION} />
|
|
803
|
+
<TranscriptText mentions={[]} role="user" text={EVENT_NOTIFICATION} />
|
|
800
804
|
</Fixture>
|
|
801
805
|
<Fixture title="Tool calls">
|
|
802
806
|
<ToolCallGallery />
|
|
@@ -107,6 +107,35 @@ const EVENTS: ConversationReportEventData[] = [
|
|
|
107
107
|
trustedSummary: "GitHub PR getsentry/junior#1200 received a review.",
|
|
108
108
|
text: "The preview is ready for visual review.",
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
type: "message",
|
|
112
|
+
messageId: "gallery-event-batch",
|
|
113
|
+
role: "user",
|
|
114
|
+
eventType: "pull_request.review.approved",
|
|
115
|
+
eventObjectType: "code_change",
|
|
116
|
+
trustedSummary: "GitHub PR getsentry/junior#1200 was approved by dcramer.",
|
|
117
|
+
events: [
|
|
118
|
+
{
|
|
119
|
+
eventType: "pull_request.review.approved",
|
|
120
|
+
label: "GitHub PR getsentry/junior#1200",
|
|
121
|
+
trustedSummary:
|
|
122
|
+
"GitHub PR getsentry/junior#1200 was approved by dcramer.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
eventType: "pull_request.review.commented",
|
|
126
|
+
label: "GitHub PR getsentry/junior#1200",
|
|
127
|
+
trustedSummary:
|
|
128
|
+
"GitHub PR getsentry/junior#1200 received a review comment from cursor[bot].",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
eventType: "pull_request.review_comment.created",
|
|
132
|
+
label: "GitHub PR getsentry/junior#1200",
|
|
133
|
+
trustedSummary:
|
|
134
|
+
"GitHub PR getsentry/junior#1200 received an inline review comment from cursor[bot].",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
text: "[task]\n\nThis is a task, not a message from a person.\n\nYou received 3 events.",
|
|
138
|
+
},
|
|
110
139
|
];
|
|
111
140
|
|
|
112
141
|
const CONVERSATION: ConversationDetailReport = {
|