@sentry/junior-dashboard 0.262.0 → 0.264.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 +6 -2
- package/dist/client.js +62 -58
- package/dist/index.js +6 -2
- package/dist/tailwind.css +1 -1
- package/package.json +7 -3
- package/src/client/components/BottomSheet.tsx +16 -18
- package/src/client/components/Drawer.tsx +22 -12
- package/src/client/components/README.md +37 -9
- package/src/client/components/ui/sheet.tsx +141 -0
- package/src/client/components/ui/useSheet.ts +74 -0
- package/src/client/conversations/ConversationChildAgents.tsx +87 -58
- package/src/client/conversations/ConversationDetails.tsx +85 -67
- package/src/client/conversations/ConversationDetailsFrame.tsx +47 -19
- package/src/client/conversations/ConversationMeta.tsx +56 -4
- package/src/client/conversations/ConversationPage.tsx +28 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
- package/src/client/conversations/README.md +6 -5
- package/src/client/conversations/ShareConversationButton.tsx +15 -2
- package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
- package/src/client/conversations/eventTranscript.ts +3 -0
- package/src/client/format.ts +3 -0
- package/src/client/lib/utils.ts +1 -0
- package/src/client/pages/dev/ComponentsPage.tsx +1 -1
- package/src/client/types.ts +2 -0
- package/src/mock-reporting/fixtures.ts +4 -0
- package/src/tailwind.css +77 -0
- package/src/client/components/Sheet.tsx +0 -141
- /package/src/client/components/{Sheet.LICENSE → ui/sheet.LICENSE} +0 -0
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
import { Sheet,
|
|
3
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "./ui/sheet";
|
|
4
|
+
import { useSheet } from "./ui/useSheet";
|
|
4
5
|
|
|
5
|
-
/** Show details in
|
|
6
|
+
/** Show app details in the shadcn sheet with a visible title and close control. */
|
|
6
7
|
export function BottomSheet(props: {
|
|
7
8
|
children: ReactNode;
|
|
8
9
|
label: string;
|
|
9
10
|
onClose(): void;
|
|
10
11
|
}) {
|
|
12
|
+
const sheet = useSheet(props.onClose);
|
|
11
13
|
return (
|
|
12
|
-
<Sheet
|
|
13
|
-
<SheetContent
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<SheetBody
|
|
24
|
-
data-base-ui-swipe-ignore=""
|
|
25
|
-
className="min-h-0 overflow-y-auto overscroll-contain"
|
|
26
|
-
>
|
|
14
|
+
<Sheet {...sheet.rootProps}>
|
|
15
|
+
<SheetContent
|
|
16
|
+
{...sheet.contentProps}
|
|
17
|
+
aria-label={props.label}
|
|
18
|
+
side="bottom"
|
|
19
|
+
className="mx-auto max-h-full w-full max-w-2xl gap-0 rounded-t-2xl outline-none data-[side=bottom]:h-[85dvh]"
|
|
20
|
+
>
|
|
21
|
+
<SheetHeader className="border-b border-dashboard-border">
|
|
22
|
+
<SheetTitle>{props.label}</SheetTitle>
|
|
23
|
+
</SheetHeader>
|
|
24
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
27
25
|
{props.children}
|
|
28
|
-
</
|
|
26
|
+
</div>
|
|
29
27
|
</SheetContent>
|
|
30
28
|
</Sheet>
|
|
31
29
|
);
|
|
@@ -3,10 +3,10 @@ import { useRef, type ReactNode } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import { Button } from "./Button";
|
|
6
|
-
import { Sheet,
|
|
6
|
+
import { Sheet, SheetClose, SheetContent } from "./ui/sheet";
|
|
7
|
+
import { useSheet } from "./ui/useSheet";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
export function Drawer(props: {
|
|
9
|
+
type DrawerProps = {
|
|
10
10
|
actions?: ReactNode;
|
|
11
11
|
children: ReactNode;
|
|
12
12
|
closeLabel: string;
|
|
@@ -15,20 +15,30 @@ export function Drawer(props: {
|
|
|
15
15
|
openKey: string;
|
|
16
16
|
titleId: string;
|
|
17
17
|
width?: "default" | "wide" | "narrow";
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Render a side drawer with shared modal focus and dismissal behavior. */
|
|
21
|
+
export function Drawer(props: DrawerProps) {
|
|
22
|
+
return <DrawerContent key={props.openKey} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DrawerContent(props: DrawerProps) {
|
|
19
26
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
27
|
+
const sheet = useSheet(props.onClose, closeRef);
|
|
20
28
|
return (
|
|
21
|
-
<Sheet
|
|
29
|
+
<Sheet {...sheet.rootProps}>
|
|
22
30
|
<SheetContent
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
{...sheet.contentProps}
|
|
32
|
+
aria-labelledby={props.titleId}
|
|
25
33
|
side="right"
|
|
34
|
+
showCloseButton={false}
|
|
26
35
|
className={cn(
|
|
36
|
+
"grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] gap-0 outline-none data-[side=right]:w-full data-[side=right]:sm:max-w-none",
|
|
27
37
|
props.width === "wide"
|
|
28
|
-
? "md:w-[min(760px,94vw)]"
|
|
38
|
+
? "data-[side=right]:md:w-[min(760px,94vw)]"
|
|
29
39
|
: props.width === "narrow"
|
|
30
|
-
? "md:w-[min(440px,94vw)]"
|
|
31
|
-
: "md:w-[min(560px,94vw)]",
|
|
40
|
+
? "data-[side=right]:md:w-[min(440px,94vw)]"
|
|
41
|
+
: "data-[side=right]:md:w-[min(560px,94vw)]",
|
|
32
42
|
)}
|
|
33
43
|
>
|
|
34
44
|
<header className="relative grid grid-cols-[minmax(0,1fr)_auto] gap-3 border-b border-dashboard-border bg-dashboard-bg-elevated px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] md:px-5">
|
|
@@ -49,9 +59,9 @@ export function Drawer(props: {
|
|
|
49
59
|
</SheetClose>
|
|
50
60
|
</div>
|
|
51
61
|
</header>
|
|
52
|
-
<
|
|
62
|
+
<div className="min-h-0 overflow-auto px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5">
|
|
53
63
|
{props.children}
|
|
54
|
-
</
|
|
64
|
+
</div>
|
|
55
65
|
</SheetContent>
|
|
56
66
|
</Sheet>
|
|
57
67
|
);
|
|
@@ -4,6 +4,36 @@ Shared dashboard components live here. Feature pages compose these surfaces
|
|
|
4
4
|
instead of copying Tailwind blocks. See `policies/frontend-components.md` for
|
|
5
5
|
the policy. `pnpm dashboard-style:check` enforces the token rules below.
|
|
6
6
|
|
|
7
|
+
## shadcn/ui
|
|
8
|
+
|
|
9
|
+
`packages/junior-dashboard/components.json` configures shadcn/ui for this package.
|
|
10
|
+
It uses Base UI (`base-nova`), Tailwind 4, and Lucide. Add components from the
|
|
11
|
+
repository root:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm --filter @sentry/junior-dashboard exec shadcn add <component>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI writes components to `src/client/components/ui/`. Its `@dashboard/*`
|
|
18
|
+
alias points to `src/client/*`. The existing `@/*` alias still points to core.
|
|
19
|
+
`src/client/lib/utils.ts` provides the shadcn `cn` helper. Existing components
|
|
20
|
+
keep the class join helper in `src/client/styles.ts` until they are migrated.
|
|
21
|
+
|
|
22
|
+
The shadcn color tokens in `src/tailwind.css` use the dashboard tokens in both
|
|
23
|
+
themes. Primary actions use `text-solid` with inverse text. Danger uses the
|
|
24
|
+
same theme-aware rose shade as existing danger controls. The `dark:` variant
|
|
25
|
+
follows `data-theme`. Keep the existing Tailwind radius scale. Do not apply the CLI's global border or body resets to existing
|
|
26
|
+
components. Check each added component against the rules below and add its
|
|
27
|
+
gallery fixture before use.
|
|
28
|
+
|
|
29
|
+
`ui/sheet.tsx` is the shadcn Base UI Dialog Sheet. It supports all four edges,
|
|
30
|
+
uses the dashboard overlay and close button, and stops motion when reduced
|
|
31
|
+
motion is set. The copied source license is in `ui/sheet.LICENSE`.
|
|
32
|
+
`Drawer` and `BottomSheet` compose this component for app details.
|
|
33
|
+
`ui/useSheet.ts` owns their close lifecycle, shared document scroll lock, and
|
|
34
|
+
focus rules when a resize puts one modal below another. Conversation browser
|
|
35
|
+
scenarios cover dismissal, nested focus, and layout changes.
|
|
36
|
+
|
|
7
37
|
## Foundations
|
|
8
38
|
|
|
9
39
|
Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
|
|
@@ -80,15 +110,13 @@ token so each theme can redefine it.
|
|
|
80
110
|
sentence-case text, with no border or fill. It is not a control.
|
|
81
111
|
- **Messages.** Use `Notice` for compact transient messages and `FormNotice`
|
|
82
112
|
for wrapping form recovery text.
|
|
83
|
-
- **Overlays.** `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
so the chat stays usable. The copied component's license is in `Sheet.LICENSE`.
|
|
91
|
-
`Tooltip` and `IconButtonTooltip` own hover text.
|
|
113
|
+
- **Overlays.** `ui/sheet.tsx` is the shadcn/ui Base UI Sheet. It uses Base UI
|
|
114
|
+
Dialog for focus and dismissal. `Drawer` adds a visible header and close
|
|
115
|
+
control. `BottomSheet` puts details below a title and close control. Both use
|
|
116
|
+
the app's shared scroll lock, skip motion when reduced motion is set, and
|
|
117
|
+
close with the close button, backdrop, or Escape. Docked conversation details
|
|
118
|
+
use a non-modal aside so the chat stays usable. `Tooltip` and
|
|
119
|
+
`IconButtonTooltip` own hover text.
|
|
92
120
|
- **Identity.** `ActorAvatar`, `ParticipantAvatarStack`, and `ObjectIcon`.
|
|
93
121
|
`ObjectIcon` owns native Ticket, Code change, Automation, Deployment, and
|
|
94
122
|
Item icons. It uses the shared type and status mapping and the original
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Adapted from shadcn/ui Base UI Sheet. See sheet.LICENSE.
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
|
|
4
|
+
import { cn } from "@dashboard/lib/utils";
|
|
5
|
+
|
|
6
|
+
import { Button } from "../Button";
|
|
7
|
+
import { XIcon } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
/** Own the open state and modal focus rules for a shadcn sheet. */
|
|
10
|
+
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
|
11
|
+
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Open the sheet from a button and restore focus when it closes. */
|
|
15
|
+
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
|
16
|
+
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Close the sheet through Base UI dismissal and focus handling. */
|
|
20
|
+
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
|
21
|
+
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
|
25
|
+
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
|
29
|
+
return (
|
|
30
|
+
<SheetPrimitive.Backdrop
|
|
31
|
+
data-slot="sheet-overlay"
|
|
32
|
+
className={cn(
|
|
33
|
+
"fixed inset-0 z-50 bg-dashboard-overlay transition-opacity duration-180 motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Place the sheet on one edge with themed chrome and a close control. */
|
|
42
|
+
function SheetContent({
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
side = "right",
|
|
46
|
+
showCloseButton = true,
|
|
47
|
+
...props
|
|
48
|
+
}: SheetPrimitive.Popup.Props & {
|
|
49
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
50
|
+
showCloseButton?: boolean;
|
|
51
|
+
}) {
|
|
52
|
+
// Keep portal order tied to the app tree when a resize mounts another sheet.
|
|
53
|
+
const portalRef = React.useRef<HTMLDivElement>(null);
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<div ref={portalRef} className="contents" />
|
|
57
|
+
<SheetPortal container={portalRef}>
|
|
58
|
+
<SheetOverlay />
|
|
59
|
+
<SheetPrimitive.Popup
|
|
60
|
+
data-slot="sheet-content"
|
|
61
|
+
data-dashboard-modal=""
|
|
62
|
+
data-side={side}
|
|
63
|
+
className={cn(
|
|
64
|
+
"fixed z-50 flex flex-col gap-4 border-dashboard-border-strong bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition-[opacity,transform] duration-180 ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
{showCloseButton && (
|
|
71
|
+
<SheetPrimitive.Close
|
|
72
|
+
data-slot="sheet-close"
|
|
73
|
+
render={<Button className="absolute top-3 right-3" size="icon" />}
|
|
74
|
+
>
|
|
75
|
+
<XIcon aria-hidden="true" size={15} strokeWidth={2.25} />
|
|
76
|
+
<span className="sr-only">Close</span>
|
|
77
|
+
</SheetPrimitive.Close>
|
|
78
|
+
)}
|
|
79
|
+
</SheetPrimitive.Popup>
|
|
80
|
+
</SheetPortal>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Group the sheet title and description clear of the close control. */
|
|
86
|
+
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
data-slot="sheet-header"
|
|
90
|
+
className={cn("flex flex-col gap-1 p-4 pr-14", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Keep sheet actions at the bottom of the content. */
|
|
97
|
+
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
data-slot="sheet-footer"
|
|
101
|
+
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Give the sheet its accessible name. */
|
|
108
|
+
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
|
109
|
+
return (
|
|
110
|
+
<SheetPrimitive.Title
|
|
111
|
+
data-slot="sheet-title"
|
|
112
|
+
className={cn("text-lg font-semibold text-foreground", className)}
|
|
113
|
+
{...props}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Describe the sheet for assistive technology. */
|
|
119
|
+
function SheetDescription({
|
|
120
|
+
className,
|
|
121
|
+
...props
|
|
122
|
+
}: SheetPrimitive.Description.Props) {
|
|
123
|
+
return (
|
|
124
|
+
<SheetPrimitive.Description
|
|
125
|
+
data-slot="sheet-description"
|
|
126
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Sheet,
|
|
134
|
+
SheetTrigger,
|
|
135
|
+
SheetClose,
|
|
136
|
+
SheetContent,
|
|
137
|
+
SheetHeader,
|
|
138
|
+
SheetFooter,
|
|
139
|
+
SheetTitle,
|
|
140
|
+
SheetDescription,
|
|
141
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Dialog } from "@base-ui/react/dialog";
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
acquireBodyScrollLock,
|
|
12
|
+
releaseBodyScrollLock,
|
|
13
|
+
} from "../../bodyScrollLock";
|
|
14
|
+
|
|
15
|
+
// A resize can mount conversation details below an already open side drawer.
|
|
16
|
+
// Document order keeps the front modal in charge across these separate roots.
|
|
17
|
+
function subscribeToSheets(onChange: () => void) {
|
|
18
|
+
const observer = new MutationObserver(onChange);
|
|
19
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Keep an app sheet mounted through dismissal and share the document scroll lock. */
|
|
24
|
+
export function useSheet(
|
|
25
|
+
onClose: () => void,
|
|
26
|
+
initialFocusRef?: RefObject<HTMLElement | null>,
|
|
27
|
+
) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const closingRef = useRef(false);
|
|
30
|
+
const popupRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const topmost = useSyncExternalStore(
|
|
32
|
+
subscribeToSheets,
|
|
33
|
+
() => {
|
|
34
|
+
const modals = document.querySelectorAll("[data-dashboard-modal]");
|
|
35
|
+
const last = modals[modals.length - 1];
|
|
36
|
+
return popupRef.current ? last === popupRef.current : !last;
|
|
37
|
+
},
|
|
38
|
+
() => true,
|
|
39
|
+
);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
acquireBodyScrollLock();
|
|
42
|
+
setOpen(true);
|
|
43
|
+
return releaseBodyScrollLock;
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
rootProps: {
|
|
48
|
+
open,
|
|
49
|
+
modal: topmost ? "trap-focus" : false,
|
|
50
|
+
disablePointerDismissal: !topmost,
|
|
51
|
+
onOpenChange(nextOpen, details) {
|
|
52
|
+
if (!nextOpen && !topmost) {
|
|
53
|
+
details.cancel();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
closingRef.current = !nextOpen;
|
|
57
|
+
setOpen(nextOpen);
|
|
58
|
+
},
|
|
59
|
+
onOpenChangeComplete(nextOpen) {
|
|
60
|
+
if (!nextOpen && closingRef.current) onClose();
|
|
61
|
+
},
|
|
62
|
+
} satisfies Dialog.Root.Props,
|
|
63
|
+
contentProps: {
|
|
64
|
+
ref: popupRef,
|
|
65
|
+
"aria-modal": topmost ? true : undefined,
|
|
66
|
+
finalFocus: () => Boolean(closingRef.current && topmost),
|
|
67
|
+
initialFocus: () => {
|
|
68
|
+
const modals = document.querySelectorAll("[data-dashboard-modal]");
|
|
69
|
+
if (modals[modals.length - 1] !== popupRef.current) return false;
|
|
70
|
+
return initialFocusRef?.current ?? true;
|
|
71
|
+
},
|
|
72
|
+
} satisfies Dialog.Popup.Props,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Bot,
|
|
4
|
+
Circle,
|
|
5
|
+
CircleAlert,
|
|
6
|
+
CircleCheck,
|
|
7
|
+
CircleDot,
|
|
8
|
+
CirclePause,
|
|
9
|
+
CircleX,
|
|
10
|
+
LoaderCircle,
|
|
11
|
+
LockKeyhole,
|
|
12
|
+
type LucideIcon,
|
|
13
|
+
} from "lucide-react";
|
|
3
14
|
import {
|
|
4
15
|
conversationChildAgentsReportSchema,
|
|
5
16
|
type ConversationChildAgent,
|
|
6
17
|
} from "@sentry/junior/api/schema";
|
|
7
18
|
|
|
8
|
-
import {
|
|
19
|
+
import type { StatusChipTone } from "../components/StatusChip";
|
|
9
20
|
import {
|
|
10
21
|
formatConversationCostTotal,
|
|
11
|
-
formatRelativeTime,
|
|
12
22
|
formatRuntime,
|
|
13
23
|
formatTime,
|
|
14
24
|
formatUsageTotal,
|
|
@@ -85,9 +95,12 @@ export function ConversationChildAgents(props: {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
return (
|
|
88
|
-
<ol
|
|
98
|
+
<ol
|
|
99
|
+
aria-label="Subagents"
|
|
100
|
+
className="m-0 grid list-none gap-px overflow-hidden rounded-lg border border-dashboard-border-subtle bg-dashboard-border-subtle p-0"
|
|
101
|
+
>
|
|
89
102
|
{query.data.agents.map((agent) => (
|
|
90
|
-
<li key={agent.conversationId}>
|
|
103
|
+
<li className="bg-dashboard-surface-panel" key={agent.conversationId}>
|
|
91
104
|
<ChildAgentRow agent={agent} onOpen={props.onOpenAgent} />
|
|
92
105
|
</li>
|
|
93
106
|
))}
|
|
@@ -95,9 +108,48 @@ export function ConversationChildAgents(props: {
|
|
|
95
108
|
);
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
const statusIcons: Partial<Record<string, LucideIcon>> = {
|
|
112
|
+
active: CircleDot,
|
|
113
|
+
blocked: CircleAlert,
|
|
114
|
+
completed: CircleCheck,
|
|
115
|
+
error: CircleX,
|
|
116
|
+
failed: CircleX,
|
|
117
|
+
paused: CirclePause,
|
|
118
|
+
running: LoaderCircle,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const statusIconToneClass: Record<StatusChipTone, string> = {
|
|
122
|
+
accent: "text-violet-300",
|
|
123
|
+
danger: "text-rose-300",
|
|
124
|
+
info: "text-cyan-300",
|
|
125
|
+
neutral: "text-dashboard-text-faint",
|
|
126
|
+
success: "text-emerald-300",
|
|
127
|
+
warning: "text-amber-300",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Mark a child agent status with one icon and an audible label. */
|
|
131
|
+
function ChildAgentStatusIcon(props: { status: string }) {
|
|
132
|
+
const Icon = statusIcons[props.status] ?? Circle;
|
|
133
|
+
return (
|
|
134
|
+
<span className="mt-px grid size-4 shrink-0 place-items-center">
|
|
135
|
+
<Icon
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className={cn(
|
|
138
|
+
"size-3.5",
|
|
139
|
+
statusIconToneClass[subagentStatusTone(props.status)],
|
|
140
|
+
props.status === "running" &&
|
|
141
|
+
"animate-spin motion-reduce:animate-none",
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
<span className="sr-only">{props.status}</span>
|
|
145
|
+
</span>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
/**
|
|
99
|
-
* Show
|
|
100
|
-
*
|
|
150
|
+
* Show one child agent as a dense row: status, task title, and run time first,
|
|
151
|
+
* then agent name and cost. The row text is the accessible name, so status,
|
|
152
|
+
* title, and error stay audible.
|
|
101
153
|
*/
|
|
102
154
|
function ChildAgentRow(props: {
|
|
103
155
|
agent: ConversationChildAgent;
|
|
@@ -107,24 +159,20 @@ function ChildAgentRow(props: {
|
|
|
107
159
|
const tone = subagentStatusTone(agent.status);
|
|
108
160
|
const task = agent.task?.trim();
|
|
109
161
|
const headline = agent.title ?? task;
|
|
110
|
-
// A title is often a short form of the task. Skip the task when it adds nothing.
|
|
111
|
-
const detail = agent.title && task && task !== agent.title ? task : undefined;
|
|
112
162
|
const runtime = formatRuntime(agent.cumulativeDurationMs);
|
|
163
|
+
const name = agent.redacted ? "Private subagent" : (agent.name ?? "subagent");
|
|
113
164
|
// Totals include this subagent's own subagents. Say so, so the row does not
|
|
114
165
|
// read as the work of this subagent alone.
|
|
115
166
|
const nested = agent.childAgentTotals?.count;
|
|
116
|
-
const
|
|
167
|
+
const meta = [
|
|
117
168
|
formatUsageTotal(agent.cumulativeUsage),
|
|
118
169
|
formatConversationCostTotal(agent.cumulativeUsage, agent.auxiliaryCosts),
|
|
119
|
-
|
|
120
|
-
nested
|
|
121
|
-
? `incl. ${nested} nested ${nested === 1 ? "subagent" : "subagents"}`
|
|
122
|
-
: "",
|
|
170
|
+
nested ? `+${nested} nested` : "",
|
|
123
171
|
].filter(Boolean);
|
|
124
172
|
|
|
125
173
|
return (
|
|
126
174
|
<button
|
|
127
|
-
className="grid w-full min-w-0 cursor-pointer
|
|
175
|
+
className="grid w-full min-w-0 cursor-pointer grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-x-2.5 gap-y-0.5 px-3 py-2.5 text-left text-inherit transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
128
176
|
onClick={() =>
|
|
129
177
|
props.onOpen({
|
|
130
178
|
conversationId: agent.conversationId,
|
|
@@ -137,59 +185,40 @@ function ChildAgentRow(props: {
|
|
|
137
185
|
status: agent.status,
|
|
138
186
|
})
|
|
139
187
|
}
|
|
188
|
+
title={[
|
|
189
|
+
task && task !== headline ? task : "",
|
|
190
|
+
`Updated ${formatTime(agent.updatedAt, {
|
|
191
|
+
dateStyle: "medium",
|
|
192
|
+
timeStyle: "short",
|
|
193
|
+
})}`,
|
|
194
|
+
]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join("\n")}
|
|
140
197
|
type="button"
|
|
141
198
|
>
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<LockKeyhole aria-hidden="true" className="size-3 shrink-0" />
|
|
146
|
-
Private subagent
|
|
147
|
-
</span>
|
|
148
|
-
) : (
|
|
149
|
-
<span className="min-w-0 truncate font-medium text-dashboard-text-muted">
|
|
150
|
-
{agent.name ?? "subagent"}
|
|
151
|
-
</span>
|
|
152
|
-
)}
|
|
153
|
-
<StatusChip className="ml-auto shrink-0" size="compact" tone={tone}>
|
|
154
|
-
{agent.status}
|
|
155
|
-
</StatusChip>
|
|
156
|
-
<time
|
|
157
|
-
className="shrink-0 text-dashboard-text-faint"
|
|
158
|
-
dateTime={agent.updatedAt}
|
|
159
|
-
title={`Updated ${formatTime(agent.updatedAt, {
|
|
160
|
-
dateStyle: "medium",
|
|
161
|
-
timeStyle: "short",
|
|
162
|
-
})}`}
|
|
163
|
-
>
|
|
164
|
-
{formatRelativeTime(agent.updatedAt)}
|
|
165
|
-
</time>
|
|
199
|
+
<ChildAgentStatusIcon status={agent.status} />
|
|
200
|
+
<span className="min-w-0 truncate text-sm font-medium leading-5 text-dashboard-text">
|
|
201
|
+
{headline ?? name}
|
|
166
202
|
</span>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
203
|
+
<span className="shrink-0 text-xs leading-5 tabular-nums text-dashboard-text-muted">
|
|
204
|
+
{runtime}
|
|
205
|
+
</span>
|
|
206
|
+
<span className="col-start-2 col-end-4 flex min-w-0 items-center gap-1.5 text-xs leading-4 text-dashboard-text-muted">
|
|
207
|
+
{agent.redacted ? (
|
|
208
|
+
<LockKeyhole aria-hidden="true" className="size-3 shrink-0" />
|
|
209
|
+
) : null}
|
|
210
|
+
<span className="min-w-0 truncate">
|
|
211
|
+
{[headline ? name : "", ...meta].filter(Boolean).join(" · ")}
|
|
175
212
|
</span>
|
|
176
|
-
|
|
213
|
+
</span>
|
|
177
214
|
{agent.error ? (
|
|
178
215
|
<span
|
|
179
216
|
className={cn(
|
|
180
|
-
"
|
|
217
|
+
"col-start-2 col-end-4 line-clamp-2 min-w-0 whitespace-pre-wrap break-words pt-0.5 text-xs leading-4",
|
|
181
218
|
errorToneClass[tone] ?? "text-dashboard-text-muted",
|
|
182
219
|
)}
|
|
183
220
|
>
|
|
184
|
-
|
|
185
|
-
<span className="line-clamp-3 min-w-0 whitespace-pre-wrap break-words">
|
|
186
|
-
{agent.error}
|
|
187
|
-
</span>
|
|
188
|
-
</span>
|
|
189
|
-
) : null}
|
|
190
|
-
{stats.length > 0 ? (
|
|
191
|
-
<span className="block text-xs tabular-nums text-dashboard-text-muted">
|
|
192
|
-
{stats.join(" · ")}
|
|
221
|
+
{agent.error}
|
|
193
222
|
</span>
|
|
194
223
|
) : null}
|
|
195
224
|
</button>
|