@sentry/junior-dashboard 0.259.0 → 0.260.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 (63) hide show
  1. package/README.md +5 -3
  2. package/dist/app.js +88 -37
  3. package/dist/chunks/github-light-QHW3AEMR.js +1 -0
  4. package/dist/client.js +86 -50
  5. package/dist/index.js +88 -37
  6. package/dist/tailwind.css +1 -1
  7. package/package.json +3 -3
  8. package/src/client/App.tsx +0 -1
  9. package/src/client/code.tsx +10 -4
  10. package/src/client/components/ProfileMenu.tsx +3 -0
  11. package/src/client/components/README.md +12 -2
  12. package/src/client/components/ThemeSwitcher.tsx +48 -0
  13. package/src/client/components/charts/ActivityChart.tsx +6 -7
  14. package/src/client/components/charts/InputCacheChart.tsx +1 -1
  15. package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
  16. package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
  17. package/src/client/components/charts/cache-input.ts +11 -3
  18. package/src/client/components/layout/DashboardHeader.tsx +2 -9
  19. package/src/client/conversations/ConversationBrief.tsx +35 -9
  20. package/src/client/conversations/ConversationHomeList.tsx +7 -1
  21. package/src/client/conversations/ConversationMeta.tsx +19 -7
  22. package/src/client/conversations/ConversationTranscript.tsx +8 -11
  23. package/src/client/conversations/EventDetails.tsx +9 -5
  24. package/src/client/conversations/README.md +6 -0
  25. package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
  26. package/src/client/conversations/TranscriptEventView.tsx +48 -9
  27. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  28. package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
  29. package/src/client/conversations/TranscriptText.tsx +28 -20
  30. package/src/client/conversations/conversationActivityPreview.ts +6 -0
  31. package/src/client/conversations/eventTranscript.ts +4 -1
  32. package/src/client/conversations/transcriptMentions.tsx +27 -10
  33. package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
  34. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
  35. package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
  36. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
  37. package/src/client/pages/code/CodeActivityChart.tsx +4 -4
  38. package/src/client/pages/dev/ComponentsPage.tsx +7 -3
  39. package/src/client/pages/dev/ConversationFixture.tsx +29 -0
  40. package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
  41. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
  42. package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
  43. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
  44. package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
  45. package/src/client/pages/people/ContributionGrid.tsx +8 -8
  46. package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
  47. package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
  48. package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
  49. package/src/client/pages/system/PluginBarChart.tsx +12 -6
  50. package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
  51. package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
  52. package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
  53. package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
  54. package/src/client/pages/system/WorkspaceList.tsx +6 -1
  55. package/src/client/pages/system/WorkspacesPage.tsx +18 -12
  56. package/src/client/pages/system/workspaceDraft.ts +8 -0
  57. package/src/client/theme.ts +52 -0
  58. package/src/client/types.ts +8 -1
  59. package/src/mock-reporting/fixtures.ts +48 -35
  60. package/src/shell.ts +4 -1
  61. package/src/tailwind.css +109 -0
  62. package/src/theme.ts +49 -0
  63. package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
