@sentry/junior-dashboard 0.259.0 → 0.261.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 (71) 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 +6 -6
  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 +14 -3
  12. package/src/client/components/Select.tsx +41 -7
  13. package/src/client/components/ThemeSwitcher.tsx +48 -0
  14. package/src/client/components/charts/ActivityChart.tsx +6 -7
  15. package/src/client/components/charts/InputCacheChart.tsx +1 -1
  16. package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
  17. package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
  18. package/src/client/components/charts/cache-input.ts +11 -3
  19. package/src/client/components/controls/DirectorySortSelect.tsx +6 -4
  20. package/src/client/components/layout/DashboardHeader.tsx +2 -9
  21. package/src/client/conversations/ConversationBrief.tsx +35 -9
  22. package/src/client/conversations/ConversationHomeList.tsx +7 -1
  23. package/src/client/conversations/ConversationMeta.tsx +19 -7
  24. package/src/client/conversations/ConversationTranscript.tsx +8 -11
  25. package/src/client/conversations/ConversationWorkspace.tsx +9 -12
  26. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -4
  27. package/src/client/conversations/EventDetails.tsx +9 -5
  28. package/src/client/conversations/README.md +6 -0
  29. package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
  30. package/src/client/conversations/TranscriptEventView.tsx +48 -9
  31. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  32. package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
  33. package/src/client/conversations/TranscriptText.tsx +28 -20
  34. package/src/client/conversations/conversationActivityPreview.ts +6 -0
  35. package/src/client/conversations/eventTranscript.ts +4 -1
  36. package/src/client/conversations/transcriptMentions.tsx +27 -10
  37. package/src/client/debouncedValue.ts +14 -0
  38. package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
  39. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
  40. package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
  41. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
  42. package/src/client/pages/automations/AutomationFilters.tsx +4 -3
  43. package/src/client/pages/code/CodeActivityChart.tsx +4 -4
  44. package/src/client/pages/code/RepositoryTabs.tsx +3 -2
  45. package/src/client/pages/dev/ComponentsPage.tsx +11 -3
  46. package/src/client/pages/dev/ConversationFixture.tsx +29 -0
  47. package/src/client/pages/dev/SelectFixtures.tsx +62 -0
  48. package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
  49. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
  50. package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
  51. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
  52. package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
  53. package/src/client/pages/people/ContributionGrid.tsx +8 -8
  54. package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
  55. package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
  56. package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
  57. package/src/client/pages/system/PluginBarChart.tsx +12 -6
  58. package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
  59. package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
  60. package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
  61. package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
  62. package/src/client/pages/system/WorkspaceList.tsx +6 -1
  63. package/src/client/pages/system/WorkspacesPage.tsx +18 -12
  64. package/src/client/pages/system/workspaceDraft.ts +8 -0
  65. package/src/client/theme.ts +52 -0
  66. package/src/client/types.ts +8 -1
  67. package/src/mock-reporting/fixtures.ts +48 -35
  68. package/src/shell.ts +4 -1
  69. package/src/tailwind.css +109 -0
  70. package/src/theme.ts +49 -0
  71. package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
@@ -22,6 +22,7 @@ import { formatCostSummary, peoplePath } from "../format";
22
22
  import { pluginUserPagePath } from "../pages/user/PluginUserPage";
23
23
  import { cn, dashboardInteractiveTextClass } from "../styles";
24
24
  import type { Identity } from "../types";
25
+ import { ThemeSwitcher } from "./ThemeSwitcher";
25
26
 
