@sentry/junior-dashboard 0.279.0 → 0.280.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 (147) hide show
  1. package/dist/app.js +100 -31
  2. package/dist/client-manifest.json +1 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +100 -31
  5. package/dist/public/chunks/AdminPage-YEKZOKH6.js +1 -0
  6. package/dist/public/chunks/AutomationPage-5GS3IUAK.js +3 -0
  7. package/dist/public/chunks/AutomationRunsPage-IPYEZU4L.js +1 -0
  8. package/dist/public/chunks/AutomationsPage-HSAYPC3C.js +1 -0
  9. package/dist/public/chunks/CodePage-IPFKXWFB.js +1 -0
  10. package/dist/public/chunks/ConnectionsPage-5XUEK6FS.js +1 -0
  11. package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
  12. package/dist/public/chunks/LocationDetailPage-27ETUGR6.js +1 -0
  13. package/dist/public/chunks/LocationsPage-WOKMQAGM.js +1 -0
  14. package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
  15. package/dist/public/chunks/PeoplePage-6YQLDOUL.js +1 -0
  16. package/dist/public/chunks/PersonProfilePage-GYMAZFIK.js +1 -0
  17. package/dist/public/chunks/PersonalTokensPage-4BCR744B.js +1 -0
  18. package/dist/public/chunks/PluginUserPage-IP6WL7MD.js +1 -0
  19. package/dist/public/chunks/RepositoryPage-TRD44LQ3.js +1 -0
  20. package/dist/public/chunks/SettingsPage-4E3SLQ36.js +1 -0
  21. package/dist/public/chunks/SystemPage-C3XBKXKW.js +1 -0
  22. package/dist/public/chunks/WorkspaceFormPage-2LS2X67Q.js +2 -0
  23. package/dist/public/chunks/WorkspacesPage-5WB3CUTX.js +1 -0
  24. package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
  25. package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
  26. package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
  27. package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
  28. package/dist/public/chunks/chunk-564V7YHG.js +1 -0
  29. package/dist/public/chunks/chunk-5GZDBGAR.js +1 -0
  30. package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
  31. package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
  32. package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
  33. package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
  34. package/dist/public/chunks/chunk-6GQL3EVJ.js +1 -0
  35. package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
  36. package/dist/public/chunks/chunk-A2QWWA7O.js +41 -0
  37. package/dist/public/chunks/chunk-AYXSBMWA.js +1 -0
  38. package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
  39. package/dist/public/chunks/chunk-B6HGRBBX.js +1 -0
  40. package/dist/public/chunks/chunk-BKOACADP.js +1 -0
  41. package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
  42. package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
  43. package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
  44. package/dist/public/chunks/chunk-EFEREKX4.js +1 -0
  45. package/dist/public/chunks/chunk-FE522UEA.js +1 -0
  46. package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
  47. package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
  48. package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
  49. package/dist/public/chunks/chunk-GO45RQDL.js +1 -0
  50. package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
  51. package/dist/public/chunks/chunk-GRMX2HX6.js +1 -0
  52. package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
  53. package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
  54. package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
  55. package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
  56. package/dist/public/chunks/chunk-IA6BJDKN.js +1 -0
  57. package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
  58. package/dist/public/chunks/chunk-J37MXUED.js +1 -0
  59. package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
  60. package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
  61. package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
  62. package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
  63. package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
  64. package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
  65. package/dist/public/chunks/chunk-N4AONKD3.js +1 -0
  66. package/dist/public/chunks/chunk-NQMDED2R.js +2 -0
  67. package/dist/public/chunks/chunk-NQOJAZFQ.js +1 -0
  68. package/dist/public/chunks/chunk-PEQNT5MO.js +1 -0
  69. package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
  70. package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
  71. package/dist/public/chunks/chunk-QNA7F7QB.js +1 -0
  72. package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
  73. package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
  74. package/dist/public/chunks/chunk-R4ORRE77.js +1 -0
  75. package/dist/public/chunks/chunk-RHIXHBS5.js +1 -0
  76. package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
  77. package/dist/public/chunks/chunk-SDKV3OID.js +1 -0
  78. package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
  79. package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
  80. package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
  81. package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
  82. package/dist/public/chunks/chunk-UAH6DWNS.js +1 -0
  83. package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
  84. package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
  85. package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
  86. package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
  87. package/dist/public/chunks/chunk-V5UDSIRA.js +63 -0
  88. package/dist/public/chunks/chunk-VC6IS564.js +1 -0
  89. package/dist/public/chunks/chunk-VFEBNOPX.js +1 -0
  90. package/dist/public/chunks/chunk-VXT3FIVL.js +55 -0
  91. package/dist/public/chunks/chunk-WJ3OXMVX.js +1 -0
  92. package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
  93. package/dist/public/chunks/chunk-WZVO3HG4.js +1 -0
  94. package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
  95. package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
  96. package/dist/public/chunks/chunk-YYNMZ4WC.js +1 -0
  97. package/dist/public/chunks/chunk-Z2JPRGFT.js +1 -0
  98. package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
  99. package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
  100. package/dist/public/client-TYC7JKRE.js +91 -0
  101. package/dist/tailwind.css +1 -1
  102. package/package.json +5 -4
  103. package/src/api/schema.ts +1 -1
  104. package/src/assets.ts +4 -1
  105. package/src/client/App.tsx +100 -23
  106. package/src/client/TracedRoutes.tsx +8 -4
  107. package/src/client/code-highlighter.ts +26 -0
  108. package/src/client/code.tsx +2 -22
  109. package/src/client/components/Drawer.tsx +12 -71
  110. package/src/client/components/DrawerContent.tsx +76 -0
  111. package/src/client/components/ProfileMenu.tsx +12 -3
  112. package/src/client/components/UserInputForm.tsx +28 -5
  113. package/src/client/conversations/ConversationCatchUp.tsx +107 -0
  114. package/src/client/conversations/ConversationContextTags.tsx +110 -0
  115. package/src/client/conversations/ConversationEventLog.tsx +213 -71
  116. package/src/client/conversations/ConversationHeader.tsx +35 -41
  117. package/src/client/conversations/ConversationHeaderActions.tsx +34 -38
  118. package/src/client/conversations/ConversationHomeList.tsx +41 -3
  119. package/src/client/conversations/ConversationMeta.tsx +6 -12
  120. package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
  121. package/src/client/conversations/ConversationPage.tsx +188 -150
  122. package/src/client/conversations/ShareConversationButton.tsx +5 -50
  123. package/src/client/conversations/SlackMark.tsx +16 -4
  124. package/src/client/conversations/TranscriptRows.tsx +24 -11
  125. package/src/client/conversations/TranscriptView.tsx +21 -13
  126. package/src/client/conversations/conversationArchive.ts +22 -0
  127. package/src/client/conversations/conversationCatchUpState.ts +96 -0
  128. package/src/client/conversations/conversationMemory.ts +8 -1
  129. package/src/client/conversations/queries.ts +11 -3
  130. package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
  131. package/src/client/conversations/transcriptBottomPinning.ts +27 -3
  132. package/src/client/conversations/transcriptReadPosition.ts +24 -2
  133. package/src/client/conversations/turnContext.ts +1 -1
  134. package/src/client/pages/admin/AdminPage.tsx +1 -1
  135. package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
  136. package/src/client/pages/memory/memoryDashboard.ts +1 -1
  137. package/src/client/pages/memory/memoryRecord.ts +1 -1
  138. package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
  139. package/src/client/pages/user/PluginUserPage.tsx +0 -6
  140. package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
  141. package/src/client/primaryNav.ts +1 -1
  142. package/src/client/review-desk/reviewItemFormat.ts +1 -1
  143. package/src/mock-reporting/fixtures.ts +43 -0
  144. package/src/mock-reporting/routes.ts +40 -3
  145. package/src/public-assets.ts +28 -20
  146. package/src/shell.ts +12 -2
  147. package/dist/public/client-ZCZXZBTL.js +0 -460
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.279.0",
3
+ "version": "0.280.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,6 +25,7 @@
25
25
  "@base-ui/react": "^1.9.0",