@@ -0,0 +1,76 @@
1
+ import type { ReactNode } from "react";
2
+ import type { BaselineSnapshotReport } from "@sentry/junior/api/schema";
3
+
4
+ import { Card } from "../../components/layout/Card";
5
+ import { formatMs, formatRelativeTime, formatTime } from "../../format";
6
+ import { formatSandboxSize } from "./SandboxSizeSummary";
7
+
8
+ /**
9
+ * Show the base Sandbox: the default size and the baseline snapshot.
10
+ * Workspaces inherit the base setup and use the default size unless they set
11
+ * vCPUs.
12
+ */
13
+ export function BaseSandboxCard(props: {
14
+ defaultVcpus: number | null;
15
+ snapshot: BaselineSnapshotReport | null;
16
+ }) {
17
+ const snapshot = props.snapshot;
18
+ return (
19
+ <Card as="section" className="mb-0" padding="md" variant="section">
20
+ <div aria-labelledby="base-sandbox-heading" className="grid gap-4">
21
+ <div className="min-w-0 grid gap-1">
22
+ <h2
23
+ className="m-0 text-base font-semibold text-dashboard-text"
24
+ id="base-sandbox-heading"
25
+ >
26
+ Base Sandbox
27
+ </h2>
28
+ <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
29
+ The base snapshot is inherited by all Workspaces.
30
+ </p>
31
+ </div>
32
+
33
+ <dl className="m-0 grid grid-cols-[max-content_minmax(0,1fr)] gap-x-6 gap-y-2 text-sm">
34
+ <Fact label="Size">{formatSandboxSize(props.defaultVcpus)}</Fact>
35
+ {snapshot ? (
36
+ <>
37
+ {snapshot.dependencyCount == null ? null : (
38
+ <Fact label="Dependencies">{snapshot.dependencyCount}</Fact>
39
+ )}
40
+ <Fact label="Build time">
41
+ {formatMs(snapshot.buildDurationMs)}
42
+ </Fact>
43
+ <Fact label="Generated">
44
+ <span title={formatTime(snapshot.generatedAt)}>
45
+ {formatRelativeTime(snapshot.generatedAt)}
46
+ </span>
47
+ </Fact>
48
+ <Fact label="Snapshot">
49
+ <span className="break-all font-mono text-xs">
50
+ {snapshot.id}
51
+ </span>
52
+ </Fact>
53
+ </>
54
+ ) : (
55
+ <Fact label="Snapshot">
56
+ <span className="text-dashboard-text-muted">
57
+ Not built yet. The first Sandbox without a Workspace builds it.
58
+ </span>
59
+ </Fact>
60
+ )}
61
+ </dl>
62
+ </div>
63
+ </Card>
64
+ );
65
+ }
66
+
67
+ function Fact(props: { children: ReactNode; label: string }) {
68
+ return (
69
+ <div className="contents">
70
+ <dt className="text-dashboard-text-muted">{props.label}</dt>
71
+ <dd className="m-0 min-w-0 tabular-nums text-dashboard-text">
72
+ {props.children}
73
+ </dd>
74
+ </div>
75
+ );
76
+ }
@@ -65,7 +65,7 @@ export function ConversationActivityChart(props: {
65
65
  >
66
66
  <g tabIndex={0}>
67
67
  <rect
68
- fill="#22d3ee"
68
+ fill="var(--color-cyan-400)"
69
69
  height={renderedHeight}
70
70
  opacity={day.conversations ? 0.78 : 0.08}
71
71
  rx="2"
@@ -89,7 +89,7 @@ export function ConversationActivityChart(props: {
89
89
  format={formatActivityChartAverage}
90
90
  layout={layout}
91
91
  maximum={maximum}
92
- stroke="#22d3ee"
92
+ stroke="var(--color-cyan-400)"
93
93
  unit={timeRangeBucketAverageUnit(bucketUnit)}
94
94
  />
95
95
  <ActivityChartDateLabels
@@ -15,12 +15,18 @@ import { formatCostSummary } from "../../format";
15
15
 
16
16
  type Widget = NonNullable<PluginOperationalReport["widgets"]>[number];
17
17
 
18
- const colors = ["#67e8f9", "#6ee7b7", "#fbbf24", "#fb7185", "#a78bfa"] as const;
18
+ const colors = [
19
+ "var(--color-cyan-300)",
20
+ "var(--color-emerald-300)",
21
+ "var(--color-amber-400)",
22
+ "var(--color-rose-400)",
23
+ "var(--color-violet-400)",
24
+ ] as const;
19
25
  const toneColors = {
20
- danger: "#fb7185",
21
- good: "#6ee7b7",
22
- neutral: "#67e8f9",
23
- warning: "#fbbf24",
26
+ danger: "var(--color-rose-400)",
27
+ good: "var(--color-emerald-300)",
28
+ neutral: "var(--color-cyan-300)",
29
+ warning: "var(--color-amber-400)",
24
30
  } as const;
25
31
 
26
32
  function seriesColor(series: Widget["series"][number], index: number): string {
@@ -97,7 +103,7 @@ export function PluginBarChart(props: {
97
103
  return (
98
104
  <g key={index}>
99
105
  <line
100
- stroke="rgba(255,255,255,0.07)"
106
+ stroke="var(--color-dashboard-border-subtle)"
101
107
  strokeDasharray="3 5"
102
108
  x1={layout.left}
103
109
  x2={layout.width - layout.right}
@@ -0,0 +1,47 @@
1
+ import { Cpu } from "lucide-react";
2
+
3
+ import { Detail, DetailList } from "../../components/DetailList";
4
+ import { Card } from "../../components/layout/Card";
5
+
6
+ /** Vercel Sandbox gives each vCPU 2 GB of memory. */
7
+ const MEMORY_GB_PER_VCPU = 2;
8
+
9
+ /** Format a Sandbox vCPU count with its memory. Null means the Vercel default. */
10
+ export function formatSandboxSize(vcpus: number | null): string {
11
+ if (vcpus === null) return "Vercel default";
12
+ return `${vcpus} vCPU${vcpus === 1 ? "" : "s"} · ${vcpus * MEMORY_GB_PER_VCPU} GB`;
13
+ }
14
+
15
+ /** Show one Sandbox size in the shared System summary surface. */
16
+ export function SandboxSizeSummary(props: {
17
+ description: string;
18
+ headingId: string;
19
+ title: string;
20
+ value: string;
21
+ }) {
22
+ return (
23
+ <Card as="section" className="mb-0" padding="md" variant="section">
24
+ <div aria-labelledby={props.headingId} className="grid gap-4">
25
+ <div className="min-w-0 grid gap-1">
26
+ <h2
27
+ className="m-0 text-base font-semibold text-dashboard-text"
28
+ id={props.headingId}
29
+ >
30
+ {props.title}
31
+ </h2>
32
+ <p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
33
+ {props.description}
34
+ </p>
35
+ </div>
36
+ <DetailList>
37
+ <Detail label="Size">
38
+ <span className="inline-flex items-center gap-2">
39
+ <Cpu aria-hidden="true" size={14} />
40
+ {props.value}
41
+ </span>
42
+ </Detail>
43
+ </DetailList>
44
+ </div>
45
+ </Card>
46
+ );
47
+ }
@@ -6,16 +6,19 @@ import { WorkspaceUsageChart } from "../../components/charts/WorkspaceUsageChart
6
6
  import { workspaceUsageDays } from "../../components/charts/workspaceUsage";
7
7
  import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
8
8
  import { Card } from "../../components/layout/Card";
9
+ import { formatSandboxSize, SandboxSizeSummary } from "./SandboxSizeSummary";
9
10
  import { SnapshotSummary } from "./SnapshotSummary";
10
11
 
11
12
  /** Load Workspace usage stats and render snapshot plus switch volume. */
12
13
  export function WorkspaceDetails(props: {
14
+ defaultVcpus: number | null;
13
15
  range: TimeRangeDays;
14
16
  workspace: WorkspaceReport;
15
17
  }) {
16
18
  const statsQuery = useStatsData();
17
19
  return (
18
20
  <WorkspaceDetailsContent
21
+ defaultVcpus={props.defaultVcpus}
19
22
  error={Boolean(statsQuery.error)}
20
23
  loading={statsQuery.isPending}
21
24
  range={props.range}
@@ -25,8 +28,9 @@ export function WorkspaceDetails(props: {
25
28
  );
26
29
  }
27
30
 
28
- /** Present snapshot metadata and switch volume for one Workspace recipe. */
31
+ /** Present snapshot metadata, Sandbox size, and switch volume for one Workspace recipe. */
29
32
  export function WorkspaceDetailsContent(props: {
33
+ defaultVcpus: number | null;
30
34
  error: boolean;
31
35
  loading: boolean;
32
36
  range: TimeRangeDays;
@@ -42,6 +46,12 @@ export function WorkspaceDetailsContent(props: {
42
46
  snapshot={props.workspace.snapshot}
43
47
  title="Current snapshot"
44
48
  />
49
+ <SandboxSizeSummary
50
+ description="Sandboxes for this Workspace start with this size."
51
+ headingId="workspace-sandbox-size-heading"
52
+ title="Sandbox size"
53
+ value={formatSandboxSize(props.workspace.vcpus ?? props.defaultVcpus)}
54
+ />
45
55
  {!props.stats ? (
46
56
  <Card padding="sm">
47
57
  <EmptyTelemetry>
@@ -6,6 +6,7 @@ import { Field } from "../../components/Field";
6
6
  import { InlineError } from "../../components/InlineError";
7
7
  import { TextArea, TextInput } from "../../components/TextInput";
8
8
  import { Card } from "../../components/layout/Card";
9
+ import { formatSandboxSize } from "./SandboxSizeSummary";
9
10
  import {
10
11
  createRepoDraft,
11
12
  type RepoDraft,
@@ -15,6 +16,7 @@ import {
15
16
  type WorkspaceEditorProps = {
16
17
  busy: boolean;
17
18
  canSave: boolean;
19
+ defaultVcpus: number | null;
18
20
  draft: WorkspaceDraft;
19
21
  error?: string;
20
22
  editing: boolean;
@@ -200,6 +202,23 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
200
202
  />
201
203
  </Field>
202
204
 
205
+ <Field
206
+ help="Sandbox vCPUs for this Workspace. Each vCPU adds 2 GB of memory. Leave empty to use the app default."
207
+ htmlFor="workspace-vcpus"
208
+ label="vCPUs"
209
+ >
210
+ <TextInput
211
+ autoComplete="off"
212
+ id="workspace-vcpus"
213
+ inputMode="numeric"
214
+ onChange={(event) =>
215
+ props.onChange({ ...props.draft, vcpus: event.target.value })
216
+ }
217
+ placeholder={formatSandboxSize(props.defaultVcpus)}
218
+ value={props.draft.vcpus}
219
+ />
220
+ </Field>
221
+
203
222
  {props.error ? <InlineError>{props.error}</InlineError> : null}
204
223
 
205
224
  <div className="flex flex-wrap items-center gap-3 border-t border-dashboard-border-subtle pt-5">
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { useEffect, useState } from "react";
3
3
  import { useNavigate, useParams } from "react-router";
4
4
  import {
5
+ workspaceListSchema,
5
6
  workspaceSchema,
6
- type BaselineSnapshotReport,
7
- type WorkspaceReport,
7
+ type WorkspaceListReport,
8
8
  } from "@sentry/junior/api/schema";
9
9
 
10
10
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
@@ -16,10 +16,7 @@ import { fetchDashboardJson, post, put } from "../../http";
16
16
  import { SystemPageLayout } from "./SystemPageLayout";
17
17
  import { WorkspaceDetails } from "./WorkspaceDetails";
18
18
  import { WorkspaceEditor } from "./WorkspaceEditor";
19
- import {
20
- readWorkspaceApiError,
21
- workspacesQueryKey,
22
- } from "./WorkspacesPage";
19
+ import { readWorkspaceApiError, workspacesQueryKey } from "./WorkspacesPage";
23
20
  import {
24
21
  canSaveWorkspaceDraft,
25
22
  createWorkspaceDraft,
@@ -56,6 +53,14 @@ export function WorkspaceFormPage() {
56
53
  ),
57
54
  retry: false,
58
55
  });
56
+ // Shares the list cache so the default size is usually loaded already.
57
+ const workspacesQuery = useQuery({
58
+ queryKey: workspacesQueryKey,
59
+ queryFn: ({ signal }) =>
60
+ fetchDashboardJson(workspaceListSchema, "/api/workspaces", signal),
61
+ retry: false,
62
+ });
63
+ const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
59
64
 
60
65
  // Seed the edit draft once per Workspace id. editWorkspaceDraft assigns fresh
61
66
  // repo keys, so calling it during every query-driven render remounts fields.
@@ -87,18 +92,17 @@ export function WorkspaceFormPage() {
87
92
  },
88
93
  onSuccess: async (workspace) => {
89
94
  await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
90
- queryClient.setQueryData<{
91
- baselineSnapshot: BaselineSnapshotReport | null;
92
- workspaces: WorkspaceReport[];
93
- }>(workspacesQueryKey, (current) => ({
94
- baselineSnapshot: current?.baselineSnapshot ?? null,
95
- workspaces: [
96
- workspace,
97
- ...(current?.workspaces ?? []).filter(
98
- (item) => item.id !== workspace.id,
99
- ),
100
- ].sort((left, right) => left.name.localeCompare(right.name)),
101
- }));
95
+ queryClient.setQueryData<WorkspaceListReport>(
96
+ workspacesQueryKey,
97
+ (current) =>
98
+ current && {
99
+ ...current,
100
+ workspaces: [
101
+ workspace,
102
+ ...current.workspaces.filter((item) => item.id !== workspace.id),
103
+ ].sort((left, right) => left.name.localeCompare(right.name)),
104
+ },
105
+ );
102
106
  navigate("/system/workspaces");
103
107
  },
104
108
  });
@@ -135,7 +139,11 @@ export function WorkspaceFormPage() {
135
139
  />
136
140
 
137
141
  {editing && workspaceQuery.data ? (
138
- <WorkspaceDetails range={range} workspace={workspaceQuery.data} />
142
+ <WorkspaceDetails
143
+ defaultVcpus={defaultVcpus}
144
+ range={range}
145
+ workspace={workspaceQuery.data}
146
+ />
139
147
  ) : null}
140
148
 
141
149
  {workspaceQuery.error ? (
@@ -150,6 +158,7 @@ export function WorkspaceFormPage() {
150
158
  ) : draft ? (
151
159
  <WorkspaceEditor
152
160
  busy={saveMutation.isPending}
161
+ defaultVcpus={defaultVcpus}
153
162
  canSave={canSaveWorkspaceDraft(draft, saveMutation.isPending)}
154
163
  draft={draft}
155
164
  editing={editing}
@@ -6,9 +6,11 @@ import { Button } from "../../components/Button";
6
6
  import { EmptyTelemetry } from "../../components/EmptyTelemetry";
7
7
  import { SelectableRow } from "../../components/SelectableRow";
8
8
  import { Card } from "../../components/layout/Card";
9
+ import { formatSandboxSize } from "./SandboxSizeSummary";
9
10
 
10
11
  type WorkspaceListProps = {
11
12
  busy: boolean;
13
+ defaultVcpus: number | null;
12
14
  onDelete(workspace: WorkspaceReport): void;
13
15
  workspaces: WorkspaceReport[];
14
16
  };
@@ -29,6 +31,7 @@ export function WorkspaceList(props: WorkspaceListProps) {
29
31
  {props.workspaces.map((workspace) => (
30
32
  <WorkspaceListItem
31
33
  busy={props.busy}
34
+ defaultVcpus={props.defaultVcpus}
32
35
  key={workspace.id}
33
36
  onDelete={props.onDelete}
34
37
  workspace={workspace}
@@ -42,6 +45,7 @@ export function WorkspaceList(props: WorkspaceListProps) {
42
45
 
43
46
  function WorkspaceListItem(props: {
44
47
  busy: boolean;
48
+ defaultVcpus: number | null;
45
49
  onDelete(workspace: WorkspaceReport): void;
46
50
  workspace: WorkspaceReport;
47
51
  }) {
@@ -72,7 +76,8 @@ function WorkspaceListItem(props: {
72
76
  </span>
73
77
  <span className="font-mono text-xs text-dashboard-text-muted">
74
78
  {workspace.repos.length} repo
75
- {workspace.repos.length === 1 ? "" : "s"}
79
+ {workspace.repos.length === 1 ? "" : "s"} ·{" "}
80
+ {formatSandboxSize(workspace.vcpus ?? props.defaultVcpus)}
76
81
  </span>
77
82
  </span>
78
83
  <span className="mt-1.5 block truncate font-mono text-xs text-dashboard-text-muted">
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { Plus } from "lucide-react";
3
3
  import {
4
4
  workspaceListSchema,
5
- type BaselineSnapshotReport,
5
+ type WorkspaceListReport,
6
6
  type WorkspaceReport,
7
7
  } from "@sentry/junior/api/schema";
8
8
 
@@ -17,7 +17,7 @@ import {
17
17
  deleteDashboardResource,
18
18
  fetchDashboardJson,
19
19
  } from "../../http";
20
- import { BaselineSnapshotCard } from "./BaselineSnapshotCard";
20
+ import { BaseSandboxCard } from "./BaseSandboxCard";
21
21
  import { SystemPageLayout, SystemRouteLoading } from "./SystemPageLayout";
22
22
  import { WorkspaceList } from "./WorkspaceList";
23
23
 
@@ -55,15 +55,16 @@ export function WorkspacesPage() {
55
55
  },
56
56
  onSuccess: async (workspace) => {
57
57
  await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
58
- queryClient.setQueryData<{
59
- baselineSnapshot: BaselineSnapshotReport | null;
60
- workspaces: WorkspaceReport[];
61
- }>(workspacesQueryKey, (current) => ({
62
- baselineSnapshot: current?.baselineSnapshot ?? null,
63
- workspaces: (current?.workspaces ?? []).filter(
64
- (item) => item.id !== workspace.id,
65
- ),
66
- }));
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
+ );
67
68
  },
68
69
  });
69
70
 
@@ -80,6 +81,7 @@ export function WorkspacesPage() {
80
81
 
81
82
  const workspaces = workspacesQuery.data?.workspaces ?? [];
82
83
  const baselineSnapshot = workspacesQuery.data?.baselineSnapshot ?? null;
84
+ const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
83
85
  return (
84
86
  <SystemPageLayout>
85
87
  <PageHeader
@@ -111,11 +113,15 @@ export function WorkspacesPage() {
111
113
  ) : null}
112
114
 
113
115
  {!workspacesQuery.error ? (
114
- <BaselineSnapshotCard snapshot={baselineSnapshot} />
116
+ <BaseSandboxCard
117
+ defaultVcpus={defaultVcpus}
118
+ snapshot={baselineSnapshot}
119
+ />
115
120
  ) : null}
116
121
 
117
122
  <WorkspaceList
118
123
  busy={deleteMutation.isPending}
124
+ defaultVcpus={defaultVcpus}
119
125
  onDelete={(workspace) => {
120
126
  if (
121
127
  window.confirm(
@@ -9,6 +9,8 @@ export type RepoDraft = {
9
9
  export type WorkspaceDraft = {
10
10
  name: string;
11
11
  setupScript: string;
12
+ /** Text input value. Empty means the app default. */
13
+ vcpus: string;
12
14
  repos: RepoDraft[];
13
15
  };
14
16
 
@@ -24,6 +26,7 @@ export function createWorkspaceDraft(): WorkspaceDraft {
24
26
  return {
25
27
  name: "",
26
28
  setupScript: "",
29
+ vcpus: "",
27
30
  repos: [createRepoDraft()],
28
31
  };
29
32
  }
@@ -32,6 +35,7 @@ export function editWorkspaceDraft(workspace: WorkspaceReport): WorkspaceDraft {
32
35
  return {
33
36
  name: workspace.name,
34
37
  setupScript: workspace.setupScript,
38
+ vcpus: workspace.vcpus === null ? "" : String(workspace.vcpus),
35
39
  repos:
36
40
  workspace.repos.length > 0
37
41
  ? workspace.repos.map((repo) => ({
@@ -48,6 +52,7 @@ export function workspaceDraftBody(draft: WorkspaceDraft) {
48
52
  return {
49
53
  name: draft.name.trim(),
50
54
  setupScript: draft.setupScript,
55
+ vcpus: draft.vcpus.trim() ? Number(draft.vcpus.trim()) : null,
51
56
  repos: draft.repos.map((repo) => ({
52
57
  provider: repo.provider.trim(),
53
58
  repo: repo.repo.trim(),
@@ -60,6 +65,9 @@ export function canSaveWorkspaceDraft(
60
65
  busy: boolean,
61
66
  ): boolean {
62
67
  if (busy || !draft.name.trim()) return false;
68
+ if (draft.vcpus.trim() && !/^[1-9]\d*$/.test(draft.vcpus.trim())) {
69
+ return false;
70
+ }
63
71
  if (draft.repos.some((repo) => !repo.provider.trim() || !repo.repo.trim())) {
64
72
  return false;
65
73
  }
@@ -0,0 +1,52 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ import {
4
+ DASHBOARD_THEME_CHANGE_EVENT,
5
+ DASHBOARD_THEME_STORAGE_KEY,
6
+ type DashboardThemeChoice,
7
+ } from "../theme";
8
+
9
+ function readThemeChoice(): DashboardThemeChoice {
10
+ try {
11
+ const choice = window.localStorage.getItem(DASHBOARD_THEME_STORAGE_KEY);
12
+ return choice === "light" || choice === "dark" ? choice : "system";
13
+ } catch {
14
+ return "system";
15
+ }
16
+ }
17
+
18
+ function subscribe(onChange: () => void): () => void {
19
+ window.addEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
20
+ window.addEventListener("storage", onChange);
21
+ return () => {
22
+ window.removeEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
23
+ window.removeEventListener("storage", onChange);
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Save a theme choice for this browser. The shell theme script applies it
29
+ * and keeps following the system color scheme for `system`.
30
+ */
31
+ function saveThemeChoice(choice: DashboardThemeChoice): void {
32
+ try {
33
+ if (choice === "system") {
34
+ window.localStorage.removeItem(DASHBOARD_THEME_STORAGE_KEY);
35
+ } else {
36
+ window.localStorage.setItem(DASHBOARD_THEME_STORAGE_KEY, choice);
37
+ }
38
+ } catch {
39
+ // Storage is blocked, so the dashboard keeps following the system.
40
+ return;
41
+ }
42
+ window.dispatchEvent(new Event(DASHBOARD_THEME_CHANGE_EVENT));
43
+ }
44
+
45
+ /** Read and update this browser's dashboard theme choice. */
46
+ export function useDashboardThemeChoice(): [
47
+ DashboardThemeChoice,
48
+ (choice: DashboardThemeChoice) => void,
49
+ ] {
50
+ const choice = useSyncExternalStore(subscribe, readThemeChoice);
51
+ return [choice, saveThemeChoice];
52
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  } from "@sentry/junior/api/schema";
10
10
  import type {
11
11
  ActorIdentity,
12
+ ConversationReportMessageEvent,
12
13
  ConversationStatsReport,
13
14
  ConversationSummaryReport,
14
15
  ConversationTurnFailureCode,
@@ -51,6 +52,8 @@ export type TranscriptViewSubagentPart = {
51
52
  };
52
53
 
53
54
  export type TranscriptViewContextEventPart = {
55
+ /** People mentioned in the summary, resolved by the API. */
56
+ mentions?: ActorIdentity[];
54
57
  event:
55
58
  | {
56
59
  createdAt: string;
@@ -81,6 +84,8 @@ export type TranscriptViewContextEventPart = {
81
84
  };
82
85
 
83
86
  export type TranscriptViewStructuredEventPart = {
87
+ /** People mentioned in the preview and details, resolved by the API. */
88
+ mentions?: ActorIdentity[];
84
89
  name: string;
85
90
  namespace: string;
86
91
  presentation: ConversationEventPresentation;
@@ -120,9 +125,11 @@ export type TranscriptViewMessage = {
120
125
  eventObjectType?: ObjectType;
121
126
  /** Short summary supplied by the Event publisher. */
122
127
  trustedSummary?: string;
128
+ /** Every Event when several arrived together in one message. */
129
+ events?: ConversationReportMessageEvent[];
123
130
  /** Whether the source message addressed Junior directly. */
124
131
  explicitMention?: boolean;
125
- /** People mentioned in the message text. */
132
+ /** People mentioned in the event text, resolved by the API. */
126
133
  mentions?: ActorIdentity[];
127
134
  /** Whether a non-mention message was used as input to a turn. */
128
135
  context?: boolean;