@sentry/junior-dashboard 0.264.0 → 0.266.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 (213) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +145 -136
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +5 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.264.0",
3
+ "version": "0.266.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,8 +23,8 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@base-ui/react": "^1.9.0",
26
- "@radix-ui/react-hover-card": "^1.1.23",
27
26
  "@tanstack/react-query": "^5.100.14",
27
+ "@uidotdev/usehooks": "2.4.1",
28
28
  "class-variance-authority": "^0.7.1",
29
29
  "cn": "^0.5.0",
30
30
  "hono": "^4.13.11",
@@ -34,10 +34,11 @@
34
34
  "react-dom": "^19.2.6",
35
35
  "react-is": "^19.2.6",
36
36
  "react-router": "^7.18.2",
37
+ "recharts": "3.8.0",
37
38
  "shiki": "4.1.0",
38
39
  "zod": "^4.5.4",
39
- "@sentry/junior": "0.264.0",
40
- "@sentry/junior-plugin-api": "0.264.0"
40
+ "@sentry/junior": "0.266.0",
41
+ "@sentry/junior-plugin-api": "0.266.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@oxlint/plugins": "1.79.0",
package/src/api/schema.ts CHANGED
@@ -37,7 +37,6 @@ export const dashboardConfigSchema = z
37
37
  authRequired: z.boolean(),
38
38
  authPath: z.string(),
39
39
  basePath: z.string(),
40
- componentGallery: z.boolean(),
41
40
  sentryConversationLinks: z.boolean(),
42
41
  timeZone: z.string().optional(),
43
42
  version: z.string().min(1),
package/src/app.ts CHANGED
@@ -79,7 +79,6 @@ export interface JuniorDashboardOptions {
79
79
  authPath?: string;
80
80
  authRequired?: boolean;
81
81
  auth?: DashboardAuth;
82
- componentGallery?: boolean;
83
82
  mockConversations?: boolean;
84
83
  }
85
84
 
