@sentry/junior-dashboard 0.273.0 → 0.275.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 (36) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/client.js +76 -76
  3. package/dist/index.js +3 -3
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +4 -4
  6. package/src/client/App.tsx +11 -0
  7. package/src/client/components/Metric.tsx +0 -32
  8. package/src/client/components/ProfileMenu.tsx +56 -4
  9. package/src/client/components/layout/DashboardChrome.tsx +3 -3
  10. package/src/client/components/layout/DashboardHeader.tsx +41 -4
  11. package/src/client/components/layout/NavRail.tsx +43 -40
  12. package/src/client/conversations/ConversationComposer.tsx +7 -4
  13. package/src/client/conversations/ConversationDetails.tsx +117 -162
  14. package/src/client/conversations/ConversationHeader.tsx +119 -133
  15. package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
  16. package/src/client/conversations/ConversationMeta.tsx +49 -44
  17. package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
  18. package/src/client/conversations/ConversationPage.tsx +33 -368
  19. package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
  20. package/src/client/conversations/ConversationSidebar.tsx +119 -60
  21. package/src/client/conversations/ConversationWorkspace.tsx +8 -3
  22. package/src/client/conversations/TelemetryMetrics.tsx +0 -14
  23. package/src/client/conversations/TranscriptView.tsx +15 -32
  24. package/src/client/conversations/UsageCard.tsx +1 -15
  25. package/src/client/conversations/composerAutocorrect.ts +12 -0
  26. package/src/client/conversations/conversationAge.ts +24 -0
  27. package/src/client/conversations/conversationRoutes.ts +14 -5
  28. package/src/client/conversations/conversationSections.ts +66 -48
  29. package/src/client/conversations/useConversationFinishedIndicators.ts +40 -8
  30. package/src/client/pages/settings/SettingsPage.tsx +28 -0
  31. package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
  32. package/src/client/review-desk/queries.ts +30 -6
  33. package/src/mock-reporting/routes.ts +1 -1
  34. package/src/tailwind.css +4 -0
  35. package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
  36. package/src/client/review-desk/ConversationReviewBar.tsx +0 -67
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.273.0",
3
+ "version": "0.275.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.273.0"
42
+ "@sentry/junior-plugin-api": "0.275.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@sentry/junior": "0.273.0"
45
+ "@sentry/junior": "0.275.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@oxlint/plugins": "1.79.0",
@@ -63,7 +63,7 @@
63
63
  "tw-animate-css": "^1.4.0",
64
64
  "typescript": "^7.0.2",
65
65
  "vitest": "^4.1.12",
66
- "@sentry/junior": "0.273.0"
66
+ "@sentry/junior": "0.275.0"
67
67
  },