26
26
  "@sentry/browser": "11.10.0",
27
27
  "@tanstack/react-query": "^5.100.14",
28
+ "@tanstack/react-virtual": "^3.14.26",
28
29
  "@uidotdev/usehooks": "2.4.1",
29
30
  "class-variance-authority": "^0.7.1",
30
31
  "cn": "^0.5.0",
@@ -41,10 +42,10 @@
41
42
  "remark-gfm": "^4.0.1",
42
43
  "shiki": "4.1.0",
43
44
  "zod": "^4.5.4",
44
- "@sentry/junior-plugin-api": "0.279.0"
45
+ "@sentry/junior-plugin-api": "0.280.0"
45
46
  },
46
47
  "peerDependencies": {
47
- "@sentry/junior": "0.279.0"
48
+ "@sentry/junior": "0.280.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@oxlint/plugins": "1.79.0",
@@ -65,7 +66,7 @@
65
66
  "tw-animate-css": "^1.4.0",
66
67
  "typescript": "^7.0.2",
67
68
  "vitest": "^4.1.12",
68
- "@sentry/junior": "0.279.0"
69
+ "@sentry/junior": "0.280.0"
69
70
  },
70
71
  "scripts": {
71
72
  "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
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
 
3
3
  const dashboardSessionUserSchema = z
4
4
  .object({
package/src/assets.ts CHANGED
@@ -1,4 +1,7 @@
1
- export const dashboardClientFile = "";
1
+ import type { DashboardClientManifest } from "./public-assets";
2
+
3
+ export const dashboardClientManifest: DashboardClientManifest | undefined =
4
+ undefined;
2
5
  export const dashboardPublicAssets: Readonly<Record<string, string>> = {};
3
6
  export const dashboardTailwindAsset = "";
4
7
  export const dashboardAvatarHeaderAsset = "";
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState, type ReactNode } from "react";
1
+ import { lazy, useEffect, useMemo, useState, type ReactNode } from "react";
2
2
  import { Navigate, Route, useLocation, useParams } from "react-router";
3
3
 
4
4
  import {
@@ -36,41 +36,20 @@ import { ConversationWorkspaceLoading } from "./conversations/ConversationWorksp
36
36
  import { useConversationData } from "./conversations/queries";
37
37
  import { readConversationStartPosition } from "./conversations/transcriptReadPosition";
38
38
  import { codeRepositoryTabs } from "./pages/code/codeRepositoryRoutes";
39
- import { RepositoryPage } from "./pages/code/RepositoryPage";
40
- import { CodePage } from "./pages/code/CodePage";
41
- import { LocationDetailPage } from "./pages/locations/LocationDetailPage";
42
- import { LocationsPage } from "./pages/locations/LocationsPage";
43
- import { PeoplePage } from "./pages/people/PeoplePage";
44
- import { PersonProfilePage } from "./pages/people/PersonProfilePage";
45
- import { ConnectionsPage } from "./pages/settings/ConnectionsPage";
46
- import { McpSettingsPage } from "./pages/settings/McpSettingsPage";
47
- import { PersonalTokensPage } from "./pages/settings/PersonalTokensPage";
48
- import { SettingsPage } from "./pages/settings/SettingsPage";
49
39
  import { SettingsPageLayout } from "./pages/settings/SettingsPageLayout";
50
- import { SystemPage } from "./pages/system/SystemPage";
51
40
  import {
52
41
  SystemPageLayout,
53
42
  SystemRouteLoading,
54
43
  } from "./pages/system/SystemPageLayout";
55
- import { WorkspaceFormPage } from "./pages/system/WorkspaceFormPage";
56
- import { WorkspacesPage } from "./pages/system/WorkspacesPage";
57
44
  import {
58
45
  isMemoryLibraryPath,
59
46
  MemoryRouteLoading,
60
47
  } from "./pages/memory/MemoryPageLayout";
61
- import { AutomationPage } from "./pages/automations/AutomationPage";
62
- import { AutomationRunsPage } from "./pages/automations/AutomationRunsPage";
63
- import { AutomationsPage } from "./pages/automations/AutomationsPage";
64
48
  import {
65
49
  AutomationsPageLayout,
66
50
  hasAutomationDetailsFrame,
67
51
  AutomationsRouteLoading,
68
52
  } from "./pages/automations/AutomationsPageLayout";
69
- import {
70
- MemoryPermalinkRoute,
71
- PluginUserPageRoute,
72
- } from "./pages/user/PluginUserPage";
73
- import { AdminPage } from "./pages/admin/AdminPage";
74
53
  import { buildPrimaryNavItems } from "./primaryNav";
75
54
  import { TracedRoutes } from "./TracedRoutes";
76
55
  import {
@@ -80,6 +59,104 @@ import {
80
59
  import { useReviewItemCountData } from "./review-desk/queries";
81
60
  import type { DashboardCoreData } from "./types";
82
61
 
62
+ // Pages outside the conversation workspace load on first visit, so the
63
+ // workspace's first paint skips their code and the chart library.
64
+ const RepositoryPage = lazy(() =>
65
+ import("./pages/code/RepositoryPage").then((module) => ({
66
+ default: module.RepositoryPage,
67
+ })),
68
+ );
69
+ const CodePage = lazy(() =>
70
+ import("./pages/code/CodePage").then((module) => ({
71
+ default: module.CodePage,
72
+ })),
73
+ );
74
+ const LocationDetailPage = lazy(() =>
75
+ import("./pages/locations/LocationDetailPage").then((module) => ({
76
+ default: module.LocationDetailPage,
77
+ })),
78
+ );
79
+ const LocationsPage = lazy(() =>
80
+ import("./pages/locations/LocationsPage").then((module) => ({
81
+ default: module.LocationsPage,
82
+ })),
83
+ );
84
+ const PeoplePage = lazy(() =>
85
+ import("./pages/people/PeoplePage").then((module) => ({
86
+ default: module.PeoplePage,
87
+ })),
88
+ );
89
+ const PersonProfilePage = lazy(() =>
90
+ import("./pages/people/PersonProfilePage").then((module) => ({
91
+ default: module.PersonProfilePage,
92
+ })),
93
+ );
94
+ const ConnectionsPage = lazy(() =>
95
+ import("./pages/settings/ConnectionsPage").then((module) => ({
96
+ default: module.ConnectionsPage,
97
+ })),
98
+ );
99
+ const McpSettingsPage = lazy(() =>
100
+ import("./pages/settings/McpSettingsPage").then((module) => ({
101
+ default: module.McpSettingsPage,
102
+ })),
103
+ );
104
+ const PersonalTokensPage = lazy(() =>
105
+ import("./pages/settings/PersonalTokensPage").then((module) => ({
106
+ default: module.PersonalTokensPage,
107
+ })),
108
+ );
109
+ const SettingsPage = lazy(() =>
110
+ import("./pages/settings/SettingsPage").then((module) => ({
111
+ default: module.SettingsPage,
112
+ })),
113
+ );
114
+ const SystemPage = lazy(() =>
115
+ import("./pages/system/SystemPage").then((module) => ({
116
+ default: module.SystemPage,
117
+ })),
118
+ );
119
+ const WorkspaceFormPage = lazy(() =>
120
+ import("./pages/system/WorkspaceFormPage").then((module) => ({
121
+ default: module.WorkspaceFormPage,
122
+ })),
123
+ );
124
+ const WorkspacesPage = lazy(() =>
125
+ import("./pages/system/WorkspacesPage").then((module) => ({
126
+ default: module.WorkspacesPage,
127
+ })),
128
+ );
129
+ const AutomationPage = lazy(() =>
130
+ import("./pages/automations/AutomationPage").then((module) => ({
131
+ default: module.AutomationPage,
132
+ })),
133
+ );
134
+ const AutomationRunsPage = lazy(() =>
135
+ import("./pages/automations/AutomationRunsPage").then((module) => ({
136
+ default: module.AutomationRunsPage,
137
+ })),
138
+ );
139
+ const AutomationsPage = lazy(() =>
140
+ import("./pages/automations/AutomationsPage").then((module) => ({
141
+ default: module.AutomationsPage,
142
+ })),
143
+ );
144
+ const MemoryPermalinkRoute = lazy(() =>
145
+ import("./pages/user/PluginUserPage").then((module) => ({
146
+ default: module.MemoryPermalinkRoute,
147
+ })),
148
+ );
149
+ const PluginUserPageRoute = lazy(() =>
150
+ import("./pages/user/PluginUserPage").then((module) => ({
151
+ default: module.PluginUserPageRoute,
152
+ })),
153
+ );
154
+ const AdminPage = lazy(() =>
155
+ import("./pages/admin/AdminPage").then((module) => ({
156
+ default: module.AdminPage,
157
+ })),
158
+ );
159
+
83
160
  const dashboardNoise = {
84
161
  backgroundImage:
85
162
  "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitchTiles'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.7'/%3E%3C/svg%3E\")",
@@ -295,7 +372,7 @@ export function DashboardShell() {
295
372
  />
296
373
  }
297
374
  />
298
- <TracedRoutes>
375
+ <TracedRoutes fallback={<LoadingView label="Loading page" />}>
299
376
  <Route element={<LegacyAutomationsRedirect />} path="/tasks" />
300
377
  <Route element={<LegacyAutomationsRedirect />} path="/tasks/*" />
301
378
  <Route
@@ -1,4 +1,4 @@
1
- import { useLayoutEffect, type ReactNode } from "react";
1
+ import { Suspense, useLayoutEffect, type ReactNode } from "react";
2
2
  import {
3
3
  createRoutesFromChildren,
4
4
  matchRoutes,
@@ -23,9 +23,13 @@ function routePattern(matches: RouteMatch[]): string {
23
23
  /**
24
24
  * Render `<Route>` children like `<Routes>`, and name the open pageload or
25
25
  * navigation span after the route pattern that matched, such as
26
- * `/conversations/:conversationId`.
26
+ * `/conversations/:conversationId`. Lazy pages show `fallback` while their
27
+ * code loads.
27
28
  */
28
- export function TracedRoutes(props: { children: ReactNode }) {
29
+ export function TracedRoutes(props: {
30
+ children: ReactNode;
31
+ fallback: ReactNode;
32
+ }) {
29
33
  const location = useLocation();
30
34
  const routes = createRoutesFromChildren(props.children);
31
35
  const element = useRoutes(routes, location);
@@ -36,5 +40,5 @@ export function TracedRoutes(props: { children: ReactNode }) {
36
40
  useLayoutEffect(() => {
37
41
  if (pattern) nameRouteSpan(pattern);
38
42
  }, [location.key, pattern]);
39
- return element;
43
+ return <Suspense fallback={props.fallback}>{element}</Suspense>;
40
44
  }
@@ -0,0 +1,26 @@
1
+ import {
2
+ createBundledHighlighter,
3
+ createSingletonShorthands,
4
+ guessEmbeddedLanguages,
5
+ } from "shiki/core";
6
+
7
+ import { dashboardCodeLanguages } from "./code-languages";
8
+
9
+ /**
10
+ * Highlight code with the dashboard languages and themes. Themes, grammars,
11
+ * and the engine load in on-demand chunks.
12
+ */
13
+ export const { codeToHtml } = createSingletonShorthands(
14
+ createBundledHighlighter({
15
+ langs: dashboardCodeLanguages,
16
+ themes: {
17
+ "github-dark": () => import("shiki/themes/github-dark.mjs"),
18
+ "github-light": () => import("shiki/themes/github-light.mjs"),
19
+ },
20
+ engine: async () => {
21
+ const { createOnigurumaEngine } = await import("shiki/engine/oniguruma");
22
+ return createOnigurumaEngine(import("shiki/wasm"));
23
+ },
24
+ }),
25
+ { guessEmbeddedLanguages },
26
+ );
@@ -1,35 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import type { BundledLanguage } from "shiki/bundle/web";
4
- import {
5
- createBundledHighlighter,
6
- createSingletonShorthands,
7
- guessEmbeddedLanguages,
8
- } from "shiki/core";
9
4
 
10
5
  import { cn } from "./styles";
11
- import { dashboardCodeLanguages } from "./code-languages";
12
6
  import {
13
7
  buildSearchDecorations,
14
8
  useTranscriptSearch,
15
9
  } from "./conversations/transcriptSearch";
16
10
 
17
- // Split themes, language grammars, and the engine into on-demand chunks.
18
- const { codeToHtml } = createSingletonShorthands(
19
- createBundledHighlighter({
20
- langs: dashboardCodeLanguages,
21
- themes: {
22
- "github-dark": () => import("shiki/themes/github-dark.mjs"),
23
- "github-light": () => import("shiki/themes/github-light.mjs"),
24
- },
25
- engine: async () => {
26
- const { createOnigurumaEngine } = await import("shiki/engine/oniguruma");
27
- return createOnigurumaEngine(import("shiki/wasm"));
28
- },
29
- }),
30
- { guessEmbeddedLanguages },
31
- );
32
-
33
11
  declare const shikiHtmlBrand: unique symbol;
34
12
 
35
13
  /** HTML generated by Shiki and safe to pass to the highlighted code shell. */
@@ -51,6 +29,8 @@ export function HighlightedCode(props: {
51
29
  const decorations = normalizedQuery
52
30
  ? buildSearchDecorations(props.code, normalizedQuery)
53
31
  : [];
32
+ // Shiki loads on the first code block, not with the dashboard entry.
33
+ const { codeToHtml } = await import("./code-highlighter");
54
34
  return (await codeToHtml(props.code, {
55
35
  decorations: decorations.length ? decorations : undefined,
56
36
  lang: props.language,
@@ -1,82 +1,23 @@
1
- import { X } from "lucide-react";
2
- import { useRef, type ReactNode } from "react";
1
+ import { lazy, Suspense } from "react";
3
2
 
4
- import { cn } from "../styles";
5
- import { Button } from "./ui/button";
6
- import {
7
- Drawer as DrawerRoot,
8
- DrawerClose,
9
- DrawerContent as DrawerPopup,
10
- } from "./ui/drawer";
11
- import { useSheet } from "./ui/useSheet";
3
+ import type { DrawerProps } from "./DrawerContent";
12
4
 
13
- type DrawerProps = {
14
- actions?: ReactNode;
15
- children: ReactNode;
16
- closeLabel: string;
17
- /** Remove the body inset, so content such as the event log meets the edges. */
18
- flush?: boolean;
19
- header: ReactNode;
20
- onClose(): void;
21
- openKey: string;
22
- width?: "default" | "wide" | "narrow";
23
- } & ({ titleId: string; label?: never } | { label: string; titleId?: never });
5
+ // A drawer opens only after a click, so base-ui's drawer loads with the first
6
+ // open instead of with the page.
7
+ const DrawerContent = lazy(() =>
8
+ import("./DrawerContent").then((module) => ({
9
+ default: module.DrawerContent,
10
+ })),
11
+ );
24
12
 
25
13
  /**
26
14
  * Render a side drawer with shared modal focus and dismissal behavior.
27
15
  * A drawer opened from inside another drawer stacks in front of it.
28
16
  */
29
17
  export function Drawer(props: DrawerProps) {
30
- return <DrawerContent key={props.openKey} {...props} />;
31
- }
32
-
33
- function DrawerContent(props: DrawerProps) {
34
- const closeRef = useRef<HTMLButtonElement>(null);
35
- const sheet = useSheet(props.onClose, closeRef);
36
18
  return (
37
- <DrawerRoot {...sheet.rootProps} swipeDirection="right">
38
- <DrawerPopup
39
- {...sheet.contentProps}
40
- aria-label={props.label}
41
- aria-labelledby={props.titleId}
42
- data-dashboard-modal=""
43
- className={cn(
44
- "data-[swipe-axis=x]:[--drawer-content-width:100%] data-[swipe-axis=x]:sm:[--drawer-content-width:100%]",
45
- props.width === "wide"
46
- ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(760px,94vw)]"
47
- : props.width === "narrow"
48
- ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(440px,94vw)]"
49
- : "data-[swipe-axis=x]:md:[--drawer-content-width:min(560px,94vw)]",
50
- )}
51
- >
52
- <header className="flex shrink-0 items-start 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">
53
- <div className="min-w-0 flex-1">{props.header}</div>
54
- <div className="flex shrink-0 items-start gap-1.5">
55
- {props.actions}
56
- <DrawerClose
57
- render={
58
- <Button
59
- ref={closeRef}
60
- aria-label={props.closeLabel}
61
- size="icon"
62
- />
63
- }
64
- >
65
- <X aria-hidden="true" size={15} strokeWidth={2.25} />
66
- </DrawerClose>
67
- </div>
68
- </header>
69
- <div
70
- className={cn(
71
- "min-h-0 flex-1 overflow-auto",
72
- props.flush
73
- ? "pb-[env(safe-area-inset-bottom)]"
74
- : "px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5",
75
- )}
76
- >
77
- {props.children}
78
- </div>
79
- </DrawerPopup>
80
- </DrawerRoot>
19
+ <Suspense fallback={null}>
20
+ <DrawerContent key={props.openKey} {...props} />
21
+ </Suspense>
81
22
  );
82
23
  }
@@ -0,0 +1,76 @@
1
+ import { X } from "lucide-react";
2
+ import { useRef, type ReactNode } from "react";
3
+
4
+ import { cn } from "../styles";
5
+ import { Button } from "./ui/button";
6
+ import {
7
+ Drawer as DrawerRoot,
8
+ DrawerClose,
9
+ DrawerContent as DrawerPopup,
10
+ } from "./ui/drawer";
11
+ import { useSheet } from "./ui/useSheet";
12
+
13
+ /** Props of a side drawer. */
14
+ export type DrawerProps = {
15
+ actions?: ReactNode;
16
+ children: ReactNode;
17
+ closeLabel: string;
18
+ /** Remove the body inset, so content such as the event log meets the edges. */
19
+ flush?: boolean;
20
+ header: ReactNode;
21
+ onClose(): void;
22
+ openKey: string;
23
+ width?: "default" | "wide" | "narrow";
24
+ } & ({ titleId: string; label?: never } | { label: string; titleId?: never });
25
+
26
+ /** Render the open side drawer. `Drawer` loads this module on demand. */
27
+ export function DrawerContent(props: DrawerProps) {
28
+ const closeRef = useRef<HTMLButtonElement>(null);
29
+ const sheet = useSheet(props.onClose, closeRef);
30
+ return (
31
+ <DrawerRoot {...sheet.rootProps} swipeDirection="right">
32
+ <DrawerPopup
33
+ {...sheet.contentProps}
34
+ aria-label={props.label}
35
+ aria-labelledby={props.titleId}
36
+ data-dashboard-modal=""
37
+ className={cn(
38
+ "data-[swipe-axis=x]:[--drawer-content-width:100%] data-[swipe-axis=x]:sm:[--drawer-content-width:100%]",
39
+ props.width === "wide"
40
+ ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(760px,94vw)]"
41
+ : props.width === "narrow"
42
+ ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(440px,94vw)]"
43
+ : "data-[swipe-axis=x]:md:[--drawer-content-width:min(560px,94vw)]",
44
+ )}
45
+ >
46
+ <header className="flex shrink-0 items-start 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">
47
+ <div className="min-w-0 flex-1">{props.header}</div>
48
+ <div className="flex shrink-0 items-start gap-1.5">
49
+ {props.actions}
50
+ <DrawerClose
51
+ render={
52
+ <Button
53
+ ref={closeRef}
54
+ aria-label={props.closeLabel}
55
+ size="icon"
56
+ />
57
+ }
58
+ >
59
+ <X aria-hidden="true" size={15} strokeWidth={2.25} />
60
+ </DrawerClose>
61
+ </div>
62
+ </header>
63
+ <div
64
+ className={cn(
65
+ "min-h-0 flex-1 overflow-auto",
66
+ props.flush
67
+ ? "pb-[env(safe-area-inset-bottom)]"
68
+ : "px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5",
69
+ )}
70
+ >
71
+ {props.children}
72
+ </div>
73
+ </DrawerPopup>
74
+ </DrawerRoot>
75
+ );
76
+ }
@@ -12,7 +12,7 @@ import type { PersonalSpendReport } from "@sentry/junior/api/schema";
12
12
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
13
13
 
14
14
  import { formatCostSummary, peoplePath } from "../format";
15
- import { pluginUserPagePath } from "../pages/user/PluginUserPage";
15
+ import { pluginUserPagePath } from "../pages/user/pluginUserPageRoutes";
16
16
  import { cn, dashboardInteractiveTextClass } from "../styles";
17
17
  import type { Identity } from "../types";
18
18
  import { ThemeSwitcher } from "./ThemeSwitcher";
@@ -170,7 +170,11 @@ export function ProfileMenu({
170
170
  ) : null}
171
171
  </div>
172
172
  </div>
173
- <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 font-sans text-xs tabular-nums text-dashboard-text-muted">
173
+ <div
174
+ aria-label="Personal model spend"
175
+ className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 font-sans text-xs tabular-nums text-dashboard-text-muted"
176
+ role="group"
177
+ >
174
178
  <span>
175
179
  7d{" "}
176
180
  <span className="font-semibold text-dashboard-text">
@@ -320,6 +324,7 @@ export function ProfileMenu({
320
324
  }
321
325
 
322
326
  const topBar = variant === "top-bar";
327
+ const menuLabel = `${open ? "Close" : "Open"} profile menu for ${name}`;
323
328
  return (
324
329
  <Popover
325
330
  onOpenChange={(nextOpen, details) => {
@@ -335,7 +340,11 @@ export function ProfileMenu({
335
340
  open={open}
336
341
  >
337
342
  <PopoverTrigger
338
- aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
343
+ aria-label={
344
+ topBar
345
+ ? `${menuLabel}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`
346
+ : menuLabel
347
+ }
339
348
  className={cn(
340
349
  "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",
341
350
  topBar
@@ -6,6 +6,7 @@ import {
6
6
  useState,
7
7
  type FormEvent,
8
8
  type ReactNode,
9
+ type Ref,
9
10
  } from "react";
10
11
  import {
11
12
  buttonUserInputField,
@@ -161,6 +162,7 @@ function TextField(props: {
161
162
  disabled: boolean;
162
163
  label: string;
163
164
  onChange(value: string): void;
165
+ ref?: Ref<HTMLTextAreaElement>;
164
166
  value: string;
165
167
  }) {
166
168
  const id = useId();
@@ -178,6 +180,7 @@ function TextField(props: {
178
180
  disabled={props.disabled}
179
181
  id={id}
180
182
  onChange={(event) => props.onChange(event.target.value)}
183
+ ref={props.ref}
181
184
  value={props.value}
182
185
  />
183
186
  </Field>
@@ -188,7 +191,9 @@ function TextField(props: {
188
191
  * Render the answer controls of an action item request. A request with
189
192
  * one short choice field shows one button per choice, and a click is the
190
193
  * answer. Choices with descriptions stack as rows, so each description stays
191
- * with its choice. Other requests show a form that the reader submits once.
194
+ * with its choice. Other… turns the buttons into the form, so the reader can
195
+ * add a note or answer in their own words. Other requests show a form that
196
+ * the reader submits once.
192
197
  * A failed `onSubmit` lets the reader try again. `secondaryActions`, such as
193
198
  * Dismiss, sit after the answer controls. `onUnsentChange` reports whether
194
199
  * the form has picks or text that the reader has not sent.
@@ -206,7 +211,15 @@ export function UserInputForm(props: {
206
211
  const choicesId = useId();
207
212
  const [values, setValues] = useState<Record<string, string[]>>({});
208
213
  const [other, setOther] = useState("");
209
- const buttonField = buttonUserInputField(props.request);
214
+ const [writeOther, setWriteOther] = useState(false);
215
+ const buttonField = writeOther
216
+ ? undefined
217
+ : buttonUserInputField(props.request);
218
+ // Other… unmounts the button that had focus, so move focus to the note.
219
+ const noteRef = useRef<HTMLTextAreaElement>(null);
220
+ useEffect(() => {
221
+ if (writeOther) noteRef.current?.focus();
222
+ }, [writeOther]);
210
223
 
211
224
  // A ref blocks a second click before React renders the disabled controls.
212
225
  // The lock stays after a sent answer; only a failed send can retry.
@@ -260,6 +273,13 @@ export function UserInputForm(props: {
260
273
  {props.secondaryActions}
261
274
  </div>
262
275
  ) : null;
276
+ // Slack cards show the same button, so both surfaces take a free-text
277
+ // answer when no choice fits.
278
+ const otherButton = (
279
+ <Button disabled={sending} onClick={() => setWriteOther(true)}>
280
+ Other…
281
+ </Button>
282
+ );
263
283
 
264
284
  return (
265
285
  <>
@@ -308,9 +328,10 @@ export function UserInputForm(props: {
308
328
  </button>
309
329
  ))}
310
330
  </div>
311
- {secondary ? (
312
- <div className="mt-3 flex flex-wrap items-center">{secondary}</div>
313
- ) : null}
331
+ <div className="mt-3 flex flex-wrap items-center gap-2">
332
+ {otherButton}
333
+ {secondary}
334
+ </div>
314
335
  </>
315
336
  ) : buttonField ? (
316
337
  <div className="mt-3 flex flex-wrap items-center gap-2">
@@ -325,6 +346,7 @@ export function UserInputForm(props: {
325
346
  {option.recommended ? <SuggestedLabel className="ml-0" /> : null}
326
347
  </Button>
327
348
  ))}
349
+ {otherButton}
328
350
  {secondary}
329
351
  </div>
330
352
  ) : (
@@ -359,6 +381,7 @@ export function UserInputForm(props: {
359
381
  disabled={sending}
360
382
  label={USER_INPUT_NOTE.label}
361
383
  onChange={setOther}
384
+ ref={noteRef}
362
385
  value={other}
363
386
  />
364
387
  <div className="flex flex-wrap items-center gap-2">