@sentry/junior-dashboard 0.271.0 → 0.273.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.
Files changed (46) hide show
  1. package/dist/app.js +44 -3
  2. package/dist/client.js +69 -67
  3. package/dist/index.js +40 -3
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +4 -4
  6. package/src/app.ts +2 -0
  7. package/src/client/App.tsx +440 -430
  8. package/src/client/components/JuniorLogo.tsx +1 -1
  9. package/src/client/components/ProfileMenu.tsx +4 -41
  10. package/src/client/components/layout/DashboardChrome.tsx +9 -4
  11. package/src/client/components/layout/DashboardHeader.tsx +11 -51
  12. package/src/client/components/layout/NavRail.tsx +156 -0
  13. package/src/client/components/ui/sidebar.tsx +336 -0
  14. package/src/client/conversations/ConversationPage.tsx +14 -0
  15. package/src/client/conversations/ConversationTranscript.tsx +74 -5
  16. package/src/client/conversations/PendingMailboxStack.tsx +22 -9
  17. package/src/client/conversations/TranscriptMessageShell.tsx +19 -8
  18. package/src/client/conversations/TranscriptMessageView.tsx +8 -1
  19. package/src/client/conversations/TranscriptRows.tsx +23 -0
  20. package/src/client/conversations/TranscriptTurnContextView.tsx +1 -1
  21. package/src/client/conversations/TranscriptView.tsx +22 -0
  22. package/src/client/conversations/conversationOutbox.ts +19 -0
  23. package/src/client/conversations/eventTranscript.ts +6 -0
  24. package/src/client/conversations/queries.ts +29 -20
  25. package/src/client/conversations/transcript.ts +12 -2
  26. package/src/client/conversations/transcriptBottomPinning.ts +84 -13
  27. package/src/client/conversations/transcriptReadPosition.ts +267 -0
  28. package/src/client/conversations/transcriptRenderModel.ts +16 -2
  29. package/src/client/format.ts +8 -1
  30. package/src/client/pages/automations/AutomationActions.tsx +2 -3
  31. package/src/client/pages/automations/AutomationEditor.tsx +18 -30
  32. package/src/client/pages/automations/AutomationOverview.tsx +2 -2
  33. package/src/client/pages/automations/AutomationVersionsTab.tsx +1 -1
  34. package/src/client/pages/settings/SettingsPage.tsx +62 -0
  35. package/src/client/review-desk/ReviewDeskPage.tsx +26 -40
  36. package/src/client/review-desk/ReviewDeskSidebar.tsx +29 -23
  37. package/src/client/review-desk/ReviewItemActions.tsx +1 -10
  38. package/src/client/review-desk/queries.ts +6 -5
  39. package/src/client/review-desk/reviewItemFormat.ts +3 -5
  40. package/src/client/types.ts +2 -0
  41. package/src/mock-reporting/read-positions.ts +39 -0
  42. package/src/mock-reporting/routes.ts +26 -1
  43. package/src/tailwind.css +1 -1
  44. package/src/client/components/AGENTS.md +0 -36
  45. package/src/client/conversations/AGENTS.md +0 -17
  46. package/src/client/pages/automations/AGENTS.md +0 -15