68
68
  "scripts": {
69
69
  "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",
@@ -240,6 +240,17 @@ export function DashboardShell() {
240
240
  mobileNavigationOpen={mobileNavigationOpen}
241
241
  navItems={primaryNavItems}
242
242
  onMobileNavigationOpenChange={setMobileNavigationOpen}
243
+ profile={
244
+ loggedIn ? (
245
+ <ProfileMenu
246
+ identity={data!.me}
247
+ onSignOut={signOut}
248
+ spend={personalSpendQuery.data}
249
+ userPages={userPages}
250
+ variant="top-bar"
251
+ />
252
+ ) : undefined
253
+ }
243
254
  mobileIdentity={
244
255
  loggedIn ? (
245
256
  <ProfileMenu
@@ -12,11 +12,6 @@ export type MetricTooltipLine = {
12
12
  valueStyle?: "heading";
13
13
  };
14
14
 
15
- export type MetricListItem = {
16
- content: ReactNode;
17
- key: string;
18
- };
19
-
20
15
  function TooltipLines(props: { lines: MetricTooltipLine[] }) {
21
16
  return (
22
17
  <span className="grid min-w-0 self-start grid-cols-[minmax(0,1fr)_auto] content-start gap-x-3 gap-y-1.5">
@@ -126,30 +121,3 @@ export function MetricValue(props: {
126
121
  </Tooltip>
127
122
  );
128
123
  }
129
-
130
- /** Render inline metadata with consistent dot spacing across dashboard headers. */
131
- export function MetricList(props: {
132
- className?: string;
133
- items: MetricListItem[];
134
- }) {
135
- return (
136
- <div
137
- className={cn(
138
- "flex flex-wrap items-center gap-x-1.5 gap-y-1",
139
- props.className,
140
- )}
141
- >
142
- {props.items.map((item, index) => (
143
- <span
144
- className="inline-flex min-w-0 items-center gap-x-1.5"
145
- key={item.key}
146
- >
147
- {index > 0 ? (
148
- <span className="text-dashboard-text-muted">·</span>
149
- ) : null}
150
- <span className="min-w-0">{item.content}</span>
151
- </span>
152
- ))}
153
- </div>
154
- );
155
- }
@@ -1,4 +1,11 @@
1
- import { Boxes, LogOut, Settings, ShieldCheck, UserRound } from "lucide-react";
1
+ import {
2
+ Boxes,
3
+ ChevronDown,
4
+ LogOut,
5
+ Settings,
6
+ ShieldCheck,
7
+ UserRound,
8
+ } from "lucide-react";
2
9
  import { useEffect, useRef, useState } from "react";
3
10
  import { Link } from "react-router";
4
11
  import type { PersonalSpendReport } from "@sentry/junior/api/schema";
@@ -19,11 +26,17 @@ type ProfileMenuProps = {
19
26
  userPages: PluginUserPageLink[];
20
27
  /**
21
28
  * `popover` is the avatar control at the bottom of the desktop nav rail.
29
+ * `top-bar` is the spend and avatar control in the tall desktop top bar.
22
30
  * `sheet-spend` is the vertical spend callout at the top of the mobile sheet.
23
31
  * `sheet-links` is plain account destinations for the mobile nav sheet.
24
32
  * `sheet-identity` is the quiet signed-in strip at the bottom of that sheet.
25
33
  */
26
- variant?: "popover" | "sheet-spend" | "sheet-links" | "sheet-identity";
34
+ variant?:
35
+ | "popover"
36
+ | "top-bar"
37
+ | "sheet-spend"
38
+ | "sheet-links"
39
+ | "sheet-identity";
27
40
  };
28
41
 
29
42
  const HOVER_OPEN_DELAY_MS = 80;
@@ -307,6 +320,7 @@ export function ProfileMenu({
307
320
  );
308
321
  }
309
322
 
323
+ const topBar = variant === "top-bar";
310
324
  return (
311
325
  <Popover
312
326
  onOpenChange={(nextOpen, details) => {
@@ -324,18 +338,56 @@ export function ProfileMenu({
324
338
  <PopoverTrigger
325
339
  aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
326
340
  className={cn(
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",
341
+ "cursor-pointer 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",
342
+ topBar
343
+ ? "group flex h-10 items-center gap-2 px-1.5"
344
+ : "grid size-10 place-items-center",
328
345
  open && "bg-dashboard-fill-hover text-dashboard-text",
329
346
  )}
330
347
  closeDelay={HOVER_CLOSE_DELAY_MS}
331
348
  delay={HOVER_OPEN_DELAY_MS}
332
349
  openOnHover
333
350
  >
351
+ {topBar ? (
352
+ <span className="flex items-center gap-2">
353
+ <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
354
+ <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
355
+ 7d
356
+ </span>
357
+ <span className="text-xs font-semibold text-dashboard-text">
358
+ <SpendAmount value={spend?.sevenDaysUsd} />
359
+ </span>
360
+ </span>
361
+ <span
362
+ aria-hidden="true"
363
+ className="h-3 w-px bg-dashboard-fill-strong"
364
+ />
365
+ <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
366
+ <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
367
+ 30d
368
+ </span>
369
+ <span className="text-xs font-semibold text-dashboard-text">
370
+ <SpendAmount value={spend?.thirtyDaysUsd} />
371
+ </span>
372
+ </span>
373
+ </span>
374
+ ) : null}
334
375
  <ProfileAvatar user={identity.user} />
376
+ {topBar ? (
377
+ <ChevronDown
378
+ aria-hidden="true"
379
+ className={cn(
380
+ "text-dashboard-text-muted transition-[color,transform] group-hover:text-dashboard-text",
381
+ open && "rotate-180 text-dashboard-text",
382
+ )}
383
+ size={14}
384
+ strokeWidth={2}
385
+ />
386
+ ) : null}
335
387
  </PopoverTrigger>
336
388
  <PopoverContent
337
389
  align="end"
338
- side="right"
390
+ side={topBar ? "bottom" : "right"}
339
391
  className="w-64 rounded-xl border-0 bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
340
392
  id="profile-popover"
341
393
  >
@@ -104,8 +104,8 @@ export function DashboardChromeProvider(props: { children: ReactNode }) {
104
104
  /**
105
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. On desktop the
108
- * header is hidden, so the border shows only under page links.
107
+ * underlines sit on it. Banners render below that border. Short desktop
108
+ * windows hide the header, so the border shows only under page links.
109
109
  */
110
110
  export function DashboardChrome(props: {
111
111
  banner?: ReactNode;
@@ -118,7 +118,7 @@ export function DashboardChrome(props: {
118
118
 
119
119
  return (
120
120
  <div className="sticky top-0 z-30 bg-dashboard-bg/95 pt-[env(safe-area-inset-top)]">
121
- <div className="border-b border-dashboard-border-strong md:border-b-0 md:has-[[data-secondary-slot]>*]:border-b">
121
+ <div className="border-b border-dashboard-border-strong md:border-b-0 md:has-[[data-secondary-slot]>*]:border-b md:tall:border-b">
122
122
  {props.header}
123
123
  <div
124
124
  className="hidden md:block"
@@ -3,6 +3,7 @@ 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";
6
7
  import { Sheet, SheetClose, SheetContent } from "../ui/sheet";
7
8
  import { useSheet } from "../ui/useSheet";
8
9
  import {
@@ -31,8 +32,8 @@ function NavCount(props: { count?: number }) {
31
32
  }
32
33
 
33
34
  /**
34
- * Render the mobile shell header and navigation sheet. Desktop navigation
35
- * lives in `NavRail`.
35
+ * Render the shell header: the mobile bar and navigation sheet, and the
36
+ * desktop top bar on tall windows. Short desktop windows use `NavRail`.
36
37
  */
37
38
  export function DashboardHeader(props: {
38
39
  /** Back target used on mobile conversation detail. */
@@ -47,6 +48,8 @@ export function DashboardHeader(props: {
47
48
  mobileSpend?: ReactNode;
48
49
  navItems: DashboardHeaderNavItem[];
49
50
  onMobileNavigationOpenChange(open: boolean): void;
51
+ /** Spend and account control at the right of the tall desktop top bar. */
52
+ profile?: ReactNode;
50
53
  version?: string;
51
54
  workspaceActive: boolean;
52
55
  }) {
@@ -63,6 +66,13 @@ export function DashboardHeader(props: {
63
66
  onOpenChangeRef.current(true);
64
67
  });
65
68
 
69
+ const navLinkClass = ({ isActive }: { isActive: boolean }) =>
70
+ cn(
71
+ "shrink-0 whitespace-nowrap rounded-lg px-3 py-2 font-sans text-sm font-medium tracking-normal no-underline transition-colors",
72
+ isActive
73
+ ? "bg-dashboard-accent/[0.1] text-dashboard-accent-text"
74
+ : cn("hover:bg-dashboard-fill-soft", dashboardInteractiveTextClass),
75
+ );
66
76
  const sheetLinkClass = ({ isActive }: { isActive: boolean }) =>
67
77
  cn(
68
78
  "rounded-lg px-3 py-3 font-sans text-sm font-medium no-underline transition-colors",
@@ -85,8 +95,35 @@ export function DashboardHeader(props: {
85
95
  }, [props.mobileNavigationOpen]);
86
96
 
87
97
  return (
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">
98
+ <header className="relative bg-dashboard-bg md:hidden md:tall:block">
99
+ <div className="hidden w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-5 px-7 py-3 md:tall:grid">
100
+ <Link
101
+ aria-label={`${getDashboardAgentName()} home`}
102
+ className="flex items-center text-inherit no-underline"
103
+ to="/"
104
+ >
105
+ <JuniorLogo />
106
+ </Link>
107
+ <nav aria-label="Primary" className="flex min-w-0 items-center gap-1">
108
+ <Link
109
+ aria-current={props.workspaceActive ? "page" : undefined}
110
+ className={navLinkClass({ isActive: props.workspaceActive })}
111
+ to="/"
112
+ >
113
+ Conversations
114
+ </Link>
115
+ {props.navItems.map((item) => (
116
+ <NavLink className={navLinkClass} key={item.key} to={item.to}>
117
+ {item.label}
118
+ <NavCount count={item.count} />
119
+ </NavLink>
120
+ ))}
121
+ </nav>
122
+ {props.profile ? (
123
+ <div className="justify-self-end">{props.profile}</div>
124
+ ) : null}
125
+ </div>
126
+ <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 md:hidden">
90
127
  {conversationMode && props.mobileBackTo ? (
91
128
  <Link
92
129
  aria-label="Back to conversations"
@@ -82,7 +82,7 @@ function RailItem(props: {
82
82
  {props.count ? (
83
83
  <SidebarMenuBadge
84
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"
85
+ className="top-0 right-0 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-0 peer-data-active/menu-button:text-dashboard-text-inverse peer-hover/menu-button:text-dashboard-text-inverse"
86
86
  >
87
87
  {props.count > 99 ? "99+" : props.count.toLocaleString("en-US")}
88
88
  </SidebarMenuBadge>
@@ -105,8 +105,8 @@ function RailNavItem(props: { item: DashboardHeaderNavItem }) {
105
105
  }
106
106
 
107
107
  /**
108
- * Desktop primary navigation as a shadcn icon sidebar. Mobile keeps the top
109
- * bar and navigation sheet in `DashboardHeader`.
108
+ * Desktop primary navigation as a shadcn icon sidebar on short windows. Tall
109
+ * desktop windows and mobile use `DashboardHeader`.
110
110
  */
111
111
  export function NavRail(props: {
112
112
  navItems: DashboardHeaderNavItem[];
@@ -114,43 +114,46 @@ export function NavRail(props: {
114
114
  workspaceActive: boolean;
115
115
  }) {
116
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
117
+ // Tall windows use the top bar in `DashboardHeader` instead.
118
+ <div className="contents md:tall:hidden">
119
+ <Sidebar
120
+ className="border-dashboard-border-strong pt-[env(safe-area-inset-top)]"
121
+ collapsible="icon"
122
+ >
123
+ <SidebarHeader className="items-center pt-3 pb-4">
124
+ <Link
125
+ aria-label={`${getDashboardAgentName()} home`}
126
+ className="grid place-items-center text-inherit no-underline"
127
+ to="/"
128
+ >
129
+ <JuniorLogo />
130
+ </Link>
131
+ </SidebarHeader>
132
+ {/* Stock icon mode hides overflow; the rail stays collapsed, so let
131
133
  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>
134
+ <SidebarContent className="group-data-[collapsible=icon]:overflow-auto">
135
+ <SidebarGroup className="items-center py-0">
136
+ <nav aria-label="Primary" className="w-full">
137
+ <SidebarMenu className="items-center gap-1.5">
138
+ <RailItem
139
+ active={props.workspaceActive}
140
+ icon={MessagesSquare}
141
+ label="Conversations"
142
+ to="/"
143
+ />
144
+ {props.navItems.map((item) => (
145
+ <RailNavItem item={item} key={item.key} />
146
+ ))}
147
+ </SidebarMenu>
148
+ </nav>
149
+ </SidebarGroup>
150
+ </SidebarContent>
151
+ {props.profile ? (
152
+ <SidebarFooter className="items-center pb-3">
153
+ {props.profile}
154
+ </SidebarFooter>
155
+ ) : null}
156
+ </Sidebar>
157
+ </div>
155
158
  );
156
159
  }
@@ -26,6 +26,7 @@ import {
26
26
  } from "../components/ui/tooltip";
27
27
  import { useDashboardOnline } from "../connection";
28
28
  import { cn, dashboardComposerSurfaceClass } from "../styles";
29
+ import { useComposerAutocorrect } from "./composerAutocorrect";
29
30
 
30
31
  const DRAFT_STORAGE_PREFIX = "junior:dashboard:conversation-draft:";
31
32
  // Keep keystrokes off the storage path. Mobile Safari pays for every write.
@@ -132,6 +133,7 @@ export const ConversationComposer = memo(function ConversationComposer(
132
133
  const [dragging, setDragging] = useState(false);
133
134
  const fileInputRef = useRef<HTMLInputElement>(null);
134
135
  const online = useDashboardOnline();
136
+ const [autocorrect] = useComposerAutocorrect();
135
137
  const id = useId();
136
138
  const textareaRef = useRef<HTMLTextAreaElement>(null);
137
139
  const draftRef = useRef(initialDraft);
@@ -490,10 +492,11 @@ export const ConversationComposer = memo(function ConversationComposer(
490
492
  </label>
491
493
  <textarea
492
494
  // Chat is technical: ids, paths, code. Keep the keyboard chat-like
493
- // (Send) and suppress browser IME assist that fights identifiers.
494
- autoCapitalize="off"
495
+ // (Send) and suppress browser IME assist that fights identifiers,
496
+ // unless the user turns autocorrect on in settings.
497
+ autoCapitalize={autocorrect ? "sentences" : "off"}
495
498
  autoComplete="off"
496
- autoCorrect="off"
499
+ autoCorrect={autocorrect ? "on" : "off"}
497
500
  className="min-h-12 max-h-28 w-full resize-none overflow-y-auto border-0 bg-transparent px-3 py-3 font-sans text-base leading-relaxed text-dashboard-text outline-none placeholder:text-dashboard-text-muted/65 md:min-h-14 md:max-h-[min(calc(12lh_+_1.5rem),40dvh)] md:px-4 md:py-3"
498
501
  disabled={Boolean(props.restoreDraftOnError && createPending)}
499
502
  enterKeyHint="send"
@@ -510,7 +513,7 @@ export const ConversationComposer = memo(function ConversationComposer(
510
513
  placeholder="Message Junior…"
511
514
  ref={textareaRef}
512
515
  rows={1}
513
- spellCheck={false}
516
+ spellCheck={autocorrect}
514
517
  />
515
518
  <div
516
519
  className={cn(