@sentry/junior-dashboard 0.264.0 → 0.265.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 (191) 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/BottomSheet.tsx +12 -2
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +15 -24
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +86 -77
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +13 -3
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. 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.265.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.265.0",
41
+ "@sentry/junior-plugin-api": "0.265.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,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}
@@ -0,0 +1,83 @@
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
+ className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
50
+ disabled={!props.getText || status === "copying"}
51
+ onClick={() => void copyText()}
52
+ type="button"
53
+ >
54
+ <Icon aria-hidden="true" size={16} strokeWidth={2} />
55
+ <span>{label}</span>
56
+ </button>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <Tooltip>
62
+ <TooltipTrigger
63
+ render={
64
+ <Button
65
+ focusableWhenDisabled
66
+ aria-label={label}
67
+ disabled={!props.getText || status === "copying"}
68
+ onClick={() => void copyText()}
69
+ size="icon"
70
+ >
71
+ <Icon
72
+ aria-hidden="true"
73
+ className={status === "failed" ? "text-rose-300" : undefined}
74
+ size={15}
75
+ strokeWidth={2}
76
+ />
77
+ </Button>
78
+ }
79
+ />
80
+ <TooltipContent size="compact">{label}</TooltipContent>
81
+ </Tooltip>
82
+ );
83
+ }
@@ -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>
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { cn } from "../styles";
4
4
  import { Card } from "./layout/Card";
5
5
  import { SearchInput } from "./SearchInput";
6
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
6
7
 
7
8
  /** Lay out labeled filter groups and an optional search field. */
8
9
  export function FilterBar(props: {
@@ -69,43 +70,27 @@ export function FilterTabList(props: {
69
70
  value: string;
70
71
  }) {
71
72
  return (
72
- <div
73
- aria-label={props.ariaLabel}
74
- className="grid min-w-0 grid-cols-3 gap-1 border-b border-dashboard-border-subtle sm:flex sm:overflow-x-auto"
75
- role="tablist"
76
- >
77
- {props.items.map((item) => {
78
- const selected = props.value === item.value;
79
- return (
80
- <button
81
- aria-selected={selected}
82
- className={cn(
83
- "relative flex min-w-0 cursor-pointer items-center justify-between gap-2 border-0 bg-transparent px-3 py-2.5 font-sans font-medium text-xs transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-px sm:shrink-0 sm:justify-start",
84
- selected
85
- ? "text-dashboard-accent-text after:bg-dashboard-accent"
86
- : "text-dashboard-text-muted after:bg-transparent hover:text-dashboard-text",
87
- )}
73
+ <Tabs onValueChange={props.onChange} value={props.value}>
74
+ <TabsList
75
+ aria-label={props.ariaLabel}
76
+ className="grid grid-cols-3 sm:flex sm:overflow-x-auto"
77
+ variant="line"
78
+ >
79
+ {props.items.map((item) => (
80
+ <TabsTrigger
81
+ className="group/tab justify-between sm:shrink-0 sm:justify-start"
88
82
  key={item.value || "all"}
89
- onClick={() => props.onChange(item.value)}
90
- role="tab"
91
- type="button"
83
+ value={item.value}
92
84
  >
93
85
  {item.label}
94
86
  {item.count !== undefined ? (
95
- <span
96
- className={cn(
97
- "rounded-sm border px-1.5 py-0.5 text-xs",
98
- selected
99
- ? "border-dashboard-accent/20 bg-dashboard-accent/10 text-dashboard-accent-text"
100
- : "border-dashboard-border bg-dashboard-fill-soft text-dashboard-text-muted",
101
- )}
102
- >
87
+ <span className="rounded-sm border border-dashboard-border bg-dashboard-fill-soft px-1.5 py-0.5 text-xs text-dashboard-text-muted group-data-active/tab:border-dashboard-accent/20 group-data-active/tab:bg-dashboard-accent/10 group-data-active/tab:text-dashboard-accent-text">
103
88
  {item.count.toLocaleString("en-US")}
104
89
  </span>
105
90
  ) : null}
106
- </button>
107
- );
108
- })}
109
- </div>
91
+ </TabsTrigger>
92
+ ))}
93
+ </TabsList>
94
+ </Tabs>
110
95
  );
111
96
  }