26
27
  type ProfileMenuProps = {
27
28
  identity: Identity;
@@ -302,6 +303,7 @@ export function ProfileMenu({
302
303
  >
303
304
  Log out
304
305
  </button>
306
+ <ThemeSwitcher className="px-3 py-2" />
305
307
  </nav>
306
308
  );
307
309
  }
@@ -405,6 +407,7 @@ export function ProfileMenu({
405
407
  id="profile-popover"
406
408
  >
407
409
  {identityBlock}
410
+ <ThemeSwitcher className="border-b border-dashboard-border-strong px-2.5 py-2" />
408
411
  {menuLinks(() => setOpen(false))}
409
412
  </div>
410
413
  ) : null}
@@ -6,8 +6,18 @@ the policy. `pnpm dashboard-style:check` enforces the token rules below.
6
6
 
7
7
  ## Foundations
8
8
 
9
- Tokens live in the `@theme` block of `src/tailwind.css`. Light mode does not
10
- exist yet. Keep every color in a token so a second theme can redefine them.
9
+ Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
10
+ token so each theme can redefine it.
11
+
12
+ - **Themes.** Dark is the default. The light theme redefines every token under
13
+ `:root[data-theme="light"]`. It also mirrors the light status and chart
14
+ shades (50-400) to their dark pair, so `text-rose-300` stays readable on the
15
+ light canvas. The head script in `src/theme.ts` sets `data-theme` before first
16
+ paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
17
+ the choice for this browser. Use the `light:` variant only for one-off values
18
+ that no token covers, such as provider brand colors.
19
+ - **Charts.** Use `var(--color-*)` for series colors in SVG attributes, not hex
20
+ values, so they follow the theme.
11
21
 
12
22
  - **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
13
23
  `bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
@@ -63,7 +73,8 @@ exist yet. Keep every color in a token so a second theme can redefine them.
63
73
  and `SearchInput`. Use `TextInput size="comfortable"` for larger mobile
64
74
  controls. Use `TextArea prose` for instructions, not code. `Field error`
65
75
  renders an error at `<htmlFor>-error`. The owning control must set
66
- `aria-invalid` and `aria-describedby`. `Select` matches native text controls.
76
+ `aria-invalid` and `aria-describedby`. `Select` owns the native control and its
77
+ inset arrow. Use its `filter` and `sort` variants for compact controls.
67
78
  - **Status.** Use `StatusDot` for dense table or list markers. Use `StatusChip`
68
79
  when the label must stay visible. `StatusChip` is a small dot and
69
80
  sentence-case text, with no border or fill. It is not a control.
@@ -1,18 +1,52 @@
1
1
  import type { SelectHTMLAttributes } from "react";
2
+ import { ChevronDown } from "lucide-react";
2
3
  import { cn } from "../styles";
3
4
 
4
- /** Match native form choices to the dashboard text controls. */
5
+ const selectClassName = {
6
+ form: "min-h-11 rounded-md border border-dashboard-border-emphasis bg-dashboard-ink pl-3 py-2 text-base text-dashboard-text focus:border-dashboard-focus focus:outline-none aria-invalid:border-rose-300 sm:min-h-10 sm:text-sm",
7
+ filter:
8
+ "h-9 rounded-lg border border-dashboard-border-emphasis bg-dashboard-control pl-2 text-sm text-dashboard-text transition-colors hover:border-dashboard-border-interactive focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25",
9
+ sort: "h-full bg-dashboard-control pl-2 font-sans text-xs text-dashboard-text-muted outline-none",
10
+ };
11
+
12
+ /** Keep native choices and position their arrow consistently across the dashboard. */
5
13
  export function Select({
6
14
  className,
15
+ variant = "form",
7
16
  ...props
8
- }: SelectHTMLAttributes<HTMLSelectElement>) {
17
+ }: SelectHTMLAttributes<HTMLSelectElement> & {
18
+ variant?: keyof typeof selectClassName;
19
+ }) {
20
+ const showArrow = !props.multiple && (props.size ?? 1) <= 1;
21
+
9
22
  return (
10
- <select
11
- {...props}
23
+ <span
12
24
  className={cn(
13
- "block min-h-11 w-full min-w-0 rounded-md border border-dashboard-border-emphasis bg-dashboard-ink px-3 py-2 text-base text-dashboard-text focus:border-dashboard-focus focus:outline-none aria-invalid:border-rose-300 disabled:cursor-not-allowed disabled:opacity-60 sm:min-h-10 sm:text-sm",
14
- className,
25
+ "relative block w-full min-w-0",
26
+ variant === "sort" && "h-full",
15
27
  )}
16
- />
28
+ >
29
+ <select
30
+ {...props}
31
+ className={cn(
32
+ "block w-full min-w-0 disabled:cursor-not-allowed disabled:opacity-60 [color-scheme:var(--dashboard-color-scheme,dark)]",
33
+ selectClassName[variant],
34
+ showArrow
35
+ ? "appearance-none pr-9 forced-colors:appearance-auto"
36
+ : "pr-3",
37
+ className,
38
+ )}
39
+ />
40
+ {showArrow ? (
41
+ <ChevronDown
42
+ aria-hidden="true"
43
+ className={cn(
44
+ "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-dashboard-text-muted forced-colors:hidden",
45
+ props.disabled && "opacity-60",
46
+ )}
47
+ size={16}
48
+ />
49
+ ) : null}
50
+ </span>
17
51
  );
18
52
  }
@@ -0,0 +1,48 @@
1
+ import { Monitor, Moon, Sun, type LucideIcon } from "lucide-react";
2
+
3
+ import type { DashboardThemeChoice } from "../../theme";
4
+ import { cn } from "../styles";
5
+ import { useDashboardThemeChoice } from "../theme";
6
+ import { ToggleButton } from "./Button";
7
+
8
+ const themeChoices = [
9
+ { Icon: Monitor, label: "System", value: "system" },
10
+ { Icon: Sun, label: "Light", value: "light" },
11
+ { Icon: Moon, label: "Dark", value: "dark" },
12
+ ] as const satisfies ReadonlyArray<{
13
+ Icon: LucideIcon;
14
+ label: string;
15
+ value: DashboardThemeChoice;
16
+ }>;
17
+
18
+ /** Choose the dashboard theme for this browser: system, light, or dark. */
19
+ export function ThemeSwitcher(props: { className?: string }) {
20
+ const [choice, setChoice] = useDashboardThemeChoice();
21
+ return (
22
+ <div
23
+ className={cn("flex items-center justify-between gap-3", props.className)}
24
+ >
25
+ <span className="font-sans text-sm font-semibold text-dashboard-text">
26
+ Theme
27
+ </span>
28
+ <div
29
+ aria-label="Theme"
30
+ className="flex gap-0.5 rounded-lg border border-dashboard-border p-0.5"
31
+ role="group"
32
+ >
33
+ {themeChoices.map(({ Icon, label, value }) => (
34
+ <ToggleButton
35
+ aria-label={`${label} theme`}
36
+ key={value}
37
+ onClick={() => setChoice(value)}
38
+ pressed={choice === value}
39
+ title={label}
40
+ variant="segment"
41
+ >
42
+ <Icon aria-hidden="true" size={14} strokeWidth={2} />
43
+ </ToggleButton>
44
+ ))}
45
+ </div>
46
+ </div>
47
+ );
48
+ }
@@ -248,7 +248,7 @@ export function ActivityChartGrid(props: {
248
248
  return (
249
249
  <g key={ratio}>
250
250
  <line
251
- stroke="rgba(255,255,255,0.07)"
251
+ stroke="var(--color-dashboard-border-subtle)"
252
252
  strokeDasharray="3 5"
253
253
  x1={props.layout.left}
254
254
  x2={props.layout.width - props.layout.right}
@@ -332,9 +332,6 @@ export function activityChartAverage(values: readonly number[]): number {
332
332
  export const chartAverageLabelClassName =
333
333
  "fill-dashboard-text-solid font-sans tabular-nums leading-none";
334
334
 
335
- /** Solid chip behind average labels so series colors never wash out the text. */
336
- const chartAverageLabelChip = "rgba(9, 12, 14, 0.92)";
337
-
338
335
  /**
339
336
  * Opt-in horizontal average across the plotted buckets.
340
337
  * Renders a dashed guide plus a compact "value / day" label on the right.
@@ -380,7 +377,7 @@ export function ActivityChartAverageLine(props: {
380
377
  return (
381
378
  <g aria-label={`average ${label}`} pointerEvents="none">
382
379
  <line
383
- stroke={props.stroke ?? "rgba(255,255,255,0.55)"}
380
+ stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
384
381
  strokeDasharray="4 4"
385
382
  strokeOpacity={0.85}
386
383
  strokeWidth={1.25}
@@ -389,8 +386,10 @@ export function ActivityChartAverageLine(props: {
389
386
  y1={y}
390
387
  y2={y}
391
388
  />
389
+ {/* Solid chip so series colors never wash out the label text. */}
392
390
  <rect
393
- fill={chartAverageLabelChip}
391
+ fill="var(--color-dashboard-bg-elevated)"
392
+ fillOpacity={0.92}
394
393
  height={chipHeight}
395
394
  rx={3}
396
395
  width={chipWidth}
@@ -399,7 +398,7 @@ export function ActivityChartAverageLine(props: {
399
398
  />
400
399
  <text
401
400
  className={chartAverageLabelClassName}
402
- fill="#ffffff"
401
+ fill="var(--color-dashboard-text-solid)"
403
402
  fontSize={fontSize}
404
403
  textAnchor="middle"
405
404
  x={chipX + chipWidth / 2}
@@ -164,7 +164,7 @@ export function InputCacheChart(props: {
164
164
  {bars}
165
165
  {incomplete ? (
166
166
  <rect
167
- fill="#94a3b8"
167
+ fill="var(--color-slate-400)"
168
168
  x={x(index) - barWidth / 2}
169
169
  y={layout.top + layout.plotHeight - 3}
170
170
  width={barWidth}
@@ -49,7 +49,7 @@ function compactDuration(value: number): string {
49
49
  function tokenChart(): ChartConfig {
50
50
  return {
51
51
  axisFormat: formatCompactNumber,
52
- color: "#22d3ee",
52
+ color: "var(--color-cyan-400)",
53
53
  format: formatCompactNumber,
54
54
  metric: "tokens",
55
55
  title: "Token usage",
@@ -61,7 +61,7 @@ function supportingCharts(): ChartConfig[] {
61
61
  return [
62
62
  {
63
63
  axisFormat: compactCurrency,
64
- color: "#fbbf24",
64
+ color: "var(--color-amber-400)",
65
65
  format: (value) => formatCostSummary({ total: value }),
66
66
  metric: "costUsd",
67
67
  title: "Model spend",
@@ -69,7 +69,7 @@ function supportingCharts(): ChartConfig[] {
69
69
  },
70
70
  {
71
71
  axisFormat: compactDuration,
72
- color: "#a78bfa",
72
+ color: "var(--color-violet-400)",
73
73
  format: formatDuration,
74
74
  metric: "durationMs",
75
75
  title: "Runtime",
@@ -14,7 +14,7 @@ import { Card } from "../layout/Card";
14
14
  import { formatActivityChartAverage, formatCompactNumber } from "../../format";
15
15
  import type { WorkspaceUsageDay } from "./workspaceUsage";
16
16
 
17
- const USAGE_COLOR = "#a78bfa";
17
+ const USAGE_COLOR = "var(--color-violet-400)";
18
18
 
19
19
  /** Plot successful Workspace switches for one recipe over a trailing window. */
20
20
  export function WorkspaceUsageChart(props: {
@@ -1,9 +1,17 @@
1
1
  import type { ConversationMetricDay } from "@sentry/junior/api/schema";
2
2
 
3
3
  export const CACHE_INPUT_SERIES = [
4
- { key: "cachedInputTokens", label: "From cache", color: "#22d3ee" },
5
- { key: "cacheCreationTokens", label: "Written to cache", color: "#fbbf24" },
6
- { key: "inputTokens", label: "Uncached", color: "#a78bfa" },
4
+ {
5
+ key: "cachedInputTokens",
6
+ label: "From cache",
7
+ color: "var(--color-cyan-400)",
8
+ },
9
+ {
10
+ key: "cacheCreationTokens",
11
+ label: "Written to cache",
12
+ color: "var(--color-amber-400)",
13
+ },
14
+ { key: "inputTokens", label: "Uncached", color: "var(--color-violet-400)" },
7
15
  ] as const;
8
16
 
9
17
  export type CacheInput = Pick<
@@ -1,9 +1,11 @@
1
+ import { Select } from "../Select";
2
+
1
3
  export type DirectorySortOption = {
2
4
  label: string;
3
5
  value: string;
4
6
  };
5
7
 
6
- /** Render a native directory sort control with an explicit dark color scheme. */
8
+ /** Keep directory sorting consistent with the dashboard's shared select. */
7
9
  export function DirectorySortSelect(props: {
8
10
  ariaLabel: string;
9
11
  onChange(value: string): void;
@@ -15,9 +17,9 @@ export function DirectorySortSelect(props: {
15
17
  <span className="flex h-full items-center border-r border-dashboard-border-subtle px-2 font-sans font-medium text-xs text-dashboard-text-muted">
16
18
  Sort
17
19
  </span>
18
- <select
20
+ <Select
19
21
  aria-label={props.ariaLabel}
20
- className="h-full min-w-0 bg-dashboard-control px-2 font-sans text-xs text-dashboard-text-muted outline-none [color-scheme:var(--dashboard-color-scheme,dark)]"
22
+ variant="sort"
21
23
  value={props.value}
22
24
  onChange={(event) => props.onChange(event.currentTarget.value)}
23
25
  >
@@ -30,7 +32,7 @@ export function DirectorySortSelect(props: {
30
32
  {option.label}
31
33
  </option>
32
34
  ))}
33
- </select>
35
+ </Select>
34
36
  </label>
35
37
  );
36
38
  }
@@ -14,11 +14,7 @@ import {
14
14
  MobileSecondaryNavigationSlot,
15
15
  useRegisterOpenMobileNavigation,
16
16
  } from "./DashboardChrome";
17
- import {
18
- cn,
19
- dashboardContainerClass,
20
- dashboardInteractiveTextClass,
21
- } from "../../styles";
17
+ import { cn, dashboardInteractiveTextClass } from "../../styles";
22
18
 
23
19
  export type DashboardHeaderNavItem = {
24
20
  key: string;
@@ -37,7 +33,6 @@ const focusableSelector = [
37
33
 
38
34
  /** Render the primary dashboard shell header and optional mobile nav sheet. */
39
35
  export function DashboardHeader(props: {
40
- compact?: boolean;
41
36
  /** Back target used on mobile conversation detail. */
42
37
  mobileBackTo?: string;
43
38
  mobileNavigationOpen: boolean;
@@ -151,12 +146,10 @@ export function DashboardHeader(props: {
151
146
  <header className="relative bg-dashboard-bg">
152
147
  <div
153
148
  className={cn(
154
- props.compact ? "w-full min-w-0" : dashboardContainerClass,
155
- "grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:gap-x-5 md:gap-y-3 md:px-4 md:py-4",
149
+ "grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:gap-x-5 md:gap-y-3 md:px-7 md:py-3",
156
150
  props.profile
157
151
  ? "md:grid-cols-[auto_minmax(0,1fr)_auto]"
158
152
  : "md:grid-cols-[auto_minmax(0,1fr)]",
159
- props.compact ? "md:px-7 md:py-3" : "md:px-8",
160
153
  )}
161
154
  >
162
155
  {conversationMode && props.mobileBackTo ? (
@@ -5,6 +5,7 @@ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
5
5
  import { Detail, DetailList } from "../components/DetailList";
6
6
  import { StatusChip, type StatusChipTone } from "../components/StatusChip";
7
7
  import { formatTime } from "../format";
8
+ import { MentionPeopleProvider, renderMentionText } from "./transcriptMentions";
8
9
 
9
10
  type ConversationBriefReport = NonNullable<ConversationDetailReport["brief"]>;
10
11
 
@@ -24,13 +25,24 @@ const outcomeTones: Record<BriefContent["outcome"]["status"], StatusChipTone> =
24
25
  export function ConversationBrief(props: {
25
26
  brief: ConversationBriefReport;
26
27
  variant?: "summary" | "full";
28
+ }) {
29
+ return (
30
+ <MentionPeopleProvider mentions={props.brief.mentions ?? []}>
31
+ <BriefContentView brief={props.brief} variant={props.variant} />
32
+ </MentionPeopleProvider>
33
+ );
34
+ }
35
+
36
+ function BriefContentView(props: {
37
+ brief: ConversationBriefReport;
38
+ variant?: "summary" | "full";
27
39
  }) {
28
40
  const { content } = props.brief;
29
41
  if (props.variant === "summary") {
30
42
  return (
31
43
  <div className="grid min-w-0 gap-4">
32
44
  <p className="m-0 text-sm leading-relaxed text-dashboard-text">
33
- {content.summary}
45
+ <BriefText text={content.summary} />
34
46
  </p>
35
47
  {content.openDecisions.length > 0 ? (
36
48
  <div className="grid gap-2">
@@ -58,12 +70,16 @@ export function ConversationBrief(props: {
58
70
  </span>
59
71
  </div>
60
72
  <p className="m-0 text-sm leading-relaxed text-dashboard-text">
61
- {content.summary}
73
+ <BriefText text={content.summary} />
62
74
  </p>
63
75
  <BriefSection title="Intent & outcome">
64
76
  <DetailList>
65
- <Detail label="Intent">{content.intent}</Detail>
66
- <Detail label="Outcome">{content.outcome.text}</Detail>
77
+ <Detail label="Intent">
78
+ <BriefText text={content.intent} />
79
+ </Detail>
80
+ <Detail label="Outcome">
81
+ <BriefText text={content.outcome.text} />
82
+ </Detail>
67
83
  </DetailList>
68
84
  </BriefSection>
69
85
  {content.decisions.length || content.openDecisions.length ? (
@@ -169,11 +185,13 @@ function BriefDecisions(props: { decisions: BriefContent["decisions"] }) {
169
185
  >
170
186
  {decision.kind}
171
187
  </StatusChip>
172
- <span className="min-w-0">{decision.text}</span>
188
+ <span className="min-w-0">
189
+ <BriefText text={decision.text} />
190
+ </span>
173
191
  </div>
174
192
  {decision.by ? (
175
193
  <div className="mt-0.5 text-xs text-dashboard-text-muted">
176
- by · {decision.by}
194
+ by · <BriefText text={decision.by} />
177
195
  </div>
178
196
  ) : null}
179
197
  </li>
@@ -189,10 +207,12 @@ function BriefOpenDecisions(props: {
189
207
  <ul className="m-0 grid list-none gap-2 p-0">
190
208
  {props.decisions.map((decision, index) => (
191
209
  <li key={`${decision.text}:${index}`}>
192
- <div>{decision.text}</div>
210
+ <div>
211
+ <BriefText text={decision.text} />
212
+ </div>
193
213
  {decision.owner ? (
194
214
  <div className="mt-1 text-xs text-dashboard-text-muted">
195
- Owner: {decision.owner}
215
+ Owner: <BriefText text={decision.owner} />
196
216
  </div>
197
217
  ) : null}
198
218
  </li>
@@ -205,12 +225,18 @@ function BriefList(props: { items: string[] }) {
205
225
  return (
206
226
  <ul className="my-0 grid gap-1.5 pl-4">
207
227
  {props.items.map((item, index) => (
208
- <li key={`${item}:${index}`}>{item}</li>
228
+ <li key={`${item}:${index}`}>
229
+ <BriefText text={item} />
230
+ </li>
209
231
  ))}
210
232
  </ul>
211
233
  );
212
234
  }
213
235
 
236
+ function BriefText(props: { text: string }) {
237
+ return <>{renderMentionText(props.text, "brief")}</>;
238
+ }
239
+
214
240
  function displayLabel(value: string): string {
215
241
  return value.replaceAll("_", " ");
216
242
  }
@@ -22,6 +22,7 @@ import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
22
22
  import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
23
23
  import { conversationPath } from "./conversationRoutes";
24
24
  import { useArchiveConversation } from "./queries";
25
+ import { MentionText } from "./transcriptMentions";
25
26
  import {
26
27
  buildConversationSections,
27
28
  type ConversationSection,
@@ -265,7 +266,12 @@ function ConversationCard(props: {
265
266
  </div>
266
267
  {conversation.activityPreview ? (
267
268
  <p className="relative z-[1] m-0 line-clamp-2 font-sans text-sm leading-relaxed text-dashboard-text-subtle pointer-events-none">
268
- {formatConversationActivityPreview(conversation.activityPreview.text)}
269
+ <MentionText
270
+ mentions={conversation.activityPreview.mentions ?? []}
271
+ text={formatConversationActivityPreview(
272
+ conversation.activityPreview.text,
273
+ )}
274
+ />
269
275
  </p>
270
276
  ) : (
271
277
  <p className="relative z-[1] m-0 font-sans text-sm leading-relaxed text-dashboard-text-muted pointer-events-none">
@@ -167,7 +167,7 @@ export function ConversationSidebarAnnotations(props: {
167
167
  {useMobileFacepile ? (
168
168
  <SidebarAnnotationIconFacepile
169
169
  annotations={mobileFacepileAnnotations}
170
- // Discs sit on the sidebar panel / row surface (#09090b).
170
+ // Discs sit on the sidebar panel / row surface.
171
171
  cutoutColor="var(--color-dashboard-surface-panel)"
172
172
  />
173
173
  ) : (
@@ -343,21 +343,33 @@ type SidebarAnnotationIconName = NonNullable<
343
343
  >;
344
344
 
345
345
  const SIDEBAR_ICON_PRESENTATION = {
346
- "circle-dot": { className: "text-[#3fb950]", Icon: CircleDot, label: "Open" },
346
+ "circle-dot": {
347
+ className: "text-[#3fb950] light:text-[#1a7f37]",
348
+ Icon: CircleDot,
349
+ label: "Open",
350
+ },
347
351
  "circle-dashed": {
348
- className: "text-[#8c959f]",
352
+ className: "text-[#8c959f] light:text-[#59636e]",
349
353
  Icon: CircleDashed,
350
354
  label: "Draft",
351
355
  },
352
- "circle-x": { className: "text-[#f85149]", Icon: CircleX, label: "Closed" },
353
- "git-merge": { className: "text-[#a371f7]", Icon: GitMerge, label: "Merged" },
356
+ "circle-x": {
357
+ className: "text-[#f85149] light:text-[#cf222e]",
358
+ Icon: CircleX,
359
+ label: "Closed",
360
+ },
361
+ "git-merge": {
362
+ className: "text-[#a371f7] light:text-[#8250df]",
363
+ Icon: GitMerge,
364
+ label: "Merged",
365
+ },
354
366
  "git-pull-request": {
355
- className: "text-[#3fb950]",
367
+ className: "text-[#3fb950] light:text-[#1a7f37]",
356
368
  Icon: GitPullRequest,
357
369
  label: "Open pull request",
358
370
  },
359
371
  "triangle-alert": {
360
- className: "text-[#d29922]",
372
+ className: "text-[#d29922] light:text-[#9a6700]",
361
373
  Icon: TriangleAlert,
362
374
  label: "Needs attention",
363
375
  },
@@ -43,7 +43,6 @@ import { transcriptEmptyClass } from "./transcriptStyles";
43
43
  import { entryMatchesSearch, useTranscriptSearch } from "./transcriptSearch";
44
44
  import { TranscriptRow, TranscriptRows } from "./TranscriptRows";
45
45
  import { TranscriptTimestampProvider } from "./TranscriptTimestamp";
46
- import { TranscriptMentionPeopleProvider } from "./transcriptMentions";
47
46
 
48
47
  type TranscriptEntry = ReturnType<typeof groupTranscriptMessages>[number];
49
48
  type TranscriptContextEntry = Extract<TranscriptEntry, { kind: "context" }>;
@@ -86,16 +85,14 @@ export const ConversationTranscriptView = memo(
86
85
 
87
86
  return (
88
87
  <TranscriptTimestampProvider>
89
- <TranscriptMentionPeopleProvider messages={messages}>
90
- <section className="min-w-0 pt-1">
91
- <SegmentEvents
92
- onOpenSubagentTranscript={props.onOpenSubagentTranscript}
93
- conversation={props.conversation}
94
- messages={messages}
95
- responding={props.responding}
96
- />
97
- </section>
98
- </TranscriptMentionPeopleProvider>
88
+ <section className="min-w-0 pt-1">
89
+ <SegmentEvents
90
+ onOpenSubagentTranscript={props.onOpenSubagentTranscript}
91
+ conversation={props.conversation}
92
+ messages={messages}
93
+ responding={props.responding}
94
+ />
95
+ </section>
99
96
  </TranscriptTimestampProvider>
100
97
  );
101
98
  },
@@ -1,5 +1,5 @@
1
1
  import type { InputImage } from "@sentry/junior/api/schema";
2
- import { useDeferredValue, useEffect, useMemo, useState } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { Globe2, LockKeyhole } from "lucide-react";
4
4
  import { useNavigate, useParams } from "react-router";
5
5
 
@@ -17,8 +17,8 @@ import {
17
17
  } from "./queries";
18
18
  import { conversationPath, NEW_CONVERSATION_PATH } from "./conversationRoutes";
19
19
  import { buildConversations, getDashboardTimeZone } from "../format";
20
+ import { useDebouncedValue } from "../debouncedValue";
20
21
  import type { Conversation } from "../types";
21
- import { cn, dashboardContainerClass } from "../styles";
22
22
  import { ConversationPage } from "./ConversationPage";
23
23
  import { useConversationFinishedIndicators } from "./useConversationFinishedIndicators";
24
24
 
@@ -28,7 +28,7 @@ const CONVERSATION_PAGE_SIZE = 20;
28
28
  export function ConversationWorkspace() {
29
29
  const [page, setPage] = useState(1);
30
30
  const [query, setQuery] = useState("");
31
- const search = useDeferredValue(query.trim());
31
+ const search = useDebouncedValue(query.trim());
32
32
  const params = useParams();
33
33
  const navigate = useNavigate();
34
34
  const selectedId = params.conversationId;
@@ -90,12 +90,7 @@ export function ConversationWorkspace() {
90
90
 
91
91
  if (home) {
92
92
  return (
93
- <main
94
- className={cn(
95
- dashboardContainerClass,
96
- "h-full min-h-0 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6",
97
- )}
98
- >
93
+ <main className="h-full min-h-0 w-full overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6">
99
94
  <div className="mx-auto grid w-full max-w-6xl gap-8">
100
95
  {createView}
101
96
  <section aria-label="Conversations" className="grid gap-3">
@@ -192,25 +187,27 @@ function NewConversationView(props: {
192
187
  footerStart={
193
188
  <div
194
189
  aria-label="Conversation visibility"
195
- className="inline-flex items-center gap-1"
190
+ className="inline-flex shrink-0 items-center gap-0.5 rounded-lg border border-dashboard-border bg-dashboard-surface-panel p-0.5"
196
191
  role="group"
197
192
  >
198
193
  <ToggleButton
194
+ className="min-w-20 gap-1.5"
199
195
  onClick={() => setVisibility("public")}
200
196
  pressed={isPublic}
201
197
  type="button"
202
198
  variant="segment"
203
199
  >
204
- <Globe2 aria-hidden="true" className="mr-1 inline size-3" />
200
+ <Globe2 aria-hidden="true" className="size-3" />
205
201
  Public
206
202
  </ToggleButton>
207
203
  <ToggleButton
204
+ className="min-w-20 gap-1.5"
208
205
  onClick={() => setVisibility("private")}
209
206
  pressed={!isPublic}
210
207
  type="button"
211
208
  variant="segment"
212
209
  >
213
- <LockKeyhole aria-hidden="true" className="mr-1 inline size-3" />
210
+ <LockKeyhole aria-hidden="true" className="size-3" />
214
211
  Private
215
212
  </ToggleButton>
216
213
  </div>
@@ -58,10 +58,7 @@ function ConversationHomeLoading() {
58
58
  <main
59
59
  aria-busy="true"
60
60
  aria-live="polite"
61
- className={cn(
62
- dashboardContainerClass,
63
- "h-full min-h-0 overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6",
64
- )}
61
+ className="h-full min-h-0 w-full overflow-y-auto overscroll-contain px-4 py-4 sm:px-6 sm:py-6"
65
62
  role="status"
66
63
  >
67
64
  <span className="sr-only">Loading conversations</span>