@@ -4,7 +4,7 @@ export function JuniorLogo() {
4
4
  <div className="relative grid size-10 shrink-0 select-none place-items-center overflow-visible">
5
5
  <img
6
6
  alt=""
7
- className="relative size-11 max-w-none -translate-y-0.5 object-contain opacity-90 drop-shadow-[0_0_7px_rgba(255,255,255,0.16)]"
7
+ className="relative -m-0.5 size-11 max-w-none object-contain opacity-90 drop-shadow-[0_0_7px_rgba(255,255,255,0.16)]"
8
8
  draggable={false}
9
9
  src="/_junior/dashboard/avatar.png"
10
10
  />
@@ -1,11 +1,4 @@
1
- import {
2
- Boxes,
3
- ChevronDown,
4
- LogOut,
5
- Settings,
6
- ShieldCheck,
7
- UserRound,
8
- } from "lucide-react";
1
+ import { Boxes, LogOut, Settings, ShieldCheck, UserRound } from "lucide-react";
9
2
  import { useEffect, useRef, useState } from "react";
10
3
  import { Link } from "react-router";
11
4
  import type { PersonalSpendReport } from "@sentry/junior/api/schema";
@@ -25,7 +18,7 @@ type ProfileMenuProps = {
25
18
  spend?: PersonalSpendReport;
26
19
  userPages: PluginUserPageLink[];
27
20
  /**
28
- * `popover` is the desktop header control.
21
+ * `popover` is the avatar control at the bottom of the desktop nav rail.
29
22
  * `sheet-spend` is the vertical spend callout at the top of the mobile sheet.
30
23
  * `sheet-links` is plain account destinations for the mobile nav sheet.
31
24
  * `sheet-identity` is the quiet signed-in strip at the bottom of that sheet.
@@ -331,48 +324,18 @@ export function ProfileMenu({
331
324
  <PopoverTrigger
332
325
  aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
333
326
  className={cn(
334
- "group flex h-10 cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-1.5 text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
327
+ "grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
335
328
  open && "bg-dashboard-fill-hover text-dashboard-text",
336
329
  )}
337
330
  closeDelay={HOVER_CLOSE_DELAY_MS}
338
331
  delay={HOVER_OPEN_DELAY_MS}
339
332
  openOnHover
340
333
  >
341
- <span className="hidden items-center gap-2 md:flex">
342
- <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
343
- <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
344
- 7d
345
- </span>
346
- <span className="text-xs font-semibold text-dashboard-text">
347
- <SpendAmount value={spend?.sevenDaysUsd} />
348
- </span>
349
- </span>
350
- <span
351
- aria-hidden="true"
352
- className="h-3 w-px bg-dashboard-fill-strong"
353
- />
354
- <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
355
- <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
356
- 30d
357
- </span>
358
- <span className="text-xs font-semibold text-dashboard-text">
359
- <SpendAmount value={spend?.thirtyDaysUsd} />
360
- </span>
361
- </span>
362
- </span>
363
334
  <ProfileAvatar user={identity.user} />
364
- <ChevronDown
365
- aria-hidden="true"
366
- className={cn(
367
- "hidden text-dashboard-text-muted transition-[color,transform] group-hover:text-dashboard-text md:block",
368
- open && "rotate-180 text-dashboard-text",
369
- )}
370
- size={14}
371
- strokeWidth={2}
372
- />
373
335
  </PopoverTrigger>
374
336
  <PopoverContent
375
337
  align="end"
338
+ side="right"
376
339
  className="w-64 rounded-xl border-0 bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
377
340
  id="profile-popover"
378
341
  >
@@ -102,9 +102,10 @@ export function DashboardChromeProvider(props: { children: ReactNode }) {
102
102
  }
103
103
 
104
104
  /**
105
- * Sticky shell chrome: primary header, secondary navigation slot, and banner.
105
+ * Sticky shell chrome: mobile header, secondary navigation slot, and banner.
106
106
  * One bottom border closes the header and page links, so page link
107
- * underlines sit on it. Banners render below that border.
107
+ * underlines sit on it. Banners render below that border. On desktop the
108
+ * header is hidden, so the border shows only under page links.
108
109
  */
109
110
  export function DashboardChrome(props: {
110
111
  banner?: ReactNode;
@@ -117,9 +118,13 @@ export function DashboardChrome(props: {
117
118
 
118
119
  return (
119
120
  <div className="sticky top-0 z-30 bg-dashboard-bg/95 pt-[env(safe-area-inset-top)]">
120
- <div className="border-b border-dashboard-border-strong">
121
+ <div className="border-b border-dashboard-border-strong md:border-b-0 md:has-[[data-secondary-slot]>*]:border-b">
121
122
  {props.header}
122
- <div className="hidden md:block" ref={chrome.secondarySlotRef} />
123
+ <div
124
+ className="hidden md:block"
125
+ data-secondary-slot=""
126
+ ref={chrome.secondarySlotRef}
127
+ />
123
128
  </div>
124
129
  {props.banner}
125
130
  </div>
@@ -3,7 +3,6 @@ import { useEffect, useRef, type ReactNode, type RefObject } from "react";
3
3
  import { Link, NavLink } from "react-router";
4
4
 
5
5
  import { getDashboardAgentName } from "../../agentName";
6
- import { JuniorLogo } from "../JuniorLogo";
7
6
  import { Sheet, SheetClose, SheetContent } from "../ui/sheet";
8
7
  import { useSheet } from "../ui/useSheet";
9
8
  import {
@@ -31,7 +30,10 @@ function NavCount(props: { count?: number }) {
31
30
  );
32
31
  }
33
32
 
34
- /** Render the primary dashboard shell header and optional mobile nav sheet. */
33
+ /**
34
+ * Render the mobile shell header and navigation sheet. Desktop navigation
35
+ * lives in `NavRail`.
36
+ */
35
37
  export function DashboardHeader(props: {
36
38
  /** Back target used on mobile conversation detail. */
37
39
  mobileBackTo?: string;
@@ -45,8 +47,6 @@ export function DashboardHeader(props: {
45
47
  mobileSpend?: ReactNode;
46
48
  navItems: DashboardHeaderNavItem[];
47
49
  onMobileNavigationOpenChange(open: boolean): void;
48
- /** Compact desktop header control. */
49
- profile?: ReactNode;
50
50
  version?: string;
51
51
  workspaceActive: boolean;
52
52
  }) {
@@ -63,13 +63,6 @@ export function DashboardHeader(props: {
63
63
  onOpenChangeRef.current(true);
64
64
  });
65
65
 
66
- const navLinkClass = ({ isActive }: { isActive: boolean }) =>
67
- cn(
68
- "shrink-0 whitespace-nowrap rounded-lg px-3 py-2 font-sans text-sm font-medium tracking-normal no-underline transition-colors",
69
- isActive
70
- ? "bg-dashboard-accent/[0.1] text-dashboard-accent-text"
71
- : cn("hover:bg-dashboard-fill-soft", dashboardInteractiveTextClass),
72
- );
73
66
  const sheetLinkClass = ({ isActive }: { isActive: boolean }) =>
74
67
  cn(
75
68
  "rounded-lg px-3 py-3 font-sans text-sm font-medium no-underline transition-colors",
@@ -92,19 +85,12 @@ export function DashboardHeader(props: {
92
85
  }, [props.mobileNavigationOpen]);
93
86
 
94
87
  return (
95
- <header className="relative bg-dashboard-bg">
96
- <div
97
- className={cn(
98
- "grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:gap-x-5 md:gap-y-3 md:px-7 md:py-3",
99
- props.profile
100
- ? "md:grid-cols-[auto_minmax(0,1fr)_auto]"
101
- : "md:grid-cols-[auto_minmax(0,1fr)]",
102
- )}
103
- >
88
+ <header className="relative bg-dashboard-bg md:hidden">
89
+ <div className="grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2">
104
90
  {conversationMode && props.mobileBackTo ? (
105
91
  <Link
106
92
  aria-label="Back to conversations"
107
- className="grid size-10 place-items-center rounded-lg text-dashboard-text no-underline transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70 md:hidden"
93
+ className="grid size-10 place-items-center rounded-lg text-dashboard-text no-underline transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70"
108
94
  to={props.mobileBackTo}
109
95
  >
110
96
  <ArrowLeft aria-hidden="true" size={20} strokeWidth={2} />
@@ -115,7 +101,7 @@ export function DashboardHeader(props: {
115
101
  aria-expanded={props.mobileNavigationOpen}
116
102
  aria-hidden={props.mobileNavigationOpen || undefined}
117
103
  aria-label="Open navigation"
118
- className="grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70 md:hidden"
104
+ className="grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70"
119
105
  onClick={() => props.onMobileNavigationOpenChange(true)}
120
106
  tabIndex={props.mobileNavigationOpen ? -1 : undefined}
121
107
  type="button"
@@ -124,7 +110,7 @@ export function DashboardHeader(props: {
124
110
  </button>
125
111
  )}
126
112
  {conversationMode ? (
127
- <div className="col-start-2 row-start-1 flex min-w-0 items-center gap-2 md:hidden">
113
+ <div className="col-start-2 row-start-1 flex min-w-0 items-center gap-2">
128
114
  {mobileTitle ? (
129
115
  <h1 className="m-0 min-w-0 truncate text-left font-display text-sm font-medium text-dashboard-text">
130
116
  {mobileTitle}
@@ -136,42 +122,16 @@ export function DashboardHeader(props: {
136
122
  <Link
137
123
  aria-label={`${getDashboardAgentName()} home`}
138
124
  className={cn(
139
- "col-start-2 row-start-1 min-w-0 max-w-full items-center justify-self-center text-inherit no-underline md:col-start-1 md:flex md:justify-self-start",
125
+ "col-start-2 row-start-1 min-w-0 max-w-full items-center justify-self-center text-inherit no-underline",
140
126
  conversationMode ? "hidden" : "flex",
141
127
  )}
142
128
  to="/"
143
129
  >
144
- <span className="truncate font-sans text-sm font-semibold md:hidden">
130
+ <span className="truncate font-sans text-sm font-semibold">
145
131
  {getDashboardAgentName()}
146
132
  </span>
147
- <span className="hidden md:block">
148
- <JuniorLogo />
149
- </span>
150
133
  </Link>
151
- <nav
152
- aria-label="Primary"
153
- className="hidden min-w-0 items-center gap-1 md:col-start-2 md:flex md:justify-self-start"
154
- >
155
- <Link
156
- aria-current={props.workspaceActive ? "page" : undefined}
157
- className={navLinkClass({ isActive: props.workspaceActive })}
158
- to="/"
159
- >
160
- Conversations
161
- </Link>
162
- {props.navItems.map((item) => (
163
- <NavLink className={navLinkClass} key={item.key} to={item.to}>
164
- {item.label}
165
- <NavCount count={item.count} />
166
- </NavLink>
167
- ))}
168
- </nav>
169
134
  {conversationMode ? <MobileHeaderActionsSlot /> : null}
170
- {props.profile ? (
171
- <div className="col-start-3 hidden justify-self-end md:block">
172
- {props.profile}
173
- </div>
174
- ) : null}
175
135
  </div>
176
136
  {props.mobileNavigationOpen ? (
177
137
  <MobileNavigationSheet
@@ -0,0 +1,156 @@
1
+ import {
2
+ Boxes,
3
+ Brain,
4
+ CodeXml,
5
+ Gauge,
6
+ Inbox,
7
+ MessagesSquare,
8
+ Zap,
9
+ type LucideIcon,
10
+ } from "lucide-react";
11
+ import type { CSSProperties, ReactNode } from "react";
12
+ import { Link, useMatch } from "react-router";
13
+
14
+ import { getDashboardAgentName } from "../../agentName";
15
+ import { JuniorLogo } from "../JuniorLogo";
16
+ import {
17
+ Sidebar,
18
+ SidebarContent,
19
+ SidebarFooter,
20
+ SidebarGroup,
21
+ SidebarHeader,
22
+ SidebarMenu,
23
+ SidebarMenuBadge,
24
+ SidebarMenuButton,
25
+ SidebarMenuItem,
26
+ SidebarProvider,
27
+ } from "../ui/sidebar";
28
+ import type { DashboardHeaderNavItem } from "./DashboardHeader";
29
+
30
+ // Plugin primary pages without an entry here use the generic plugin icon.
31
+ const NAV_ICONS: Record<string, LucideIcon> = {
32
+ automations: Zap,
33
+ code: CodeXml,
34
+ "memory:memories": Brain,
35
+ review: Inbox,
36
+ system: Gauge,
37
+ };
38
+
39
+ // 56px rail with 40px targets, so the icons keep their size from the top bar.
40
+ const RAIL_STYLE = { "--sidebar-width-icon": "3.5rem" } as CSSProperties;
41
+
42
+ /**
43
+ * Lay out the desktop nav rail beside the page. The rail is always the
44
+ * collapsed (icon) shadcn sidebar, and the provider's spacer keeps page
45
+ * content clear of it.
46
+ */
47
+ export function NavRailProvider(props: { children: ReactNode }) {
48
+ return (
49
+ <SidebarProvider open={false} style={RAIL_STYLE}>
50
+ {props.children}
51
+ </SidebarProvider>
52
+ );
53
+ }
54
+
55
+ function RailItem(props: {
56
+ active: boolean;
57
+ count?: number;
58
+ icon: LucideIcon;
59
+ label: string;
60
+ to: string;
61
+ }) {
62
+ const Icon = props.icon;
63
+ const label = props.count
64
+ ? `${props.label} ${props.count.toLocaleString("en-US")}`
65
+ : props.label;
66
+ return (
67
+ <SidebarMenuItem>
68
+ <SidebarMenuButton
69
+ aria-label={label}
70
+ className="justify-center rounded-lg text-dashboard-text-muted group-data-[collapsible=icon]:size-10! group-data-[collapsible=icon]:p-0! data-active:bg-dashboard-accent/[0.12] data-active:font-normal data-active:text-dashboard-accent-text [&_svg]:size-5"
71
+ isActive={props.active}
72
+ render={
73
+ <Link
74
+ aria-current={props.active ? "page" : undefined}
75
+ to={props.to}
76
+ />
77
+ }
78
+ tooltip={{ children: label, size: "compact", sideOffset: 6 }}
79
+ >
80
+ <Icon aria-hidden="true" strokeWidth={1.9} />
81
+ </SidebarMenuButton>
82
+ {props.count ? (
83
+ <SidebarMenuBadge
84
+ aria-hidden="true"
85
+ className="-top-2 -right-1.5 h-4 min-w-4 rounded-full bg-dashboard-accent px-1 text-2xs font-semibold leading-none normal-nums text-dashboard-text-inverse ring-2 ring-sidebar group-data-[collapsible=icon]:flex peer-data-[size=default]/menu-button:-top-2 peer-data-active/menu-button:text-dashboard-text-inverse peer-hover/menu-button:text-dashboard-text-inverse"
86
+ >
87
+ {props.count > 99 ? "99+" : props.count.toLocaleString("en-US")}
88
+ </SidebarMenuBadge>
89
+ ) : null}
90
+ </SidebarMenuItem>
91
+ );
92
+ }
93
+
94
+ function RailNavItem(props: { item: DashboardHeaderNavItem }) {
95
+ const active = useMatch({ end: false, path: props.item.to }) !== null;
96
+ return (
97
+ <RailItem
98
+ active={active}
99
+ count={props.item.count}
100
+ icon={NAV_ICONS[props.item.key] ?? Boxes}
101
+ label={props.item.label}
102
+ to={props.item.to}
103
+ />
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Desktop primary navigation as a shadcn icon sidebar. Mobile keeps the top
109
+ * bar and navigation sheet in `DashboardHeader`.
110
+ */
111
+ export function NavRail(props: {
112
+ navItems: DashboardHeaderNavItem[];
113
+ profile?: ReactNode;
114
+ workspaceActive: boolean;
115
+ }) {
116
+ return (
117
+ <Sidebar
118
+ className="border-dashboard-border-strong pt-[env(safe-area-inset-top)]"
119
+ collapsible="icon"
120
+ >
121
+ <SidebarHeader className="items-center pt-3 pb-4">
122
+ <Link
123
+ aria-label={`${getDashboardAgentName()} home`}
124
+ className="grid place-items-center text-inherit no-underline"
125
+ to="/"
126
+ >
127
+ <JuniorLogo />
128
+ </Link>
129
+ </SidebarHeader>
130
+ {/* Stock icon mode hides overflow; the rail stays collapsed, so let
131
+ plugin pages scroll on short windows. */}
132
+ <SidebarContent className="group-data-[collapsible=icon]:overflow-auto">
133
+ <SidebarGroup className="items-center py-0">
134
+ <nav aria-label="Primary" className="w-full">
135
+ <SidebarMenu className="items-center gap-1.5">
136
+ <RailItem
137
+ active={props.workspaceActive}
138
+ icon={MessagesSquare}
139
+ label="Conversations"
140
+ to="/"
141
+ />
142
+ {props.navItems.map((item) => (
143
+ <RailNavItem item={item} key={item.key} />
144
+ ))}
145
+ </SidebarMenu>
146
+ </nav>
147
+ </SidebarGroup>
148
+ </SidebarContent>
149
+ {props.profile ? (
150
+ <SidebarFooter className="items-center pb-3">
151
+ {props.profile}
152
+ </SidebarFooter>
153
+ ) : null}
154
+ </Sidebar>
155
+ );
156
+ }