@sentry/junior-dashboard 0.263.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.
@@ -1,31 +1,29 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
- import { Sheet, SheetBody, SheetContent } from "./Sheet";
3
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from "./ui/sheet";
4
+ import { useSheet } from "./ui/useSheet";
4
5
 
5
- /** Show details in a Base UI sheet with a swipe handle and no header controls. */
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 onClose={props.onClose} side="bottom">
13
- <SheetContent label={props.label} side="bottom">
14
- <div className="rounded-t-2xl px-4 pt-1 md:px-5">
15
- <div
16
- aria-hidden="true"
17
- data-drawer-handle=""
18
- className="flex touch-none cursor-grab justify-center py-3 active:cursor-grabbing"
19
- >
20
- <span className="h-1 w-9 rounded-full bg-dashboard-border-interactive" />
21
- </div>
22
- </div>
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
- </SheetBody>
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, SheetBody, SheetClose, SheetContent } from "./Sheet";
6
+ import { Sheet, SheetClose, SheetContent } from "./ui/sheet";
7
+ import { useSheet } from "./ui/useSheet";
7
8
 
8
- /** Render a side drawer with shared modal focus and dismissal behavior. */
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 key={props.openKey} onClose={props.onClose} side="right">
29
+ <Sheet {...sheet.rootProps}>
22
30
  <SheetContent
23
- initialFocusRef={closeRef}
24
- titleId={props.titleId}
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
- <SheetBody className="min-h-0 overflow-auto px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5">
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
- </SheetBody>
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.** `Sheet` is a local adaptation of shadcn/ui's Base UI Sheet.
84
- It uses Base UI Drawer for focus, dismissal, and native swipe support. The
85
- app's shared scroll lock keeps it in sync with the mobile conversation shell.
86
- `SheetContent` owns side placement, backdrop fade, and slide transitions.
87
- `Drawer` adds a visible header and close control. `BottomSheet` adds a swipe
88
- handle and keeps content scrolling separate from gestures. Both skip motion
89
- when reduced motion is set. Docked conversation details use a non-modal aside
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 { Bot, CircleAlert, LockKeyhole } from "lucide-react";
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 { StatusChip, type StatusChipTone } from "../components/StatusChip";
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 aria-label="Subagents" className="m-0 grid list-none gap-2 p-0">
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 who ran, what it was asked to do, and what it cost. The row text is the
100
- * accessible name, so status, title, task, and error stay audible.
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 stats = [
167
+ const meta = [
117
168
  formatUsageTotal(agent.cumulativeUsage),
118
169
  formatConversationCostTotal(agent.cumulativeUsage, agent.auxiliaryCosts),
119
- runtime ? `${runtime} runtime` : "",
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 gap-1.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5 text-left text-inherit transition-colors duration-200 hover:border-dashboard-border-emphasis hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
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
- <span className="flex min-w-0 items-center gap-2 text-xs">
143
- {agent.redacted ? (
144
- <span className="flex min-w-0 items-center gap-1.5 text-dashboard-text-muted">
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
- {headline ? (
168
- <span className="line-clamp-2 break-words text-sm font-semibold leading-snug text-dashboard-text">
169
- {headline}
170
- </span>
171
- ) : null}
172
- {detail ? (
173
- <span className="line-clamp-2 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-muted">
174
- {detail}
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
- ) : null}
213
+ </span>
177
214
  {agent.error ? (
178
215
  <span
179
216
  className={cn(
180
- "flex min-w-0 items-start gap-1.5 text-xs leading-relaxed",
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
- <CircleAlert aria-hidden="true" className="mt-0.5 size-3 shrink-0" />
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>