@sentry/junior-dashboard 0.263.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 (197) 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 +148 -135
  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 +9 -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 +26 -18
  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 +34 -33
  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 +101 -64
  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 +151 -0
  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/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -1,21 +1,19 @@
1
- import { ChevronRight, FolderGit2, Trash2 } from "lucide-react";
2
- import { useNavigate } from "react-router";
1
+ import { ChevronRight, FolderGit2 } from "lucide-react";
3
2
  import type { WorkspaceReport } from "@sentry/junior/api/schema";
4
3
 
5
- import { Button } from "../../components/Button";
6
4
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
7
5
  import { SelectableRow } from "../../components/SelectableRow";
8
6
  import { Card } from "../../components/layout/Card";
9
- import { formatSandboxSize } from "./SandboxSizeSummary";
7
+ import { formatSandboxSize } from "./sandboxSize";
10
8
 
11
9
  type WorkspaceListProps = {
12
- busy: boolean;
13
10
  defaultVcpus: number | null;
14
- onDelete(workspace: WorkspaceReport): void;
11
+ onSelect(workspace: WorkspaceReport): void;
12
+ selectedId: string | undefined;
15
13
  workspaces: WorkspaceReport[];
16
14
  };
17
15
 
18
- /** List Workspace recipes and expose their edit and delete actions. */
16
+ /** List Workspace recipes. Selecting a row opens its details. */
19
17
  export function WorkspaceList(props: WorkspaceListProps) {
20
18
  return (
21
19
  <Card padding="none">
@@ -30,10 +28,10 @@ export function WorkspaceList(props: WorkspaceListProps) {
30
28
  <ul className="m-0 list-none p-0">
31
29
  {props.workspaces.map((workspace) => (
32
30
  <WorkspaceListItem
33
- busy={props.busy}
34
31
  defaultVcpus={props.defaultVcpus}
35
32
  key={workspace.id}
36
- onDelete={props.onDelete}
33
+ onSelect={() => props.onSelect(workspace)}
34
+ selected={workspace.id === props.selectedId}
37
35
  workspace={workspace}
38
36
  />
39
37
  ))}
@@ -44,26 +42,27 @@ export function WorkspaceList(props: WorkspaceListProps) {
44
42
  }
45
43
 
46
44
  function WorkspaceListItem(props: {
47
- busy: boolean;
48
45
  defaultVcpus: number | null;
49
- onDelete(workspace: WorkspaceReport): void;
46
+ onSelect(): void;
47
+ selected: boolean;
50
48
  workspace: WorkspaceReport;
51
49
  }) {
52
50
  const { workspace } = props;
53
- const navigate = useNavigate();
54
- const openWorkspace = () =>
55
- navigate(`/system/workspaces/${encodeURIComponent(workspace.id)}`);
56
51
  return (
57
- <li className="border-b border-dashboard-border-subtle last:border-b-0">
52
+ <li
53
+ className="border-b border-dashboard-border-subtle last:border-b-0"
54
+ data-workspace-id={workspace.id}
55
+ >
58
56
  <SelectableRow
59
- className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-4 py-3.5"
60
- onSelect={openWorkspace}
61
- selected={false}
57
+ className="px-4 py-3.5"
58
+ onSelect={props.onSelect}
59
+ selected={props.selected}
62
60
  >
63
61
  <button
64
- aria-label={`Manage ${workspace.name}`}
65
- className="flex min-w-0 cursor-pointer items-center gap-3 border-0 bg-transparent p-0 text-left"
66
- onClick={openWorkspace}
62
+ aria-expanded={props.selected}
63
+ aria-label={`View Workspace details: ${workspace.name}`}
64
+ className="flex w-full min-w-0 cursor-pointer items-center gap-3 rounded-md border-0 bg-transparent p-0 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
65
+ onClick={props.onSelect}
67
66
  type="button"
68
67
  >
69
68
  <span className="grid size-9 shrink-0 place-items-center rounded-md border border-dashboard-border-strong bg-dashboard-fill-soft text-dashboard-accent transition-colors group-hover:border-dashboard-accent/20 group-hover:bg-dashboard-accent/[0.06] group-hover:text-dashboard-accent-text">
@@ -94,15 +93,6 @@ function WorkspaceListItem(props: {
94
93
  size={16}
95
94
  />
96
95
  </button>
97
- <Button
98
- aria-label={`Delete ${workspace.name}`}
99
- disabled={props.busy}
100
- onClick={() => props.onDelete(workspace)}
101
- size="icon"
102
- title={`Delete ${workspace.name}`}
103
- >
104
- <Trash2 aria-hidden="true" size={15} />
105
- </Button>
106
96
  </SelectableRow>
107
97
  </li>
108
98
  );
@@ -1,138 +1,136 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1
+ import { useRef, useState } from "react";
2
+ import { Link, useNavigate, useParams } from "react-router";
2
3
  import { Plus } from "lucide-react";
3
- import {
4
- workspaceListSchema,
5
- type WorkspaceListReport,
6
- type WorkspaceReport,
7
- } from "@sentry/junior/api/schema";
4
+ import type { WorkspaceReport } from "@sentry/junior/api/schema";
8
5
 
9
6
  import { getDashboardAgentName } from "../../agentName";
10
- import { ButtonLink } from "../../components/Button";
7
+ import { buttonVariants } from "../../components/ui/button";
11
8
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
12
- import { InlineError } from "../../components/InlineError";
9
+ import { PageContentSkeleton } from "../../components/PageContentSkeleton";
13
10
  import { Card } from "../../components/layout/Card";
14
11
  import { PageHeader } from "../../components/layout/PageHeader";
15
- import {
16
- DashboardApiError,
17
- deleteDashboardResource,
18
- fetchDashboardJson,
19
- } from "../../http";
12
+ import { PageLayout } from "../../components/layout/PageLayout";
13
+ import { useDocumentTitle } from "../../documentTitle";
14
+ import { workspacePath } from "../../format";
15
+ import { cn } from "../../styles";
20
16
  import { BaseSandboxCard } from "./BaseSandboxCard";
21
- import { SystemPageLayout, SystemRouteLoading } from "./SystemPageLayout";
17
+ import { SystemNavigation } from "./SystemNavigation";
18
+ import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
19
+ import { WorkspaceDetails } from "./WorkspaceDetails";
22
20
  import { WorkspaceList } from "./WorkspaceList";
21
+ import { useWorkspacesData } from "./workspaceQueries";
23
22
 
24
- export const workspacesQueryKey = ["dashboard", "workspaces"] as const;
23
+ const listPath = "/system/workspaces";
25
24
 
26
- /** Return a useful dashboard error message without exposing response details. */
27
- export function readWorkspaceApiError(
28
- error: unknown,
29
- fallback: string,
30
- ): string {
31
- if (error instanceof DashboardApiError) {
32
- return error.apiError ?? fallback;
33
- }
34
- if (error instanceof Error && error.message.trim()) {
35
- return error.message;
36
- }
37
- return fallback;
38
- }
39
-
40
- /** Manage install-wide repository Workspace recipes. */
25
+ /** List install-wide Workspace recipes. The URL selects one for the details panel. */
41
26
  export function WorkspacesPage() {
42
- const queryClient = useQueryClient();
43
- const workspacesQuery = useQuery({
44
- queryKey: workspacesQueryKey,
45
- queryFn: ({ signal }) =>
46
- fetchDashboardJson(workspaceListSchema, "/api/workspaces", signal),
47
- retry: false,
48
- });
49
- const deleteMutation = useMutation({
50
- mutationFn: async (workspace: WorkspaceReport) => {
51
- await deleteDashboardResource(
52
- `/api/workspaces/${encodeURIComponent(workspace.id)}`,
53
- );
54
- return workspace;
55
- },
56
- onSuccess: async (workspace) => {
57
- await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
58
- queryClient.setQueryData<WorkspaceListReport>(
59
- workspacesQueryKey,
60
- (current) =>
61
- current && {
62
- ...current,
63
- workspaces: current.workspaces.filter(
64
- (item) => item.id !== workspace.id,
65
- ),
66
- },
67
- );
68
- },
69
- });
70
-
71
- if (!workspacesQuery.data && !workspacesQuery.error) {
72
- return (
73
- <SystemRouteLoading
74
- description="Repository recipes Junior can switch into."
75
- label="Loading Workspaces"
76
- title="Workspaces"
77
- variant="list"
78
- />
79
- );
80
- }
81
-
27
+ const { workspaceId } = useParams();
28
+ const navigate = useNavigate();
29
+ const listRef = useRef<HTMLDivElement>(null);
30
+ const deleteSource = useRef<HTMLButtonElement | null>(null);
31
+ const [deleteTarget, setDeleteTarget] = useState<WorkspaceReport>();
32
+ const workspacesQuery = useWorkspacesData();
82
33
  const workspaces = workspacesQuery.data?.workspaces ?? [];
83
- const baselineSnapshot = workspacesQuery.data?.baselineSnapshot ?? null;
84
34
  const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
85
- return (
86
- <SystemPageLayout>
87
- <PageHeader
88
- actions={
89
- <ButtonLink to="/system/workspaces/new">
90
- <Plus aria-hidden="true" size={14} />
91
- New Workspace
92
- </ButtonLink>
93
- }
94
- description={`Named repository recipes ${getDashboardAgentName()} can switch into without cloning each turn.`}
95
- title="Workspaces"
96
- />
97
-
98
- {workspacesQuery.error ? (
99
- <Card padding="sm">
100
- <EmptyTelemetry>
101
- Workspaces failed to load. Try refreshing the dashboard.
102
- </EmptyTelemetry>
103
- </Card>
104
- ) : null}
35
+ const selected = workspaces.find((item) => item.id === workspaceId);
36
+ // PageHeader keeps its title after close, so this page owns the tab title.
37
+ useDocumentTitle(selected?.name ?? "Workspaces");
105
38
 
106
- {deleteMutation.error ? (
107
- <InlineError>
108
- {readWorkspaceApiError(
109
- deleteMutation.error,
110
- "Could not delete the Workspace. Try again.",
111
- )}
112
- </InlineError>
113
- ) : null}
39
+ const closeDetails = () => {
40
+ const trigger = workspaceId
41
+ ? listRef.current?.querySelector<HTMLButtonElement>(
42
+ `[data-workspace-id="${CSS.escape(workspaceId)}"] button`,
43
+ )
44
+ : undefined;
45
+ navigate(listPath);
46
+ (trigger ?? listRef.current)?.focus();
47
+ };
114
48
 
115
- {!workspacesQuery.error ? (
116
- <BaseSandboxCard
117
- defaultVcpus={defaultVcpus}
118
- snapshot={baselineSnapshot}
49
+ return (
50
+ <SystemNavigation className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] md:grid-rows-[minmax(0,1fr)]">
51
+ <div
52
+ className={cn(
53
+ "grid h-full min-h-0 min-w-0",
54
+ workspaceId && "xl:grid-cols-[minmax(0,1fr)_25rem]",
55
+ )}
56
+ >
57
+ <div
58
+ aria-label="Workspaces list"
59
+ className="min-h-0 overflow-y-auto overscroll-contain outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
60
+ ref={listRef}
61
+ role="region"
62
+ tabIndex={0}
63
+ >
64
+ <PageLayout>
65
+ <PageHeader
66
+ actions={
67
+ <Link className={buttonVariants()} to={`${listPath}/new`}>
68
+ <Plus aria-hidden="true" size={14} />
69
+ New Workspace
70
+ </Link>
71
+ }
72
+ description={`Named repository recipes ${getDashboardAgentName()} can switch into without cloning each turn.`}
73
+ title="Workspaces"
74
+ />
75
+ {workspacesQuery.error ? (
76
+ <Card padding="sm">
77
+ <EmptyTelemetry>
78
+ Workspaces failed to load. Try refreshing the dashboard.
79
+ </EmptyTelemetry>
80
+ </Card>
81
+ ) : workspacesQuery.data ? (
82
+ <>
83
+ <BaseSandboxCard
84
+ defaultVcpus={defaultVcpus}
85
+ snapshot={workspacesQuery.data.baselineSnapshot}
86
+ />
87
+ <WorkspaceList
88
+ defaultVcpus={defaultVcpus}
89
+ onSelect={(workspace) =>
90
+ navigate(
91
+ workspace.id === workspaceId
92
+ ? listPath
93
+ : workspacePath(workspace.id),
94
+ )
95
+ }
96
+ selectedId={workspaceId}
97
+ workspaces={workspaces}
98
+ />
99
+ </>
100
+ ) : (
101
+ <PageContentSkeleton label="Loading Workspaces" variant="list" />
102
+ )}
103
+ </PageLayout>
104
+ </div>
105
+ {workspaceId ? (
106
+ <WorkspaceDetails
107
+ defaultVcpus={defaultVcpus}
108
+ key={workspaceId}
109
+ listError={Boolean(workspacesQuery.error)}
110
+ onClose={closeDetails}
111
+ onDelete={(workspace, source) => {
112
+ deleteSource.current = source;
113
+ setDeleteTarget(workspace);
114
+ }}
115
+ workspace={selected}
116
+ workspaceId={workspaceId}
117
+ />
118
+ ) : null}
119
+ </div>
120
+ {deleteTarget ? (
121
+ <WorkspaceDeleteDialog
122
+ onClose={() => setDeleteTarget(undefined)}
123
+ onDeleted={() => {
124
+ if (workspaceId === deleteTarget.id) navigate(listPath);
125
+ }}
126
+ returnFocus={() =>
127
+ deleteSource.current?.isConnected
128
+ ? deleteSource.current
129
+ : (listRef.current ?? false)
130
+ }
131
+ workspace={deleteTarget}
119
132
  />
120
133
  ) : null}
121
-
122
- <WorkspaceList
123
- busy={deleteMutation.isPending}
124
- defaultVcpus={defaultVcpus}
125
- onDelete={(workspace) => {
126
- if (
127
- window.confirm(
128
- `Delete Workspace “${workspace.name}”? Active conversations keep their current Sandbox until the next switch.`,
129
- )
130
- ) {
131
- deleteMutation.mutate(workspace);
132
- }
133
- }}
134
- workspaces={workspaces}
135
- />
136
- </SystemPageLayout>
134
+ </SystemNavigation>
137
135
  );
138
136
  }
@@ -0,0 +1,8 @@
1
+ /** Vercel Sandbox gives each vCPU 2 GB of memory. */
2
+ const MEMORY_GB_PER_VCPU = 2;
3
+
4
+ /** Format a Sandbox vCPU count with its memory. Null means the Vercel default. */
5
+ export function formatSandboxSize(vcpus: number | null): string {
6
+ if (vcpus === null) return "Vercel default";
7
+ return `${vcpus} vCPU${vcpus === 1 ? "" : "s"} · ${vcpus * MEMORY_GB_PER_VCPU} GB`;
8
+ }
@@ -0,0 +1,53 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import {
3
+ workspaceListSchema,
4
+ workspaceSchema,
5
+ } from "@sentry/junior/api/schema";
6
+
7
+ import { DashboardApiError, fetchDashboardJson } from "../../http";
8
+
9
+ export const workspacesQueryKey = ["dashboard", "workspaces"] as const;
10
+
11
+ /** Cache key for one Workspace detail read. */
12
+ export function workspaceQueryKey(workspaceId: string | undefined) {
13
+ return ["dashboard", "workspace", workspaceId] as const;
14
+ }
15
+
16
+ /** Return a useful dashboard error message without exposing response details. */
17
+ export function readWorkspaceApiError(
18
+ error: unknown,
19
+ fallback: string,
20
+ ): string {
21
+ if (error instanceof DashboardApiError) {
22
+ return error.apiError ?? fallback;
23
+ }
24
+ if (error instanceof Error && error.message.trim()) {
25
+ return error.message;
26
+ }
27
+ return fallback;
28
+ }
29
+
30
+ /** Load Workspace recipes, the default Sandbox size, and the baseline snapshot. */
31
+ export function useWorkspacesData() {
32
+ return useQuery({
33
+ queryKey: workspacesQueryKey,
34
+ queryFn: ({ signal }) =>
35
+ fetchDashboardJson(workspaceListSchema, "/api/workspaces", signal),
36
+ retry: false,
37
+ });
38
+ }
39
+
40
+ /** Load one Workspace recipe with its current snapshot, which the list omits. */
41
+ export function useWorkspaceData(workspaceId: string | undefined) {
42
+ return useQuery({
43
+ enabled: workspaceId !== undefined,
44
+ queryKey: workspaceQueryKey(workspaceId),
45
+ queryFn: ({ signal }) =>
46
+ fetchDashboardJson(
47
+ workspaceSchema,
48
+ `/api/workspaces/${encodeURIComponent(workspaceId!)}`,
49
+ signal,
50
+ ),
51
+ retry: false,
52
+ });
53
+ }
@@ -1,8 +1,18 @@
1
1
  import { Boxes, Trash2 } from "lucide-react";
2
+ import { useState } from "react";
2
3
  import { Navigate, useLocation, useParams } from "react-router";
3
4
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
4
5
 
5
6
  import { InlineError } from "../../components/InlineError";
7
+ import {
8
+ AlertDialog,
9
+ AlertDialogAction,
10
+ AlertDialogCancel,
11
+ AlertDialogContent,
12
+ AlertDialogDescription,
13
+ AlertDialogFooter,
14
+ AlertDialogTitle,
15
+ } from "../../components/ui/alert-dialog";
6
16
  import { LoadingView } from "../../components/LoadingView";
7
17
  import { LoadMorePagination } from "../../components/Pagination";
8
18
  import { SearchInput } from "../../components/SearchInput";
@@ -11,7 +21,11 @@ import { PageHeader } from "../../components/layout/PageHeader";
11
21
  import { MemoryPage } from "../memory/MemoryPage";
12
22
  import { pathWithSearch } from "../../searchParams";
13
23
  import { dashboardContainerClass } from "../../styles";
14
- import { usePluginUserPageData } from "./pluginUserPageData";
24
+ import {
25
+ usePluginUserPageData,
26
+ type PluginUserPageRecord,
27
+ type PluginUserPageRecordAction,
28
+ } from "./pluginUserPageData";
15
29
 
16
30
  /** Build the canonical dashboard path for a plugin-owned page. */
17
31
  export function pluginUserPagePath(pluginName: string, pageId: string): string {
@@ -68,6 +82,12 @@ export function PluginUserPage(props: { page: PluginUserPageLink }) {
68
82
  searchText,
69
83
  setSearchText,
70
84
  } = usePluginUserPageData(props.page);
85
+ // Keep the action after close so the closing dialog keeps its text.
86
+ const [confirming, setConfirming] = useState<{
87
+ action: PluginUserPageRecordAction;
88
+ record: PluginUserPageRecord;
89
+ }>();
90
+ const [confirmOpen, setConfirmOpen] = useState(false);
71
91
 
72
92
  if (!query.data && !query.error) {
73
93
  return <LoadingView label={`Loading ${props.page.label}`} />;
@@ -158,7 +178,14 @@ export function PluginUserPage(props: { page: PluginUserPageLink }) {
158
178
  }`}
159
179
  disabled={action.isPending}
160
180
  key={`${recordAction.method}:${recordAction.href}`}
161
- onClick={() => runAction(recordAction)}
181
+ onClick={() => {
182
+ if (!recordAction.confirmation) {
183
+ runAction(recordAction);
184
+ return;
185
+ }
186
+ setConfirming({ action: recordAction, record });
187
+ setConfirmOpen(true);
188
+ }}
162
189
  title={recordAction.label}
163
190
  type="button"
164
191
  >
@@ -205,6 +232,28 @@ export function PluginUserPage(props: { page: PluginUserPageLink }) {
205
232
  </div>
206
233
  )}
207
234
  </section>
235
+ <AlertDialog onOpenChange={setConfirmOpen} open={confirmOpen}>
236
+ <AlertDialogContent>
237
+ <AlertDialogTitle>{confirming?.action.confirmation}</AlertDialogTitle>
238
+ <AlertDialogDescription>
239
+ {confirming?.record.title}
240
+ </AlertDialogDescription>
241
+ <AlertDialogFooter>
242
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
243
+ <AlertDialogAction
244
+ onClick={() => {
245
+ if (confirming) runAction(confirming.action);
246
+ setConfirmOpen(false);
247
+ }}
248
+ variant={
249
+ confirming?.action.tone === "danger" ? "danger" : "primary"
250
+ }
251
+ >
252
+ {confirming?.action.label}
253
+ </AlertDialogAction>
254
+ </AlertDialogFooter>
255
+ </AlertDialogContent>
256
+ </AlertDialog>
208
257
  </div>
209
258
  );
210
259
  }
@@ -87,14 +87,9 @@ export function usePluginUserPageData(page: PluginUserPageLink) {
87
87
  },
88
88
  });
89
89
 
90
+ // Callers ask for the action's confirmation before they run it.
90
91
  function runAction(recordAction: PluginUserPageRecordAction) {
91
92
  if (actionStarted.current) return;
92
- if (
93
- recordAction.confirmation &&
94
- !window.confirm(recordAction.confirmation)
95
- ) {
96
- return;
97
- }
98
93
  actionStarted.current = true;
99
94
  action.mutate(recordAction);
100
95
  }
@@ -17,3 +17,7 @@ export const dashboardInteractiveTextClass =
17
17
  /** Raised input surface so the chat box reads against the near-black shell. */
18
18
  export const dashboardComposerSurfaceClass =
19
19
  "rounded-xl border border-dashboard-border-emphasis bg-dashboard-surface-raised shadow-[0_8px_32px_rgba(0,0,0,0.35)] transition-colors";
20
+
21
+ /** Turn a disclosure chevron when its `group` trigger sets `aria-expanded`. */
22
+ export const dashboardDisclosureChevronClass =
23
+ "size-3.5 shrink-0 transition-transform group-aria-expanded:rotate-90 motion-reduce:transition-none";
package/src/client.tsx CHANGED
@@ -3,6 +3,8 @@ import { Component, type ErrorInfo, type ReactNode } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
4
  import { createBrowserRouter, RouterProvider } from "react-router";
5
5
 
6
+ import { Toaster } from "./client/components/ui/toast";
7
+ import { TooltipProvider } from "./client/components/ui/tooltip";
6
8
  import { DashboardShell } from "./client/App";
7
9
  import { getDashboardAgentName } from "./client/agentName";
8
10
 
@@ -87,7 +89,11 @@ const router = createBrowserRouter(
87
89
  createRoot(root).render(
88
90
  <DashboardErrorBoundary>
89
91
  <QueryClientProvider client={queryClient}>
90
- <RouterProvider router={router} />
92
+ <Toaster>
93
+ <TooltipProvider>
94
+ <RouterProvider router={router} />
95
+ </TooltipProvider>
96
+ </Toaster>
91
97
  </QueryClientProvider>
92
98
  </DashboardErrorBoundary>,
93
99
  );
@@ -33,13 +33,15 @@ export function mockAutomationCollection(
33
33
  (automation.status !== "paused" &&
34
34
  automation.status !== "completed" &&
35
35
  (automation.status === "blocked" ||
36
- (automation.kind === "event" && !automation.triggerAvailable) ||
36
+ (automation.kind === "event" &&
37
+ automation.trigger.problems.length > 0) ||
37
38
  automation.lastRunStatus === "failed" ||
38
39
  automation.lastRunStatus === "blocked"))) &&
39
40
  (input.type === "all" || automation.kind === input.type) &&
40
41
  (input.state === "all" ||
41
42
  (input.state === "unavailable"
42
- ? automation.kind === "event" && !automation.triggerAvailable
43
+ ? automation.kind === "event" &&
44
+ automation.trigger.problems.length > 0
43
45
  : automation.status === input.state)) &&
44
46
  (!input.creator || creator(automation) === input.creator) &&
45
47
  (!input.destination || destination(automation) === input.destination) &&
@@ -2972,7 +2972,6 @@ function mockTasks(): AutomationSummary[] {
2972
2972
  teamId: "T123",
2973
2973
  visibility: "public",
2974
2974
  },
2975
- events: ["issue.closed"],
2976
2975
  id: "event-1",
2977
2976
  instruction: "Summarize the closed issue",
2978
2977
  kind: "event",
@@ -2986,7 +2985,11 @@ function mockTasks(): AutomationSummary[] {
2986
2985
  outcomes: [],
2987
2986
  title: "Closed issue summary",
2988
2987
  totalRuns: 7,
2989
- triggerAvailable: true,
2988
+ trigger: {
2989
+ events: ["issue closed"],
2990
+ filters: ["labels includes bug", "repository is api or web"],
2991
+ problems: [],
2992
+ },
2990
2993
  status: "active",
2991
2994
  },
2992
2995
  {
@@ -2999,7 +3002,6 @@ function mockTasks(): AutomationSummary[] {
2999
3002
  teamId: "T123",
3000
3003
  visibility: "public",
3001
3004
  },
3002
- events: ["incident.updated"],
3003
3005
  id: "event-2",
3004
3006
  instruction: "Notify responders when the incident changes",
3005
3007
  kind: "event",
@@ -3016,7 +3018,11 @@ function mockTasks(): AutomationSummary[] {
3016
3018
  ],
3017
3019
  title: "Incident change alerts",
3018
3020
  totalRuns: 0,
3019
- triggerAvailable: false,
3021
+ trigger: {
3022
+ events: ["incident updated"],
3023
+ filters: [],
3024
+ problems: ["The plugin for this source is not enabled."],
3025
+ },
3020
3026
  status: "active",
3021
3027
  },
3022
3028
  ];
@@ -3115,7 +3121,19 @@ export function readMockAutomationExecutions(
3115
3121
  const costUsd = costs[index % costs.length];
3116
3122
  const durationMs = durations[index % durations.length];
3117
3123
  const totalTokens = tokens[index % tokens.length];
3124
+ const executedAtMs = nowMs - index * 86_400_000 - 3_600_000;
3118
3125
  return {
3126
+ automationVersion: index < 3 ? 2 : 1,
3127
+ credentialMode: task.credentialMode,
3128
+ ...(status === "failed"
3129
+ ? { error: "Slack rejected the result message: channel_not_found." }
3130
+ : status === "blocked"
3131
+ ? { error: "GitHub needs the creator to reconnect their account." }
3132
+ : undefined),
3133
+ ...(task.kind === "scheduled"
3134
+ ? { scheduledFor: new Date(executedAtMs - 60_000).toISOString() }
3135
+ : undefined),
3136
+ startedAt: new Date(executedAtMs - (durationMs ?? 5_000)).toISOString(),
3119
3137
  ...(hasConversation
3120
3138
  ? {
3121
3139
  conversationId:
@@ -3128,9 +3146,7 @@ export function readMockAutomationExecutions(
3128
3146
  ...(totalTokens !== undefined ? { totalTokens } : undefined),
3129
3147
  }
3130
3148
  : undefined),
3131
- executedAt: new Date(
3132
- nowMs - index * 86_400_000 - 3_600_000,
3133
- ).toISOString(),
3149
+ executedAt: new Date(executedAtMs).toISOString(),
3134
3150
  executionId: `${id}-run-${index + 1}`,
3135
3151
  status,
3136
3152
  };
@@ -17,6 +17,7 @@ import {
17
17
  conversationParamsSchema,
18
18
  conversationPendingMessagesReportSchema,
19
19
  conversationStatsReportSchema,
20
+ conversationWatchesReportSchema,
20
21
  codeOverviewReportSchema,
21
22
  codePersonReportSchema,
22
23
  codeRepositoryParamsSchema,
@@ -58,6 +59,7 @@ import {
58
59
  readMockAutomationList,
59
60
  setMockConversationArchived,
60
61
  } from "./fixtures";
62
+ import { readMockConversationWatches } from "./watches";
61
63
 
62
64
  function errorResponse(error: string, status: 400 | 404): Response {
63
65
  return jsonResponse(apiErrorSchema, { error }, { status });
@@ -205,6 +207,16 @@ export function createMockReportingApi(): Hono<{
205
207
  ? jsonResponse(conversationChildAgentsReportSchema, report)
206
208
  : errorResponse("Conversation not found.", 404);
207
209
  });
210
+ app.get("/conversations/:conversationId/watches", (c) => {
211
+ const params = conversationParamsSchema.safeParse(c.req.param());
212
+ if (!params.success) {
213
+ return errorResponse("Invalid route parameters.", 400);
214
+ }
215
+ const report = readMockConversationWatches(params.data.conversationId);
216
+ return report
217
+ ? jsonResponse(conversationWatchesReportSchema, report)
218
+ : errorResponse("Conversation not found.", 404);
219
+ });
208
220
  app.get("/conversations/:conversationId/pending-messages", (c) => {
209
221
  const params = conversationParamsSchema.safeParse(c.req.param());
210
222
  if (!params.success) {