@sentry/junior-dashboard 0.268.0 → 0.269.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 +7 -105
- package/dist/app.js +12 -9
- package/dist/client.js +72 -75
- package/dist/index.js +12 -9
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +2 -1
- package/src/app.ts +11 -7
- package/src/client/App.tsx +1 -5
- package/src/client/api.ts +4 -19
- package/src/client/components/AGENTS.md +65 -0
- package/src/client/components/ActorAvatar.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +1 -0
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +1 -1
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/conversations/AGENTS.md +42 -0
- package/src/client/conversations/ConversationPage.tsx +0 -27
- package/src/client/format.ts +7 -23
- package/src/client/pages/automations/AGENTS.md +34 -0
- package/src/client/pages/automations/AutomationActions.tsx +18 -20
- package/src/client/pages/automations/AutomationEditor.tsx +3 -4
- package/src/client/pages/automations/AutomationOverview.tsx +3 -3
- package/src/client/pages/automations/AutomationStatus.tsx +2 -2
- package/src/client/pages/system/AGENTS.md +12 -0
- package/src/client/review-desk/ConversationReviewBar.tsx +14 -8
- package/src/client/review-desk/ReviewDeskPage.tsx +99 -79
- package/src/client/review-desk/ReviewItemDetail.tsx +15 -7
- package/src/client/review-desk/queries.ts +25 -16
- package/src/mock-reporting/routes.ts +1 -1
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -219
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/UserInputRequestCard.tsx +0 -39
- package/src/client/conversations/userInputRequest.ts +0 -56
- package/src/client/pages/automations/README.md +0 -97
- package/src/client/pages/system/README.md +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.269.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"remark-gfm": "^4.0.1",
|
|
40
40
|
"shiki": "4.1.0",
|
|
41
41
|
"zod": "^4.5.4",
|
|
42
|
-
"@sentry/junior-plugin-api": "0.
|
|
42
|
+
"@sentry/junior-plugin-api": "0.269.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@sentry/junior": "0.
|
|
45
|
+
"@sentry/junior": "0.269.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tw-animate-css": "^1.4.0",
|
|
63
63
|
"typescript": "^7.0.2",
|
|
64
64
|
"vitest": "^4.1.12",
|
|
65
|
-
"@sentry/junior": "0.
|
|
65
|
+
"@sentry/junior": "0.269.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
|
package/src/api/schema.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const dashboardIdentitySchema = z
|
|
|
18
18
|
.object({
|
|
19
19
|
user: dashboardSessionUserSchema
|
|
20
20
|
.extend({
|
|
21
|
+
/** Junior API path of the viewer's Slack photo, then Google photo. */
|
|
22
|
+
avatarUrl: z.string().optional(),
|
|
21
23
|
/** Junior-wide admin role. The server still checks it on every admin route. */
|
|
22
24
|
isAdmin: z.boolean(),
|
|
23
25
|
})
|
|
@@ -38,7 +40,6 @@ export const dashboardConfigSchema = z
|
|
|
38
40
|
authPath: z.string(),
|
|
39
41
|
basePath: z.string(),
|
|
40
42
|
sentryConversationLinks: z.boolean(),
|
|
41
|
-
timeZone: z.string().optional(),
|
|
42
43
|
version: z.string().min(1),
|
|
43
44
|
})
|
|
44
45
|
.strict();
|
package/src/app.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
authenticatePersonalToken,
|
|
5
5
|
createJuniorApi,
|
|
6
6
|
jsonResponse,
|
|
7
|
+
readViewerAvatarUrl,
|
|
7
8
|
resolveViewerUser,
|
|
8
9
|
updateViewerDisplayName,
|
|
9
10
|
type ConversationWorkQueue,
|
|
@@ -300,11 +301,6 @@ function mockViewerFromSession(session: DashboardSession) {
|
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
303
|
|
|
303
|
-
/** Return an operator-configured timezone override, or undefined to let each viewer's browser use its own local timezone. */
|
|
304
|
-
function dashboardTimeZone(): string | undefined {
|
|
305
|
-
return process.env.JUNIOR_TIMEZONE || undefined;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
304
|
function pluginRoutePrefix(pluginName: string): string {
|
|
309
305
|
return `/api/plugins/${pluginName}`;
|
|
310
306
|
}
|
|
@@ -565,15 +561,21 @@ export function createDashboardApp(
|
|
|
565
561
|
authPath,
|
|
566
562
|
basePath,
|
|
567
563
|
sentryConversationLinks: hasSentryConversationLinks(),
|
|
568
|
-
timeZone: dashboardTimeZone(),
|
|
569
564
|
version: JUNIOR_VERSION,
|
|
570
565
|
});
|
|
571
566
|
});
|
|
572
|
-
|
|
567
|
+
// Mock reporting has no SQL identities, so it has no profile photos.
|
|
568
|
+
const viewerAvatarUrl = async (userId: string | undefined) =>
|
|
569
|
+
userId && !options.mockConversations
|
|
570
|
+
? await readViewerAvatarUrl(userId)
|
|
571
|
+
: undefined;
|
|
572
|
+
app.get("/api/me", async (c) => {
|
|
573
573
|
const session = c.get("authSession");
|
|
574
574
|
const viewer = c.get("viewer");
|
|
575
|
+
const avatarUrl = await viewerAvatarUrl(viewer?.id);
|
|
575
576
|
return jsonResponse(dashboardIdentitySchema, {
|
|
576
577
|
user: {
|
|
578
|
+
...(avatarUrl ? { avatarUrl } : undefined),
|
|
577
579
|
email: session.user.email,
|
|
578
580
|
emailVerified: session.user.emailVerified,
|
|
579
581
|
isAdmin: viewer?.isAdmin === true,
|
|
@@ -632,8 +634,10 @@ export function createDashboardApp(
|
|
|
632
634
|
{ status: 404 },
|
|
633
635
|
);
|
|
634
636
|
}
|
|
637
|
+
const avatarUrl = await viewerAvatarUrl(updated.id);
|
|
635
638
|
return jsonResponse(dashboardIdentitySchema, {
|
|
636
639
|
user: {
|
|
640
|
+
...(avatarUrl ? { avatarUrl } : undefined),
|
|
637
641
|
email: session.user.email,
|
|
638
642
|
emailVerified: session.user.emailVerified,
|
|
639
643
|
isAdmin: updated.isAdmin === true,
|
package/src/client/App.tsx
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
buildConversations,
|
|
25
25
|
conversationDetailTitle,
|
|
26
26
|
conversationDisplayTitle,
|
|
27
|
-
setDashboardTimeZone,
|
|
28
27
|
} from "./format";
|
|
29
28
|
import { isNewConversationPath } from "./conversations/conversationRoutes";
|
|
30
29
|
import { useDashboardServerVersion } from "./dashboard-version";
|
|
@@ -84,9 +83,6 @@ export function DashboardShell() {
|
|
|
84
83
|
const data = query.data;
|
|
85
84
|
const serverVersion = useDashboardServerVersion(data?.config.version);
|
|
86
85
|
const userPages = userPagesQuery.data ?? [];
|
|
87
|
-
if (data) {
|
|
88
|
-
setDashboardTimeZone(data.config.timeZone);
|
|
89
|
-
}
|
|
90
86
|
const loading = !data && !query.error;
|
|
91
87
|
const loggedIn = Boolean(data?.config.authRequired && data.me.user.email);
|
|
92
88
|
const personalSpendQuery = usePersonalSpendData(loggedIn);
|
|
@@ -119,7 +115,7 @@ export function DashboardShell() {
|
|
|
119
115
|
loggedIn,
|
|
120
116
|
pathname: location.pathname,
|
|
121
117
|
primaryUserPages,
|
|
122
|
-
reviewCount: reviewCountQuery.data?.
|
|
118
|
+
reviewCount: reviewCountQuery.data?.needsViewer,
|
|
123
119
|
userPagesPending: userPagesQuery.isPending,
|
|
124
120
|
});
|
|
125
121
|
|
package/src/client/api.ts
CHANGED
|
@@ -33,23 +33,9 @@ import { fetchDashboardJson } from "./http";
|
|
|
33
33
|
import type { DashboardCoreData, SystemData } from "./types";
|
|
34
34
|
|
|
35
35
|
const dashboardMetadataStaleTimeMs = 5 * 60_000;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/** Schedule the next spend refresh from the age of the server-cached report. */
|
|
40
|
-
export function personalSpendRefreshDelay(
|
|
41
|
-
generatedAt: string | undefined,
|
|
42
|
-
nowMs = Date.now(),
|
|
43
|
-
): number {
|
|
44
|
-
if (!generatedAt) return PERSONAL_SPEND_REFRESH_MS;
|
|
45
|
-
const generatedAtMs = Date.parse(generatedAt);
|
|
46
|
-
if (!Number.isFinite(generatedAtMs)) return PERSONAL_SPEND_REFRESH_MS;
|
|
47
|
-
const ageMs = Math.max(0, nowMs - generatedAtMs);
|
|
48
|
-
return Math.max(
|
|
49
|
-
MIN_PERSONAL_SPEND_REFRESH_MS,
|
|
50
|
-
PERSONAL_SPEND_REFRESH_MS - ageMs,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
36
|
+
// The server reuses its cached report until the viewer's spend changes, so a
|
|
37
|
+
// short poll shows a finished Turn without a full aggregate query each time.
|
|
38
|
+
const PERSONAL_SPEND_REFRESH_MS = 10_000;
|
|
53
39
|
|
|
54
40
|
/** Fetch dashboard shell data shared across browser routes. */
|
|
55
41
|
export function useDashboardCoreData() {
|
|
@@ -314,8 +300,7 @@ export function usePersonalSpendData(enabled: boolean) {
|
|
|
314
300
|
"/api/people/me/spend",
|
|
315
301
|
signal,
|
|
316
302
|
),
|
|
317
|
-
refetchInterval:
|
|
318
|
-
personalSpendRefreshDelay(query.state.data?.generatedAt),
|
|
303
|
+
refetchInterval: PERSONAL_SPEND_REFRESH_MS,
|
|
319
304
|
retry: false,
|
|
320
305
|
staleTime: 0,
|
|
321
306
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Dashboard UI kit
|
|
2
|
+
|
|
3
|
+
Shared dashboard components live here. Feature pages compose them instead of
|
|
4
|
+
copying Tailwind blocks. Follow `policies/frontend-components.md`.
|
|
5
|
+
`pnpm dashboard-style:check` enforces the token rules.
|
|
6
|
+
|
|
7
|
+
## shadcn/ui
|
|
8
|
+
|
|
9
|
+
- Add components with
|
|
10
|
+
`pnpm --filter @sentry/junior-dashboard exec shadcn add <component>`. They go
|
|
11
|
+
to `ui/`.
|
|
12
|
+
- `ui/` files are shadcn source on dashboard tokens. Remove unused exports.
|
|
13
|
+
Change a `ui/` file when every caller needs the same style.
|
|
14
|
+
- Use `ui/` components directly. Add an app component only when it adds
|
|
15
|
+
structure or behavior, not to rename a `ui/` component or its props.
|
|
16
|
+
- Do not apply the CLI's global border or body resets. Keep the Tailwind
|
|
17
|
+
radius scale.
|
|
18
|
+
|
|
19
|
+
## Component rules
|
|
20
|
+
|
|
21
|
+
- Use `ui/alert-dialog` for every confirmation. Never use `window.confirm`.
|
|
22
|
+
- Use `ui/dialog` for a short task form. Never put a form in a `Drawer` or
|
|
23
|
+
`DetailsPanel`. Use `useSheet` for each dialog and mount it only while open.
|
|
24
|
+
- Use `ui/trace` for transcript activity rows. Put only inline elements in
|
|
25
|
+
the row button.
|
|
26
|
+
- Give an icon-only control a label with `TooltipContent size="compact"`.
|
|
27
|
+
Never use the native `title` attribute.
|
|
28
|
+
- Set `focusableWhenDisabled` on a `ui/button` tooltip trigger that can be
|
|
29
|
+
disabled.
|
|
30
|
+
- Build form fields from `ui/field`. Use the ids `<control id>-error` and
|
|
31
|
+
`<control id>-help`.
|
|
32
|
+
- Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
|
|
33
|
+
`aria-label`.
|
|
34
|
+
- `ui/alert` sets `role="alert"`. Pass `role="status"` for a message that is
|
|
35
|
+
not an error.
|
|
36
|
+
- A form page has one `Button variant="primary"`.
|
|
37
|
+
- `ObjectIcon` owns native object icons. Do not replace it with a provider
|
|
38
|
+
logo.
|
|
39
|
+
|
|
40
|
+
## Tokens
|
|
41
|
+
|
|
42
|
+
- Keep every color in a token in `src/tailwind.css`, so each theme can
|
|
43
|
+
redefine it. Use `light:` only for values that no token covers.
|
|
44
|
+
- Use `var(--color-*)` for chart series. Never use hex values.
|
|
45
|
+
- Do not use `white/*` or `black/*` fills, borders, or rings, or hex surface
|
|
46
|
+
colors. Shadows may use black.
|
|
47
|
+
- Use `outline-dashboard-focus` for keyboard focus only.
|
|
48
|
+
- Use the named type scale. Keep `text-2xs` for compact badges and chart
|
|
49
|
+
annotations.
|
|
50
|
+
- Labels use sentence case. Do not use uppercase or letter spacing.
|
|
51
|
+
|
|
52
|
+
## Navigation
|
|
53
|
+
|
|
54
|
+
- App navigation uses pills, page links use `SegmentedNav`, and panel
|
|
55
|
+
switches use `ui/tabs`. Keep the three levels distinct.
|
|
56
|
+
|
|
57
|
+
## Ownership
|
|
58
|
+
|
|
59
|
+
- Conversation mobile geometry has hard owners: `layout/VisualViewportShell`,
|
|
60
|
+
`conversations/ChatLayout`, and `conversations/ComposerDock`. Pages pass
|
|
61
|
+
children only. Do not add viewport, offset, or safe-area math in pages.
|
|
62
|
+
- Home and create must not use `ComposerDock`.
|
|
63
|
+
- Feature surfaces stay under `conversations/` or `pages/` until two real
|
|
64
|
+
callers share them. Add a named component on the second real use.
|
|
65
|
+
- Keep Tailwind classes on the owning component.
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ShieldCheck,
|
|
9
9
|
UserRound,
|
|
10
10
|
} from "lucide-react";
|
|
11
|
-
import { useState } from "react";
|
|
11
|
+
import { useEffect, useRef, useState } from "react";
|
|
12
12
|
import { Link } from "react-router";
|
|
13
13
|
import type { PersonalSpendReport } from "@sentry/junior/api/schema";
|
|
14
14
|
import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
@@ -18,6 +18,7 @@ import { pluginUserPagePath } from "../pages/user/PluginUserPage";
|
|
|
18
18
|
import { cn, dashboardInteractiveTextClass } from "../styles";
|
|
19
19
|
import type { Identity } from "../types";
|
|
20
20
|
import { ThemeSwitcher } from "./ThemeSwitcher";
|
|
21
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
21
22
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
22
23
|
|
|
23
24
|
type ProfileMenuProps = {
|
|
@@ -49,6 +50,83 @@ function initials(name: string | null | undefined, email: string): string {
|
|
|
49
50
|
return email.slice(0, 1).toUpperCase();
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** Show the viewer's photo, with initials while it loads or when there is none. */
|
|
54
|
+
function ProfileAvatar({
|
|
55
|
+
className,
|
|
56
|
+
user,
|
|
57
|
+
}: {
|
|
58
|
+
className?: string;
|
|
59
|
+
user: Identity["user"];
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<Avatar
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
className={cn("border-0 shadow-black/40", className)}
|
|
65
|
+
>
|
|
66
|
+
<AvatarImage alt="User avatar" src={user.avatarUrl} />
|
|
67
|
+
<AvatarFallback className="font-bold tracking-wide">
|
|
68
|
+
{initials(user.name, user.email!)}
|
|
69
|
+
</AvatarFallback>
|
|
70
|
+
</Avatar>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SPEND_COUNT_UP_MS = 700;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Show one spend total. When the total changes, count up from the last shown
|
|
78
|
+
* value and tint the figure until the count ends.
|
|
79
|
+
*/
|
|
80
|
+
function SpendAmount({ value }: { value?: number }) {
|
|
81
|
+
const [shown, setShown] = useState(value);
|
|
82
|
+
const [counting, setCounting] = useState(false);
|
|
83
|
+
const shownRef = useRef(value);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const from = shownRef.current;
|
|
87
|
+
if (
|
|
88
|
+
value === undefined ||
|
|
89
|
+
from === undefined ||
|
|
90
|
+
from === value ||
|
|
91
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
92
|
+
) {
|
|
93
|
+
shownRef.current = value;
|
|
94
|
+
setShown(value);
|
|
95
|
+
setCounting(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const startedAt = performance.now();
|
|
100
|
+
let frame = 0;
|
|
101
|
+
const step = (time: number) => {
|
|
102
|
+
const progress = Math.min(1, (time - startedAt) / SPEND_COUNT_UP_MS);
|
|
103
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
104
|
+
const next = progress < 1 ? from + (value - from) * eased : value;
|
|
105
|
+
shownRef.current = next;
|
|
106
|
+
setShown(next);
|
|
107
|
+
if (progress < 1) {
|
|
108
|
+
frame = requestAnimationFrame(step);
|
|
109
|
+
} else {
|
|
110
|
+
setCounting(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
setCounting(true);
|
|
114
|
+
frame = requestAnimationFrame(step);
|
|
115
|
+
return () => cancelAnimationFrame(frame);
|
|
116
|
+
}, [value]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<span
|
|
120
|
+
className={cn(
|
|
121
|
+
"transition-colors duration-700 motion-reduce:transition-none",
|
|
122
|
+
counting && "text-dashboard-accent-text",
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{shown === undefined ? "—" : formatCostSummary({ total: shown })}
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
52
130
|
const profileLinkClass =
|
|
53
131
|
"flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm font-semibold text-dashboard-text no-underline transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:bg-dashboard-fill-strong focus-visible:text-dashboard-text focus-visible:outline-none";
|
|
54
132
|
|
|
@@ -73,19 +151,10 @@ export function ProfileMenu({
|
|
|
73
151
|
(page) => page.navigation === "profile",
|
|
74
152
|
);
|
|
75
153
|
|
|
76
|
-
const avatar = (
|
|
77
|
-
<span
|
|
78
|
-
aria-hidden="true"
|
|
79
|
-
className="grid size-8 shrink-0 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
80
|
-
>
|
|
81
|
-
{initials(identity.user.name, email)}
|
|
82
|
-
</span>
|
|
83
|
-
);
|
|
84
|
-
|
|
85
154
|
const identityBlock = (
|
|
86
155
|
<div className="border-b border-dashboard-border-strong px-2.5 py-2.5">
|
|
87
156
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
88
|
-
{
|
|
157
|
+
<ProfileAvatar className="size-8" user={identity.user} />
|
|
89
158
|
<div className="min-w-0">
|
|
90
159
|
<p className="m-0 truncate text-sm font-semibold text-dashboard-text">
|
|
91
160
|
{name}
|
|
@@ -101,13 +170,13 @@ export function ProfileMenu({
|
|
|
101
170
|
<span>
|
|
102
171
|
7d{" "}
|
|
103
172
|
<span className="font-semibold text-dashboard-text">
|
|
104
|
-
{
|
|
173
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
105
174
|
</span>
|
|
106
175
|
</span>
|
|
107
176
|
<span>
|
|
108
177
|
30d{" "}
|
|
109
178
|
<span className="font-semibold text-dashboard-text">
|
|
110
|
-
{
|
|
179
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
111
180
|
</span>
|
|
112
181
|
</span>
|
|
113
182
|
</div>
|
|
@@ -186,13 +255,13 @@ export function ProfileMenu({
|
|
|
186
255
|
<span className="whitespace-nowrap">
|
|
187
256
|
7d{" "}
|
|
188
257
|
<span className="font-semibold text-dashboard-text">
|
|
189
|
-
{
|
|
258
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
190
259
|
</span>
|
|
191
260
|
</span>
|
|
192
261
|
<span className="whitespace-nowrap">
|
|
193
262
|
30d{" "}
|
|
194
263
|
<span className="font-semibold text-dashboard-text">
|
|
195
|
-
{
|
|
264
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
196
265
|
</span>
|
|
197
266
|
</span>
|
|
198
267
|
</div>
|
|
@@ -254,12 +323,7 @@ export function ProfileMenu({
|
|
|
254
323
|
aria-label={`Signed in as ${name}`}
|
|
255
324
|
className="flex min-w-0 items-center gap-2.5 px-1"
|
|
256
325
|
>
|
|
257
|
-
<
|
|
258
|
-
aria-hidden="true"
|
|
259
|
-
className="grid size-7 shrink-0 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
260
|
-
>
|
|
261
|
-
{initials(identity.user.name, email)}
|
|
262
|
-
</span>
|
|
326
|
+
<ProfileAvatar user={identity.user} />
|
|
263
327
|
<div className="min-w-0">
|
|
264
328
|
<p className="m-0 truncate text-xs font-semibold text-dashboard-text">
|
|
265
329
|
{name}
|
|
@@ -304,7 +368,7 @@ export function ProfileMenu({
|
|
|
304
368
|
7d
|
|
305
369
|
</span>
|
|
306
370
|
<span className="text-xs font-semibold text-dashboard-text">
|
|
307
|
-
{
|
|
371
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
308
372
|
</span>
|
|
309
373
|
</span>
|
|
310
374
|
<span
|
|
@@ -316,16 +380,11 @@ export function ProfileMenu({
|
|
|
316
380
|
30d
|
|
317
381
|
</span>
|
|
318
382
|
<span className="text-xs font-semibold text-dashboard-text">
|
|
319
|
-
{
|
|
383
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
320
384
|
</span>
|
|
321
385
|
</span>
|
|
322
386
|
</span>
|
|
323
|
-
<
|
|
324
|
-
aria-hidden="true"
|
|
325
|
-
className="grid size-7 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
326
|
-
>
|
|
327
|
-
{initials(identity.user.name, email)}
|
|
328
|
-
</span>
|
|
387
|
+
<ProfileAvatar user={identity.user} />
|
|
329
388
|
<ChevronDown
|
|
330
389
|
aria-hidden="true"
|
|
331
390
|
className={cn(
|
|
@@ -138,7 +138,7 @@ function TextField(props: {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Render the answer controls of
|
|
141
|
+
* Render the answer controls of an action item request. A request with
|
|
142
142
|
* one short choice field shows one button per choice, and a click is the
|
|
143
143
|
* answer. Other requests show a form that the reader submits once. A failed
|
|
144
144
|
* `onSubmit` lets the reader try again. `secondaryActions`, such as Dismiss,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# System cache reporting
|
|
2
|
+
|
|
3
|
+
- `cache-input.ts` owns the cache input calculation.
|
|
4
|
+
- `ConversationMetricDay` comes from stored Run metrics, not Sentry spans.
|
|
5
|
+
Its input, cached input, and cache-creation counters are disjoint. Add
|
|
6
|
+
them once for total input. Do not include output. Never use this transform
|
|
7
|
+
for Sentry span totals.
|
|
8
|
+
- Period shares use summed token counts, not the mean of bucket shares.
|
|
9
|
+
- A missing counter is not a zero. When an active bucket has no counter, omit
|
|
10
|
+
its share and the whole-period share.
|
|
11
|
+
- Do not call these values a cache hit rate or a cache failure rate. Field
|
|
12
|
+
presence is not call-level coverage.
|
|
13
|
+
- The page time control selects the buckets for the chart and `CacheWrites`.
|
|
14
|
+
Do not add a local time selector.
|
|
15
|
+
- Add a reporting contract before you add per-call filters or drilldowns.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Transcript components
|
|
2
|
+
|
|
3
|
+
Keep transcript layout in these feature components. Use the UI kit in
|
|
4
|
+
`../components/` for shared controls and overlays.
|
|
5
|
+
|
|
6
|
+
## Ownership
|
|
7
|
+
|
|
8
|
+
- `TranscriptRows` and `TranscriptRow` own row gaps, padding, and indent.
|
|
9
|
+
Keep horizontal padding inside each row, so focus outlines are not clipped.
|
|
10
|
+
- `TranscriptMessageShell` owns the avatar column and message gutter.
|
|
11
|
+
- `TranscriptSummaryTrigger` owns disclosure padding and focus. Do not repeat
|
|
12
|
+
negative margins at call sites.
|
|
13
|
+
- `ChatLayout` keeps the Conversation header outside the transcript scroll.
|
|
14
|
+
- `eventLogRows` and `filterEventLogRows` own row text and search match. The
|
|
15
|
+
page step controls use them too.
|
|
16
|
+
|
|
17
|
+
## Rules
|
|
18
|
+
|
|
19
|
+
- Display text keeps Slack tokens such as `<@U123>`. The API returns
|
|
20
|
+
`mentions` beside the owning object. Render with `TranscriptText` or
|
|
21
|
+
`MentionText`. Do not resolve names in the dashboard, and do not rewrite
|
|
22
|
+
tokens into links on the server.
|
|
23
|
+
- The Automation trigger card shows the credentials from the dispatch record
|
|
24
|
+
of the run, not the current setting. Keep the run input collapsed, because
|
|
25
|
+
it is not from a person.
|
|
26
|
+
- Each `Transcript` sets the panel for its own rows. A nested transcript never
|
|
27
|
+
uses the panel of the page behind it.
|
|
28
|
+
- The page has one right panel. Conversation details, event details, and turn
|
|
29
|
+
context never show at the same time. Closing a panel returns focus to the
|
|
30
|
+
control that opened it.
|
|
31
|
+
- The URL owns the selected event in the main Conversation. Standalone
|
|
32
|
+
transcripts keep selection in `ConversationEventLog`.
|
|
33
|
+
- A pending exit must not close a new panel or another Conversation.
|
|
34
|
+
- A pending fork blocks dismissal of its dialog.
|
|
35
|
+
- Raw JSON keeps the original ISO values. Persist the UTC/local mode, not the
|
|
36
|
+
timezone.
|
|
37
|
+
|
|
38
|
+
## Checks
|
|
39
|
+
|
|
40
|
+
- Review with `JUNIOR_DASHBOARD_MOCK_CONVERSATIONS=true pnpm dev`.
|
|
41
|
+
- Keep visual checks out of automated assertions. Browser tests cover
|
|
42
|
+
opening, closing, and keyboard interaction.
|
|
@@ -73,11 +73,6 @@ import {
|
|
|
73
73
|
hasConversationUsageBreakdown,
|
|
74
74
|
} from "./ConversationUsageBreakdown";
|
|
75
75
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
76
|
-
import { UserInputRequestCard } from "./UserInputRequestCard";
|
|
77
|
-
import {
|
|
78
|
-
openUserInputRequest,
|
|
79
|
-
type OpenUserInputRequest,
|
|
80
|
-
} from "./userInputRequest";
|
|
81
76
|
import {
|
|
82
77
|
buildConversations,
|
|
83
78
|
conversationDetailTitle,
|
|
@@ -197,10 +192,6 @@ export function ConversationPage(props: {
|
|
|
197
192
|
() => committedMessageIds(detail.data?.events),
|
|
198
193
|
[detail.data?.events],
|
|
199
194
|
);
|
|
200
|
-
const userInputRequest = useMemo(
|
|
201
|
-
() => openUserInputRequest(detail.data?.events),
|
|
202
|
-
[detail.data?.events],
|
|
203
|
-
);
|
|
204
195
|
// Cancel needs the latest mailbox watermark, but not as a render prop. Live
|
|
205
196
|
// polls refresh generatedAt every 2s even when the queue is unchanged.
|
|
206
197
|
const pendingGeneratedAtRef = useRef(detail.pendingGeneratedAt);
|
|
@@ -494,7 +485,6 @@ export function ConversationPage(props: {
|
|
|
494
485
|
detail.inlineMessages.length === 0
|
|
495
486
|
}
|
|
496
487
|
stoppable={live}
|
|
497
|
-
userInputRequest={userInputRequest}
|
|
498
488
|
/>
|
|
499
489
|
) : undefined
|
|
500
490
|
}
|
|
@@ -585,8 +575,6 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
585
575
|
sendsInline: boolean;
|
|
586
576
|
/** True while the conversation has a Turn that a stop can end. */
|
|
587
577
|
stoppable: boolean;
|
|
588
|
-
/** Junior's open question, if no user message answers it yet. */
|
|
589
|
-
userInputRequest?: OpenUserInputRequest;
|
|
590
578
|
}) {
|
|
591
579
|
const appendMessage = useAppendConversationMessage(props.conversationId);
|
|
592
580
|
const stopTurn = useStopConversationTurn(props.conversationId);
|
|
@@ -665,13 +653,6 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
665
653
|
const cancellableMessageIds = props.pendingMessages
|
|
666
654
|
.filter((message) => message.clientStatus === undefined)
|
|
667
655
|
.map((message) => message.inboundMessageId);
|
|
668
|
-
const onAnswerUserInput = useCallback(async (answer: string) => {
|
|
669
|
-
onPinRequestRef.current();
|
|
670
|
-
await appendMessageRef.current.mutateAsync({
|
|
671
|
-
idempotencyKey: crypto.randomUUID(),
|
|
672
|
-
message: answer,
|
|
673
|
-
});
|
|
674
|
-
}, []);
|
|
675
656
|
const onCancelMessage = useCallback(
|
|
676
657
|
(message: ConversationMailboxMessage) => {
|
|
677
658
|
const receivedBefore = props.pendingGeneratedAtRef.current;
|
|
@@ -796,14 +777,6 @@ const ConversationReplyFooter = memo(function ConversationReplyFooter(props: {
|
|
|
796
777
|
{props.pendingAuthorization ? (
|
|
797
778
|
<PendingAuthorization authorization={props.pendingAuthorization} />
|
|
798
779
|
) : null}
|
|
799
|
-
{/* A queued message already answers the question. */}
|
|
800
|
-
{props.userInputRequest && props.pendingMessages.length === 0 ? (
|
|
801
|
-
<UserInputRequestCard
|
|
802
|
-
key={props.userInputRequest.toolCallId}
|
|
803
|
-
onAnswer={onAnswerUserInput}
|
|
804
|
-
request={props.userInputRequest}
|
|
805
|
-
/>
|
|
806
|
-
) : null}
|
|
807
780
|
<ConversationReviewBar conversationId={props.conversationId} />
|
|
808
781
|
<PendingMailboxStack
|
|
809
782
|
cancelError={cancelError}
|
package/src/client/format.ts
CHANGED
|
@@ -19,8 +19,8 @@ import { getDashboardAgentName } from "./agentName";
|
|
|
19
19
|
import { formatDuration } from "./components/Duration";
|
|
20
20
|
import { conversationTranscriptMessages } from "./conversations/eventTranscript";
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
function
|
|
22
|
+
/** Read the viewer's own browser timezone, used for absolute timestamps and charts. */
|
|
23
|
+
export function getDashboardTimeZone(): string {
|
|
24
24
|
try {
|
|
25
25
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
26
26
|
} catch {
|
|
@@ -28,22 +28,6 @@ function browserTimeZone(): string {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
let dashboardTimeZone = browserTimeZone();
|
|
32
|
-
|
|
33
|
-
/** Apply an operator-configured timezone override from the config API, if any. Otherwise the viewer's own browser timezone is used. */
|
|
34
|
-
export function setDashboardTimeZone(timeZone: string | undefined): void {
|
|
35
|
-
dashboardTimeZone = timeZone || browserTimeZone();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Read the dashboard display timezone used for absolute timestamps and charts. */
|
|
39
|
-
export function getDashboardTimeZone(): string {
|
|
40
|
-
return dashboardTimeZone;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function displayTimeZone(): string {
|
|
44
|
-
return dashboardTimeZone;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
31
|
function isActiveSummary(summary: ConversationSummaryReport): boolean {
|
|
48
32
|
return summary.status === "active";
|
|
49
33
|
}
|
|
@@ -69,7 +53,7 @@ function parseTime(value: string | undefined): number | null {
|
|
|
69
53
|
return Number.isFinite(time) ? time : null;
|
|
70
54
|
}
|
|
71
55
|
|
|
72
|
-
/** Format absolute dashboard timestamps in the
|
|
56
|
+
/** Format absolute dashboard timestamps in the viewer's browser timezone. */
|
|
73
57
|
export function formatTime(
|
|
74
58
|
value: string | undefined,
|
|
75
59
|
options?: Intl.DateTimeFormatOptions,
|
|
@@ -78,7 +62,7 @@ export function formatTime(
|
|
|
78
62
|
if (time == null) return "none";
|
|
79
63
|
return new Date(time).toLocaleString(undefined, {
|
|
80
64
|
...options,
|
|
81
|
-
timeZone:
|
|
65
|
+
timeZone: getDashboardTimeZone(),
|
|
82
66
|
});
|
|
83
67
|
}
|
|
84
68
|
|
|
@@ -166,10 +150,10 @@ export function formatMessageTimestamp(
|
|
|
166
150
|
? date.toLocaleString(undefined, {
|
|
167
151
|
dateStyle: "medium",
|
|
168
152
|
timeStyle: "medium",
|
|
169
|
-
timeZone:
|
|
153
|
+
timeZone: getDashboardTimeZone(),
|
|
170
154
|
})
|
|
171
155
|
: date.toLocaleTimeString(undefined, {
|
|
172
|
-
timeZone:
|
|
156
|
+
timeZone: getDashboardTimeZone(),
|
|
173
157
|
});
|
|
174
158
|
}
|
|
175
159
|
|
|
@@ -210,7 +194,7 @@ export function formatTranscriptTimestampDetails(value: number): {
|
|
|
210
194
|
return {
|
|
211
195
|
local: date.toLocaleString(undefined, {
|
|
212
196
|
...options,
|
|
213
|
-
timeZone:
|
|
197
|
+
timeZone: getDashboardTimeZone(),
|
|
214
198
|
}),
|
|
215
199
|
utc: date.toLocaleString(undefined, { ...options, timeZone: "UTC" }),
|
|
216
200
|
};
|