@@ -129,9 +128,6 @@ function resolveDashboardOptions(
129
128
  ...options,
130
129
  authRequired:
131
130
  options.authRequired ?? readEnvFlag("JUNIOR_DASHBOARD_AUTH_REQUIRED"),
132
- componentGallery:
133
- options.componentGallery ??
134
- readEnvFlag("JUNIOR_DASHBOARD_COMPONENT_GALLERY"),
135
131
  mockConversations:
136
132
  options.mockConversations ??
137
133
  readEnvFlag("JUNIOR_DASHBOARD_MOCK_CONVERSATIONS"),
@@ -142,12 +138,8 @@ function isJsonRoute(pathname: string): boolean {
142
138
  return pathname.startsWith("/api/");
143
139
  }
144
140
 
145
- function isDashboardPagePath(
146
- pathname: string,
147
- basePath: string,
148
- options: { componentGallery?: boolean } = {},
149
- ): boolean {
150
- for (const { nested, path } of dashboardPagePaths(basePath, options)) {
141
+ function isDashboardPagePath(pathname: string, basePath: string): boolean {
142
+ for (const { nested, path } of dashboardPagePaths(basePath)) {
151
143
  if (pathname === path || (nested && pathname.startsWith(`${path}/`))) {
152
144
  return true;
153
145
  }
@@ -158,7 +150,6 @@ function isDashboardPagePath(
158
150
 
159
151
  interface DashboardReturnPathOptions {
160
152
  authenticatedRoutes?: readonly AuthenticatedRoute[];
161
- componentGallery?: boolean;
162
153
  }
163
154
 
164
155
  function dashboardReturnPath(
@@ -167,7 +158,7 @@ function dashboardReturnPath(
167
158
  options: DashboardReturnPathOptions = {},
168
159
  ): string | undefined {
169
160
  if (
170
- !isDashboardPagePath(url.pathname, basePath, options) &&
161
+ !isDashboardPagePath(url.pathname, basePath) &&
171
162
  !isAuthPath(url.pathname, options.authenticatedRoutes ?? [])
172
163
  ) {
173
164
  return undefined;
@@ -190,7 +181,7 @@ function requestedReturnPath(
190
181
  const returnUrl = new URL(next, url.origin);
191
182
  if (
192
183
  returnUrl.origin !== url.origin ||
193
- (!isDashboardPagePath(returnUrl.pathname, basePath, options) &&
184
+ (!isDashboardPagePath(returnUrl.pathname, basePath) &&
194
185
  !isAuthPath(returnUrl.pathname, options.authenticatedRoutes ?? []))
195
186
  ) {
196
187
  return undefined;
@@ -399,7 +390,6 @@ export function createDashboardApp(
399
390
  if (canonicalUrl) return Response.redirect(canonicalUrl, 302);
400
391
  const returnUrl = callbackUrl(c.req.raw, basePath, {
401
392
  authenticatedRoutes,
402
- componentGallery: options.componentGallery,
403
393
  });
404
394
  if (!authRequired || !auth) {
405
395
  return Response.redirect(returnUrl, 302);
@@ -471,7 +461,6 @@ export function createDashboardApp(
471
461
  if (!auth) {
472
462
  return unauthorized(c.req.raw, basePath, canonicalBaseURL, {
473
463
  authenticatedRoutes,
474
- componentGallery: options.componentGallery,
475
464
  });
476
465
  }
477
466
  const browserSession = await auth.getSession(c.req.raw);
@@ -491,7 +480,6 @@ export function createDashboardApp(
491
480
  if (!session) {
492
481
  return unauthorized(c.req.raw, basePath, canonicalBaseURL, {
493
482
  authenticatedRoutes,
494
- componentGallery: options.componentGallery,
495
483
  });
496
484
  }
497
485
  if (!dashboardSessionIsAuthorized(session, allowedDomains)) {
@@ -543,9 +531,7 @@ export function createDashboardApp(
543
531
  }
544
532
  }
545
533
 
546
- for (const { nested, path } of dashboardPagePaths(basePath, {
547
- componentGallery: options.componentGallery,
548
- })) {
534
+ for (const { nested, path } of dashboardPagePaths(basePath)) {
549
535
  app.get(path, () => renderDashboard(basePath, agentName));
550
536
  if (nested) {
551
537
  app.get(`${path}/*`, () => renderDashboard(basePath, agentName));
@@ -580,7 +566,6 @@ export function createDashboardApp(
580
566
  authRequired,
581
567
  authPath,
582
568
  basePath,
583
- componentGallery: options.componentGallery === true,
584
569
  sentryConversationLinks: hasSentryConversationLinks(),
585
570
  timeZone: dashboardTimeZone(),
586
571
  version: JUNIOR_VERSION,
@@ -31,7 +31,6 @@ import { useDashboardServerVersion } from "./dashboard-version";
31
31
  import { ConversationWorkspace } from "./conversations/ConversationWorkspace";
32
32
  import { ConversationWorkspaceLoading } from "./conversations/ConversationWorkspaceLoading";
33
33
  import { useConversationData } from "./conversations/queries";
34
- import { ComponentsPage } from "./pages/dev/ComponentsPage";
35
34
  import { codeRepositoryTabs } from "./pages/code/codeRepositoryRoutes";
36
35
  import { RepositoryPage } from "./pages/code/RepositoryPage";
37
36
  import { CodePage } from "./pages/code/CodePage";
@@ -49,13 +48,17 @@ import {
49
48
  } from "./pages/system/SystemPageLayout";
50
49
  import { WorkspaceFormPage } from "./pages/system/WorkspaceFormPage";
51
50
  import { WorkspacesPage } from "./pages/system/WorkspacesPage";
52
- import { MemoryRouteLoading } from "./pages/memory/MemoryPageLayout";
51
+ import {
52
+ isMemoryLibraryPath,
53
+ MemoryRouteLoading,
54
+ } from "./pages/memory/MemoryPageLayout";
53
55
  import { AutomationExecutionsPage } from "./pages/automations/AutomationExecutionsPage";
54
56
  import { AutomationRunsPage } from "./pages/automations/AutomationRunsPage";
55
57
  import { AutomationVersionsPage } from "./pages/automations/AutomationVersionsPage";
56
58
  import { AutomationsPage } from "./pages/automations/AutomationsPage";
57
59
  import {
58
60
  AutomationsPageLayout,
61
+ hasAutomationDetailsFrame,
59
62
  AutomationsRouteLoading,
60
63
  } from "./pages/automations/AutomationsPageLayout";
61
64
  import {
@@ -121,17 +124,6 @@ export function DashboardShell() {
121
124
  setMobileNavigationOpen(false);
122
125
  }, [location.pathname]);
123
126
 
124
- useEffect(() => {
125
- if (!mobileNavigationOpen) return;
126
-
127
- function closeOnEscape(event: KeyboardEvent) {
128
- if (event.key === "Escape") setMobileNavigationOpen(false);
129
- }
130
-
131
- document.addEventListener("keydown", closeOnEscape);
132
- return () => document.removeEventListener("keydown", closeOnEscape);
133
- }, [mobileNavigationOpen]);
134
-
135
127
  async function signOut() {
136
128
  await fetch(`${data?.config.authPath ?? "/api/auth"}/sign-out`, {
137
129
  credentials: "same-origin",
@@ -142,7 +134,14 @@ export function DashboardShell() {
142
134
 
143
135
  return (
144
136
  <DashboardChromeProvider>
145
- <VisualViewportShell className="bg-dashboard-bg" enabled={workspace}>
137
+ <VisualViewportShell
138
+ className="bg-dashboard-bg"
139
+ enabled={
140
+ workspace ||
141
+ hasAutomationDetailsFrame(location.pathname) ||
142
+ isMemoryLibraryPath(location.pathname)
143
+ }
144
+ >
146
145
  <DashboardChrome
147
146
  banner={
148
147
  <>
@@ -424,22 +423,6 @@ export function DashboardShell() {
424
423
  path="/conversations/:conversationId"
425
424
  />
426
425
  <Route element={<Navigate replace to="/" />} path="/conversations" />
427
- <Route
428
- element={
429
- loading ? (
430
- <LoadingView label="Loading components" />
431
- ) : !data ? (
432
- <LoadingView
433
- label={query.error?.message ?? "Dashboard unavailable"}
434
- />
435
- ) : data.config.componentGallery ? (
436
- <ComponentsPage />
437
- ) : (
438
- <Navigate replace to="/" />
439
- )
440
- }
441
- path="/dev/*"
442
- />
443
426
  <Route
444
427
  element={<LegacySystemRedirect section="people" />}
445
428
  path="/people"
@@ -496,6 +479,21 @@ export function DashboardShell() {
496
479
  }
497
480
  path="/system/workspaces/new"
498
481
  />
482
+ <Route
483
+ element={
484
+ loading ? (
485
+ <SystemRouteLoading
486
+ description="Repository recipes Junior can switch into."
487
+ label="Loading Workspaces"
488
+ title="Workspaces"
489
+ variant="list"
490
+ />
491
+ ) : (
492
+ <WorkspacesPage />
493
+ )
494
+ }
495
+ path="/system/workspaces/:workspaceId"
496
+ />
499
497
  <Route
500
498
  element={
501
499
  loading ? (
@@ -506,7 +504,7 @@ export function DashboardShell() {
506
504
  <WorkspaceFormPage />
507
505
  )
508
506
  }
509
- path="/system/workspaces/:workspaceId"
507
+ path="/system/workspaces/:workspaceId/edit"
510
508
  />
511
509
  <Route
512
510
  element={
package/src/client/api.ts CHANGED
@@ -103,6 +103,13 @@ export function useConversationsData(search = "") {
103
103
  signal,
104
104
  ),
105
105
  placeholderData: keepPreviousData,
106
+ // Active rows change while no Conversation is open, such as a new title.
107
+ refetchInterval: (query) =>
108
+ query.state.data?.conversations.some(
109
+ (conversation) => conversation.status === "active",
110
+ )
111
+ ? 10_000
112
+ : false,
106
113
  retry: false,
107
114
  });
108
115
  }
@@ -1,5 +1,4 @@
1
- import { useState } from "react";
2
- import { cn } from "../styles";
1
+ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
3
2
 
4
3
  /** Show an actor's image, with initials when the image is missing or fails. */
5
4
  export function ActorAvatar(props: {
@@ -7,7 +6,6 @@ export function ActorAvatar(props: {
7
6
  imageUrl?: string;
8
7
  size: "detail" | "list";
9
8
  }) {
10
- const [failedUrl, setFailedUrl] = useState<string>();
11
9
  const words = props.name.trim().split(/\s+/).filter(Boolean);
12
10
  // The compact size fits one initial at the 12px type floor.
13
11
  const initials =
@@ -17,24 +15,14 @@ export function ActorAvatar(props: {
17
15
  ? `${words[0]![0]}${words.at(-1)![0]}`
18
16
  : props.name.slice(0, 2);
19
17
  return (
20
- <span
21
- aria-hidden="true"
22
- className={cn(
23
- "relative inline-grid shrink-0 place-items-center overflow-hidden rounded-full border-2 border-dashboard-bg bg-dashboard-focus font-sans font-semibold leading-none tracking-tight text-dashboard-text-inverse shadow-sm",
24
- props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
25
- )}
26
- >
27
- {initials.toUpperCase()}
28
- {props.imageUrl && props.imageUrl !== failedUrl ? (
29
- <img
30
- alt=""
31
- className="absolute inset-0 size-full object-cover"
32
- loading="lazy"
33
- onError={() => setFailedUrl(props.imageUrl)}
34
- referrerPolicy="no-referrer"
35
- src={props.imageUrl}
36
- />
37
- ) : null}
38
- </span>
18
+ <Avatar aria-hidden="true" size={props.size === "list" ? "sm" : "default"}>
19
+ <AvatarImage
20
+ alt=""
21
+ loading="lazy"
22
+ referrerPolicy="no-referrer"
23
+ src={props.imageUrl}
24
+ />
25
+ <AvatarFallback>{initials.toUpperCase()}</AvatarFallback>
26
+ </Avatar>
39
27
  );
40
28
  }
@@ -1,3 +1,4 @@
1
+ import { cn } from "../styles";
1
2
  import type { ReactNode } from "react";
2
3
 
3
4
  import { Sheet, SheetContent, SheetHeader, SheetTitle } from "./ui/sheet";
@@ -7,6 +8,8 @@ import { useSheet } from "./ui/useSheet";
7
8
  export function BottomSheet(props: {
8
9
  children: ReactNode;
9
10
  label: string;
11
+ title?: string;
12
+ size?: "full" | "content";
10
13
  onClose(): void;
11
14
  }) {
12
15
  const sheet = useSheet(props.onClose);
@@ -16,10 +19,17 @@ export function BottomSheet(props: {
16
19
  {...sheet.contentProps}
17
20
  aria-label={props.label}
18
21
  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]"
22
+ className={cn(
23
+ "mx-auto w-full max-w-2xl gap-0 rounded-t-2xl outline-none",
24
+ props.size === "content"
25
+ ? "max-h-[85dvh]"
26
+ : "max-h-full data-[side=bottom]:h-[85dvh]",
27
+ )}
20
28
  >
21
29
  <SheetHeader className="border-b border-dashboard-border">
22
- <SheetTitle>{props.label}</SheetTitle>
30
+ <SheetTitle className="break-words">
31
+ {props.title ?? props.label}
32
+ </SheetTitle>
23
33
  </SheetHeader>
24
34
  <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
25
35
  {props.children}
@@ -1,4 +1,5 @@
1
1
  import { useDashboardOnline } from "../connection";
2
+ import { Alert } from "./ui/alert";
2
3
 
3
4
  /** Explain that cached data remains visible while network reads are paused. */
4
5
  export function ConnectionBanner() {
@@ -6,12 +7,12 @@ export function ConnectionBanner() {
6
7
  if (online) return null;
7
8
 
8
9
  return (
9
- <div
10
+ <Alert
10
11
  aria-live="polite"
11
- className="border-t border-amber-300/15 bg-amber-300/[0.07] px-3 py-1.5 text-center font-sans text-xs text-amber-100/80"
12
+ className="block rounded-none border-0 border-t border-amber-300/15 bg-amber-300/[0.07] px-3 py-1.5 text-center font-sans text-xs text-amber-100/80"
12
13
  role="status"
13
14
  >
14
15
  You’re offline. Drafts stay on this device.
15
- </div>
16
+ </Alert>
16
17
  );
17
18
  }
@@ -0,0 +1,82 @@
1
+ import { useState } from "react";
2
+ import { Check, CircleAlert, Copy } from "lucide-react";
3
+
4
+ import { Button } from "./ui/button";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
6
+
7
+ /** Copy available text and show whether the clipboard write succeeded. */
8
+ export function CopyButton(props: {
9
+ getText?: () => Promise<string> | string;
10
+ label: string;
11
+ copyingLabel?: string;
12
+ /** `menu` shows icon + label for mobile overflow lists. */
13
+ layout?: "icon" | "menu";
14
+ }) {
15
+ const [status, setStatus] = useState<
16
+ "copied" | "copying" | "failed" | "idle"
17
+ >("idle");
18
+ const label =
19
+ status === "copied"
20
+ ? "Copied"
21
+ : status === "copying"
22
+ ? (props.copyingLabel ?? "Copying")
23
+ : status === "failed"
24
+ ? "Copy failed"
25
+ : props.label;
26
+ const Icon = {
27
+ copied: Check,
28
+ copying: Copy,
29
+ failed: CircleAlert,
30
+ idle: Copy,
31
+ }[status];
32
+
33
+ async function copyText() {
34
+ if (!props.getText) return;
35
+
36
+ try {
37
+ setStatus("copying");
38
+ await navigator.clipboard.writeText(await props.getText());
39
+ setStatus("copied");
40
+ } catch {
41
+ setStatus("failed");
42
+ }
43
+ }
44
+
45
+ if (props.layout === "menu") {
46
+ return (
47
+ <Button
48
+ aria-label={label}
49
+ disabled={!props.getText || status === "copying"}
50
+ onClick={() => void copyText()}
51
+ size="menu"
52
+ >
53
+ <Icon aria-hidden="true" size={16} strokeWidth={2} />
54
+ <span>{label}</span>
55
+ </Button>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <Tooltip>
61
+ <TooltipTrigger
62
+ render={
63
+ <Button
64
+ focusableWhenDisabled
65
+ aria-label={label}
66
+ disabled={!props.getText || status === "copying"}
67
+ onClick={() => void copyText()}
68
+ size="icon"
69
+ >
70
+ <Icon
71
+ aria-hidden="true"
72
+ className={status === "failed" ? "text-rose-300" : undefined}
73
+ size={15}
74
+ strokeWidth={2}
75
+ />
76
+ </Button>
77
+ }
78
+ />
79
+ <TooltipContent size="compact">{label}</TooltipContent>
80
+ </Tooltip>
81
+ );
82
+ }
@@ -0,0 +1,133 @@
1
+ import { X } from "lucide-react";
2
+ import { useEffect, useRef, useSyncExternalStore, type ReactNode } from "react";
3
+
4
+ import { Button } from "./ui/button";
5
+ import { BottomSheet } from "./BottomSheet";
6
+ import { Drawer } from "./Drawer";
7
+
8
+ const panelMediaQuery = "(min-width: 1280px)";
9
+
10
+ function subscribeToPanelLayout(onChange: () => void) {
11
+ const media = window.matchMedia(panelMediaQuery);
12
+ media.addEventListener("change", onChange);
13
+ return () => media.removeEventListener("change", onChange);
14
+ }
15
+
16
+ function hasPanelSpace() {
17
+ return window.matchMedia(panelMediaQuery).matches;
18
+ }
19
+
20
+ /** Report whether details fit beside their source instead of in a sheet. */
21
+ export function useDetailsPanel(): boolean {
22
+ return useSyncExternalStore(
23
+ subscribeToPanelLayout,
24
+ hasPanelSpace,
25
+ () => false,
26
+ );
27
+ }
28
+
29
+ /** Show details beside their source, or in a bottom sheet when space is limited. */
30
+ export function DetailsPanel(props: {
31
+ actions?: ReactNode;
32
+ children: ReactNode;
33
+ onClose(): void;
34
+ label: string;
35
+ title: ReactNode;
36
+ sheetTitle?: string;
37
+ /** Nested details stay modal inside an existing drawer. */
38
+ overlay?: boolean;
39
+ }) {
40
+ const { onClose, overlay } = props;
41
+ const closeLabel = `Close ${props.label.toLowerCase()}`;
42
+ const panel = useDetailsPanel();
43
+ const closeRef = useRef<HTMLButtonElement>(null);
44
+ const wasPanel = useRef(panel);
45
+ useEffect(() => {
46
+ if (
47
+ panel &&
48
+ !wasPanel.current &&
49
+ !document.querySelector("[data-dashboard-modal]")
50
+ ) {
51
+ closeRef.current?.focus({ preventScroll: true });
52
+ }
53
+ wasPanel.current = panel;
54
+ }, [panel]);
55
+ useEffect(() => {
56
+ if (!panel || overlay) return;
57
+ function onKeyDown(event: KeyboardEvent) {
58
+ if (
59
+ event.key === "Escape" &&
60
+ !event.defaultPrevented &&
61
+ !document.querySelector("[data-dashboard-modal]")
62
+ ) {
63
+ event.preventDefault();
64
+ onClose();
65
+ }
66
+ }
67
+ document.addEventListener("keydown", onKeyDown);
68
+ return () => document.removeEventListener("keydown", onKeyDown);
69
+ }, [panel, overlay, onClose]);
70
+ if (!panel) {
71
+ return (
72
+ <BottomSheet
73
+ label={props.label}
74
+ title={props.sheetTitle}
75
+ onClose={onClose}
76
+ >
77
+ {props.actions ? (
78
+ <div className="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_auto]">
79
+ <div className="min-h-0">{props.children}</div>
80
+ <div className="flex justify-end gap-1 border-t border-dashboard-border px-4 py-2">
81
+ {props.actions}
82
+ </div>
83
+ </div>
84
+ ) : (
85
+ props.children
86
+ )}
87
+ </BottomSheet>
88
+ );
89
+ }
90
+
91
+ if (overlay) {
92
+ return (
93
+ <Drawer
94
+ actions={props.actions}
95
+ closeLabel={closeLabel}
96
+ header={props.title}
97
+ label={props.label}
98
+ onClose={onClose}
99
+ openKey={props.label}
100
+ width="wide"
101
+ >
102
+ {props.children}
103
+ </Drawer>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <aside
109
+ aria-label={props.label}
110
+ className="grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] border-l border-dashboard-border bg-dashboard-bg-elevated"
111
+ >
112
+ <header className="flex items-center justify-between gap-3 border-b border-dashboard-border py-2 pl-5 pr-3">
113
+ <div className="flex min-w-0 items-center gap-3">
114
+ <div className="min-w-0">{props.title}</div>
115
+ {props.actions ? (
116
+ <div className="flex items-center gap-0.5">{props.actions}</div>
117
+ ) : null}
118
+ </div>
119
+ <Button
120
+ ref={closeRef}
121
+ aria-label={closeLabel}
122
+ onClick={onClose}
123
+ size="icon"
124
+ >
125
+ <X aria-hidden="true" size={15} />
126
+ </Button>
127
+ </header>
128
+ <div className="min-h-0 overflow-y-auto overscroll-contain">
129
+ {props.children}
130
+ </div>
131
+ </aside>
132
+ );
133
+ }
@@ -1,9 +1,7 @@
1
- import { X } from "lucide-react";
2
1
  import { useRef, type ReactNode } from "react";
3
2
 
4
3
  import { cn } from "../styles";
5
- import { Button } from "./Button";
6
- import { Sheet, SheetClose, SheetContent } from "./ui/sheet";
4
+ import { Sheet, SheetContent, SheetHeader } from "./ui/sheet";
7
5
  import { useSheet } from "./ui/useSheet";
8
6
 
9
7
  type DrawerProps = {
@@ -13,9 +11,8 @@ type DrawerProps = {
13
11
  header: ReactNode;
14
12
  onClose(): void;
15
13
  openKey: string;
16
- titleId: string;
17
14
  width?: "default" | "wide" | "narrow";
18
- };
15
+ } & ({ titleId: string; label?: never } | { label: string; titleId?: never });
19
16
 
20
17
  /** Render a side drawer with shared modal focus and dismissal behavior. */
21
18
  export function Drawer(props: DrawerProps) {
@@ -29,9 +26,15 @@ function DrawerContent(props: DrawerProps) {
29
26
  <Sheet {...sheet.rootProps}>
30
27
  <SheetContent
31
28
  {...sheet.contentProps}
29
+ aria-label={props.label}
32
30
  aria-labelledby={props.titleId}
33
31
  side="right"
34
- showCloseButton={false}
32
+ closeButtonProps={{
33
+ ref: closeRef,
34
+ "aria-label": props.closeLabel,
35
+ className:
36
+ "right-4 top-[max(0.75rem,env(safe-area-inset-top))] md:right-5",
37
+ }}
35
38
  className={cn(
36
39
  "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",
37
40
  props.width === "wide"
@@ -41,24 +44,12 @@ function DrawerContent(props: DrawerProps) {
41
44
  : "data-[side=right]:md:w-[min(560px,94vw)]",
42
45
  )}
43
46
  >
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">
45
- <div className="min-w-0">{props.header}</div>
46
- <div className="flex items-start gap-1.5">
47
- {props.actions}
48
- <SheetClose
49
- render={
50
- <Button
51
- ref={closeRef}
52
- aria-label={props.closeLabel}
53
- size="icon"
54
- title="Close"
55
- />
56
- }
57
- >
58
- <X aria-hidden="true" size={15} strokeWidth={2.25} />
59
- </SheetClose>
60
- </div>
61
- </header>
47
+ <SheetHeader className="flex-row items-start gap-3 border-b border-dashboard-border bg-dashboard-bg-elevated px-4 py-3 pr-14 pt-[max(0.75rem,env(safe-area-inset-top))] md:pl-5 md:pr-14">
48
+ <div className="min-w-0 flex-1">{props.header}</div>
49
+ {props.actions ? (
50
+ <div className="flex items-start gap-1.5">{props.actions}</div>
51
+ ) : null}
52
+ </SheetHeader>
62
53
  <div className="min-h-0 overflow-auto px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5">
63
54
  {props.children}
64
55
  </div>
@@ -1,10 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
+ import { Empty, EmptyDescription } from "./ui/empty";
4
+
3
5
  /** Render the dashboard empty-state block with quiet warning context. */
4
6
  export function EmptyTelemetry(props: { children: ReactNode }) {
5
7
  return (
6
- <div className="min-w-0 rounded-lg border border-dashboard-border-strong bg-dashboard-bg px-4 py-3 text-base leading-relaxed text-dashboard-text-muted">
7
- {props.children}
8
- </div>
8
+ <Empty>
9
+ <EmptyDescription>{props.children}</EmptyDescription>
10
+ </Empty>
9
11
  );
10
12
  }