@sentry/junior-dashboard 0.276.0 → 0.278.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 (73) hide show
  1. package/dist/app.js +196 -72
  2. package/dist/client.js +94 -89
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +191 -70
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +5 -4
  7. package/src/api/schema.ts +20 -2
  8. package/src/app.ts +80 -12
  9. package/src/auth.ts +5 -1
  10. package/src/client/App.tsx +9 -4
  11. package/src/client/components/DetailsPanel.tsx +1 -1
  12. package/src/client/components/FormNotice.tsx +11 -14
  13. package/src/client/components/ProfileMenu.tsx +5 -6
  14. package/src/client/components/ui/resizable.tsx +50 -0
  15. package/src/client/conversations/AutoArchiveToggle.tsx +71 -0
  16. package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
  17. package/src/client/conversations/ConversationChildAgents.tsx +2 -1
  18. package/src/client/conversations/ConversationDetails.tsx +6 -3
  19. package/src/client/conversations/ConversationHeader.tsx +15 -2
  20. package/src/client/conversations/ConversationHeaderActions.tsx +27 -0
  21. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  22. package/src/client/conversations/ConversationMemories.tsx +64 -67
  23. package/src/client/conversations/ConversationMeta.tsx +2 -7
  24. package/src/client/conversations/ConversationPage.tsx +175 -79
  25. package/src/client/conversations/ConversationSidebar.tsx +82 -25
  26. package/src/client/conversations/ConversationTranscript.tsx +11 -8
  27. package/src/client/conversations/ConversationWatches.tsx +15 -21
  28. package/src/client/conversations/ConversationWorkspace.tsx +132 -44
  29. package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
  30. package/src/client/conversations/DetailsList.tsx +8 -14
  31. package/src/client/conversations/EventDetails.tsx +2 -62
  32. package/src/client/conversations/MemoryIcons.tsx +23 -0
  33. package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
  34. package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
  35. package/src/client/conversations/TranscriptMemoryPopover.tsx +20 -25
  36. package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
  37. package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
  38. package/src/client/conversations/TranscriptView.tsx +89 -20
  39. package/src/client/conversations/autoArchiveQueries.ts +53 -0
  40. package/src/client/conversations/conversationArchive.ts +255 -0
  41. package/src/client/conversations/conversationMemory.ts +2 -1
  42. package/src/client/conversations/conversationSections.ts +24 -18
  43. package/src/client/conversations/conversationSidebarLayout.ts +61 -0
  44. package/src/client/conversations/eventLog.ts +2 -31
  45. package/src/client/conversations/eventTranscript.ts +9 -42
  46. package/src/client/conversations/queries.ts +121 -250
  47. package/src/client/conversations/transcript.ts +46 -16
  48. package/src/client/conversations/transcriptBottomPinning.ts +40 -24
  49. package/src/client/conversations/transcriptReadPosition.ts +31 -7
  50. package/src/client/conversations/transcriptRenderModel.ts +1 -14
  51. package/src/client/conversations/transcriptSearch.tsx +3 -11
  52. package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
  53. package/src/client/conversations/usePinConversation.ts +88 -0
  54. package/src/client/display-text/DisplayText.tsx +7 -2
  55. package/src/client/display-text/codeBlocks.ts +92 -0
  56. package/src/client/format.ts +3 -67
  57. package/src/client/http.ts +5 -5
  58. package/src/client/markdownExport.ts +32 -42
  59. package/src/client/pages/code/RepositoryTabs.tsx +2 -2
  60. package/src/client/pages/memory/MemoryDetails.tsx +6 -33
  61. package/src/client/pages/memory/MemoryPage.tsx +7 -92
  62. package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
  63. package/src/client/pages/memory/memoryDashboard.ts +2 -38
  64. package/src/client/pages/memory/memoryRecord.ts +1 -6
  65. package/src/client/pages/settings/AutoArchiveSettings.tsx +145 -0
  66. package/src/client/pages/settings/SettingsPage.tsx +2 -0
  67. package/src/client/review-desk/ConversationReviewItems.tsx +7 -9
  68. package/src/client/review-desk/queries.ts +2 -1
  69. package/src/client/types.ts +19 -33
  70. package/src/conversation-cache.ts +7 -9
  71. package/src/mock-reporting/fixtures.ts +126 -60
  72. package/src/mock-reporting/routes.ts +50 -1
  73. package/src/tailwind.css +30 -0
@@ -5,7 +5,6 @@ import {
5
5
  ActivityChartXAxis,
6
6
  ActivityChartYAxis,
7
7
  } from "../../components/charts/ActivityChart";
8
- import { ChartLegend } from "../../components/charts/ChartLegend";
9
8
  import {
10
9
  type TimeRangeBucketUnit,
11
10
  type TimeRangeDays,
@@ -14,16 +13,11 @@ import { Card } from "../../components/layout/Card";
14
13
  import { ChartContainer, ChartTooltip } from "../../components/ui/chart";
15
14
  import type { MemoryDay } from "./memoryDashboard";
16
15
 
17
- const series = [
18
- { color: "var(--color-cyan-300)", key: "personal", label: "Personal" },
19
- { color: "var(--color-emerald-300)", key: "public", label: "Public" },
20
- ] as const;
16
+ const chartConfig = {
17
+ count: { color: "var(--color-emerald-300)", label: "Memories" },
18
+ };
21
19
 
22
- const chartConfig = Object.fromEntries(
23
- series.map((item) => [item.key, { color: item.color, label: item.label }]),
24
- );
25
-
26
- /** Render viewer memory creation as a stacked personal/public timeline. */
20
+ /** Render memory creation as a timeline. */
27
21
  export function MemoryTimeline(props: {
28
22
  bucketUnit?: TimeRangeBucketUnit;
29
23
 
@@ -33,7 +27,7 @@ export function MemoryTimeline(props: {
33
27
  const bucketUnit = props.bucketUnit ?? "day";
34
28
 
35
29
  const days = props.days;
36
- const totals = days.map((day) => day.personal + day.public);
30
+ const totals = days.map((day) => day.count);
37
31
  const maximum = Math.max(1, ...totals);
38
32
  const hasMemories = totals.some((total) => total > 0);
39
33
 
@@ -45,15 +39,13 @@ export function MemoryTimeline(props: {
45
39
  </h2>
46
40
  <p className="mt-1 mb-0 font-sans text-xs leading-relaxed text-dashboard-text-muted">
47
41
  {bucketUnit === "hour"
48
- ? "Stacked personal + public memories created each hour."
42
+ ? "Memories created each hour."
49
43
  : bucketUnit === "6hour"
50
- ? "Stacked personal + public memories created each 6 hours."
51
- : "Stacked personal + public memories created each day."}
44
+ ? "Memories created each 6 hours."
45
+ : "Memories created each day."}
52
46
  </p>
53
47
  </div>
54
48
 
55
- <ChartLegend ariaLabel="Memory visibility legend" items={series} />
56
-
57
49
  <div className="relative mt-3 overflow-hidden">
58
50
  <ChartContainer
59
51
  className="aspect-auto h-50 w-full"
@@ -71,25 +63,17 @@ export function MemoryTimeline(props: {
71
63
  <ChartTooltip
72
64
  content={
73
65
  <ActivityChartTooltipContent
74
- rows={(day: MemoryDay) => [
75
- ["personal", day.personal],
76
- ["public", day.public],
77
- ["total", day.personal + day.public],
78
- ]}
66
+ rows={(day: MemoryDay) => [["memories", day.count]]}
79
67
  />
80
68
  }
81
69
  />
82
- {series.map((item) => (
83
- <Bar
84
- dataKey={item.key}
85
- fill={`var(--color-${item.key})`}
86
- fillOpacity={0.82}
87
- isAnimationActive={false}
88
- key={item.key}
89
- maxBarSize={13}
90
- stackId="visibility"
91
- />
92
- ))}
70
+ <Bar
71
+ dataKey="count"
72
+ fill="var(--color-count)"
73
+ fillOpacity={0.82}
74
+ isAnimationActive={false}
75
+ maxBarSize={13}
76
+ />
93
77
  </BarChart>
94
78
  </ChartContainer>
95
79
  {!hasMemories ? (
@@ -1,4 +1,4 @@
1
- import { keepPreviousData, useQuery } from "@tanstack/react-query";
1
+ import { useQuery } from "@tanstack/react-query";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
@@ -7,9 +7,8 @@ const memoryBucketSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2})?$/);
7
7
 
8
8
  const memoryDaySchema = z
9
9
  .object({
10
+ count: z.number().int().min(0),
10
11
  date: memoryBucketSchema,
11
- personal: z.number().int().min(0),
12
- public: z.number().int().min(0),
13
12
  })
14
13
  .strict();
15
14
 
@@ -38,10 +37,8 @@ export const memoryDashboardSchema = z
38
37
  embedded: z.number().int().min(0),
39
38
  explicit: z.number().int().min(0),
40
39
  knowledge: z.number().int().min(0),
41
- personal: z.number().int().min(0),
42
40
  preference: z.number().int().min(0),
43
41
  procedure: z.number().int().min(0),
44
- public: z.number().int().min(0),
45
42
  })
46
43
  .strict(),
47
44
  })
@@ -64,36 +61,3 @@ export function useMemoryDashboardData() {
64
61
  retry: false,
65
62
  });
66
63
  }
67
-
68
- const memoryCountsSchema = z
69
- .object({
70
- active: z.number().int().min(0),
71
- personal: z.number().int().min(0),
72
- public: z.number().int().min(0),
73
- })
74
- .strict();
75
-
76
- /**
77
- * Load viewer-scoped memory totals that match a search query. The query stays
78
- * idle without a search, because the dashboard summary owns unfiltered totals.
79
- */
80
- export function useMemorySearchCounts(searchQuery: string) {
81
- return useQuery({
82
- enabled: Boolean(searchQuery),
83
- placeholderData: keepPreviousData,
84
- queryFn: ({ signal }) =>
85
- fetchDashboardJson(
86
- memoryCountsSchema,
87
- `/api/plugins/memory/counts?${new URLSearchParams({ q: searchQuery })}`,
88
- signal,
89
- ),
90
- queryKey: [
91
- "dashboard",
92
- "plugin-user-page",
93
- "memory",
94
- "counts",
95
- searchQuery,
96
- ],
97
- retry: false,
98
- });
99
- }
@@ -14,7 +14,6 @@ const memoryRecordSchema = z
14
14
  observedAt: z.iso.datetime(),
15
15
  origin: z.enum(["automatic", "explicit", "other"]),
16
16
  sourcePlatform: z.enum(["local", "slack", "web"]),
17
- visibility: z.enum(["private", "public"]),
18
17
  })
19
18
  .strict();
20
19
 
@@ -43,10 +42,7 @@ export function memoryPageRecord(memory: MemoryRecord): PluginUserPageRecord {
43
42
  return {
44
43
  actions: [
45
44
  {
46
- confirmation:
47
- memory.visibility === "public"
48
- ? "Forget this memory for everyone?"
49
- : "Forget this memory?",
45
+ confirmation: "Forget this memory for everyone?",
50
46
  href: `/api/plugins/memory/memories/${encodeURIComponent(memory.id)}`,
51
47
  label: "Forget",
52
48
  method: "DELETE",
@@ -58,7 +54,6 @@ export function memoryPageRecord(memory: MemoryRecord): PluginUserPageRecord {
58
54
  { label: "Type", value: titleCase(memory.kind) },
59
55
  { label: "Learned", value: learned },
60
56
  { label: "Source", value: titleCase(memory.sourcePlatform) },
61
- { label: "Visibility", value: titleCase(memory.visibility) },
62
57
  { label: "Remembered", value: formattedDate(memory.createdAt) },
63
58
  { label: "Observed", value: formattedDate(memory.observedAt) },
64
59
  {
@@ -0,0 +1,145 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+
3
+ import {
4
+ AUTO_ARCHIVE_MAX_GRACE_HOURS,
5
+ dashboardIdentitySchema,
6
+ type DashboardIdentity,
7
+ } from "../../../api/schema";
8
+ import { Button } from "../../components/ui/button";
9
+ import { Checkbox } from "../../components/ui/checkbox";
10
+ import { Field, FieldDescription, FieldLabel } from "../../components/ui/field";
11
+ import { NativeSelect } from "../../components/ui/native-select";
12
+ import { InlineError } from "../../components/InlineError";
13
+ import { Card } from "../../components/layout/Card";
14
+ import { useArchiveDoneConversations } from "../../conversations/autoArchiveQueries";
15
+ import { patch } from "../../http";
16
+ import type { DashboardCoreData } from "../../types";
17
+
18
+ const dashboardCoreQueryKey = ["dashboard", "core"] as const;
19
+
20
+ /** Grace periods offered in Settings, in hours. */
21
+ const AUTO_ARCHIVE_GRACE_OPTIONS = [1, 6, 24, 72, AUTO_ARCHIVE_MAX_GRACE_HOURS];
22
+
23
+ function formatArchivedCount(count: number): string {
24
+ if (count === 0) return "No done conversations to archive.";
25
+ return count === 1
26
+ ? "Archived 1 conversation."
27
+ : `Archived ${count} conversations.`;
28
+ }
29
+
30
+ function formatGraceHours(hours: number): string {
31
+ if (hours % 24 !== 0) return hours === 1 ? "1 hour" : `${hours} hours`;
32
+ const days = hours / 24;
33
+ if (days === 7) return "1 week";
34
+ return days === 1 ? "1 day" : `${days} days`;
35
+ }
36
+
37
+ /**
38
+ * Let the user turn auto-archive on for their conversations and pick how long
39
+ * finished work must stay quiet before Junior archives it.
40
+ */
41
+ export function AutoArchiveSettings({
42
+ identity,
43
+ }: {
44
+ identity: DashboardIdentity;
45
+ }) {
46
+ const queryClient = useQueryClient();
47
+ const update = useMutation({
48
+ mutationFn: (
49
+ settings: Pick<
50
+ DashboardIdentity,
51
+ "autoArchiveDefault" | "autoArchiveGraceHours"
52
+ >,
53
+ ) => patch(dashboardIdentitySchema, "/api/me", settings),
54
+ onSuccess: (updated) => {
55
+ queryClient.setQueryData<DashboardCoreData>(
56
+ dashboardCoreQueryKey,
57
+ (current) => (current ? { ...current, me: updated } : current),
58
+ );
59
+ },
60
+ });
61
+ const archiveDone = useArchiveDoneConversations();
62
+ const enabled = identity.autoArchiveDefault ?? false;
63
+ const graceHours = identity.autoArchiveGraceHours ?? 24;
64
+ // Keep a saved value that is not a preset, so the select shows the truth.
65
+ const graceOptions = AUTO_ARCHIVE_GRACE_OPTIONS.includes(graceHours)
66
+ ? AUTO_ARCHIVE_GRACE_OPTIONS
67
+ : [...AUTO_ARCHIVE_GRACE_OPTIONS, graceHours].sort((a, b) => a - b);
68
+
69
+ return (
70
+ <Card className="mt-6 mb-0" padding="md" variant="raised">
71
+ <h2 className="m-0 text-lg font-bold">Conversations</h2>
72
+ <label className="mt-4 flex cursor-pointer items-start gap-3">
73
+ <Checkbox
74
+ checked={enabled}
75
+ className="mt-0.5"
76
+ disabled={update.isPending}
77
+ onCheckedChange={(checked) =>
78
+ update.mutate({ autoArchiveDefault: checked })
79
+ }
80
+ />
81
+ <span className="grid gap-0.5">
82
+ <span className="text-sm font-medium">
83
+ Auto-archive conversations when done
84
+ </span>
85
+ <span className="text-xs text-dashboard-text-muted">
86
+ Archive a conversation for you when all of its linked work, such as
87
+ pull requests, is merged or closed and nobody replies during the
88
+ wait time. You can change this for each conversation in its details.
89
+ </span>
90
+ </span>
91
+ </label>
92
+ <Field className="mt-5 max-w-xs">
93
+ <FieldLabel htmlFor="auto-archive-grace">Wait time</FieldLabel>
94
+ <NativeSelect
95
+ aria-describedby="auto-archive-grace-help"
96
+ disabled={update.isPending}
97
+ id="auto-archive-grace"
98
+ onChange={(event) =>
99
+ update.mutate({ autoArchiveGraceHours: Number(event.target.value) })
100
+ }
101
+ value={graceHours}
102
+ >
103
+ {graceOptions.map((hours) => (
104
+ <option key={hours} value={hours}>
105
+ {formatGraceHours(hours)}
106
+ </option>
107
+ ))}
108
+ </NativeSelect>
109
+ <FieldDescription id="auto-archive-grace-help">
110
+ How long finished work and the conversation stay quiet before archive.
111
+ </FieldDescription>
112
+ </Field>
113
+ {update.isError ? (
114
+ <InlineError className="mt-3">
115
+ Could not save this setting. Try again.
116
+ </InlineError>
117
+ ) : null}
118
+ <div className="mt-5 grid gap-1.5">
119
+ <div className="flex flex-wrap items-center gap-3">
120
+ <Button
121
+ disabled={archiveDone.isPending}
122
+ onClick={() => archiveDone.mutate()}
123
+ type="button"
124
+ >
125
+ {archiveDone.isPending ? "Archiving…" : "Archive done now"}
126
+ </Button>
127
+ {archiveDone.isSuccess ? (
128
+ <p className="m-0 text-sm text-dashboard-text-muted" role="status">
129
+ {formatArchivedCount(archiveDone.data.archived)}
130
+ </p>
131
+ ) : null}
132
+ {archiveDone.isError ? (
133
+ <InlineError>
134
+ Could not archive conversations. Try again.
135
+ </InlineError>
136
+ ) : null}
137
+ </div>
138
+ <p className="m-0 text-xs text-dashboard-text-muted">
139
+ Archive every done conversation once, without the wait time. Skips
140
+ conversations where you turned auto-archive off.
141
+ </p>
142
+ </div>
143
+ </Card>
144
+ );
145
+ }
@@ -24,6 +24,7 @@ import { InlineError } from "../../components/InlineError";
24
24
  import { Card } from "../../components/layout/Card";
25
25
  import { DashboardApiError, fetchDashboardJson, patch, put } from "../../http";
26
26
  import type { DashboardCoreData } from "../../types";
27
+ import { AutoArchiveSettings } from "./AutoArchiveSettings";
27
28
  import { SettingsPageLayout } from "./SettingsPageLayout";
28
29
 
29
30
  const dashboardCoreQueryKey = ["dashboard", "core"] as const;
@@ -108,6 +109,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
108
109
  </form>
109
110
  <ConversationStartSetting />
110
111
  <ComposerAutocorrectSetting />
112
+ <AutoArchiveSettings identity={identity} />
111
113
  <CustomizationForm />
112
114
  </SettingsPageLayout>
113
115
  );
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
3
3
  import type { ReviewItemPayload } from "@sentry/junior/api/schema";
4
4
 
5
5
  import { LoadMorePagination } from "../components/Pagination";
6
+ import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
6
7
  import {
7
8
  DetailsCard,
8
9
  DetailsList,
@@ -74,9 +75,9 @@ function ReviewItemRow(props: {
74
75
  if (props.defaultExpanded) setExpanded(true);
75
76
  }, [props.defaultExpanded]);
76
77
  return (
77
- <>
78
+ <Collapsible className="min-w-0" onOpenChange={setExpanded} open={expanded}>
78
79
  <DetailsRow
79
- expanded={expanded}
80
+ disclosure
80
81
  icon={<Inbox aria-hidden="true" className="size-3.5" />}
81
82
  meta={
82
83
  <span className="grid size-5 place-items-center">
@@ -86,14 +87,11 @@ function ReviewItemRow(props: {
86
87
  />
87
88
  </span>
88
89
  }
89
- onToggle={() => setExpanded((value) => !value)}
90
90
  title={props.item.title}
91
91
  />
92
- {expanded ? (
93
- <div className="px-1.5 pb-1">
94
- <ReviewItemCard item={props.item} />
95
- </div>
96
- ) : null}
97
- </>
92
+ <CollapsibleContent className="px-1.5 pb-1">
93
+ <ReviewItemCard item={props.item} />
94
+ </CollapsibleContent>
95
+ </Collapsible>
98
96
  );
99
97
  }
@@ -20,7 +20,8 @@ import {
20
20
  import { conversationDetailTitle } from "../format";
21
21
  import { fetchDashboardJson, post } from "../http";
22
22
 
23
- const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
23
+ /** Prefix of every Review item read, for refreshes after a change. */
24
+ export const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
24
25
  const COUNT_REFRESH_MS = 60_000;
25
26
 
26
27
  /**
@@ -56,32 +56,22 @@ export type TranscriptViewSubagentPart = {
56
56
  export type TranscriptViewContextEventPart = {
57
57
  /** People mentioned in the summary, resolved by the API. */
58
58
  mentions?: ActorIdentity[];
59
- event:
60
- | {
61
- createdAt: string;
62
- type: "compaction";
63
- modelId?: string;
64
- modelProfile?: string;
65
- summary?: string;
66
- details?: {
67
- reason: "capacity";
68
- estimatedInputTokens: number;
69
- replacementInputTokens?: number;
70
- triggerTokens: number;
71
- inputLimitTokens: number;
72
- inputMessageCount: number;
73
- retainedMessageCount: number;
74
- summaryChars: number;
75
- };
76
- }
77
- | {
78
- createdAt: string;
79
- modelId: string;
80
- modelProfile: string;
81
- reasoningLevel?: string;
82
- summary?: string;
83
- type: "handoff";
84
- };
59
+ event: {
60
+ createdAt: string;
61
+ type: "compaction";
62
+ modelId?: string;
63
+ summary?: string;
64
+ details?: {
65
+ reason: "capacity";
66
+ estimatedInputTokens: number;
67
+ replacementInputTokens?: number;
68
+ triggerTokens: number;
69
+ inputLimitTokens: number;
70
+ inputMessageCount: number;
71
+ retainedMessageCount: number;
72
+ summaryChars: number;
73
+ };
74
+ };
85
75
  type: "context_event";
86
76
  };
87
77
 
@@ -138,6 +128,8 @@ export type TranscriptViewMessage = {
138
128
  events?: ConversationReportMessageEvent[];
139
129
  /** Whether the source message addressed Junior directly. */
140
130
  explicitMention?: boolean;
131
+ /** Quiet message: stored, but not delivered to the Location. */
132
+ quiet?: true;
141
133
  /** Person changes to Review items that this message records. */
142
134
  reviewItemChanges?: ReviewItemChangeRecord[];
143
135
  /** People mentioned in the event text, resolved by the API. */
@@ -148,13 +140,6 @@ export type TranscriptViewMessage = {
148
140
  messageId?: string;
149
141
  /** True while the message is accepted in the mailbox but not yet in history. */
150
142
  pending?: boolean;
151
- route?: {
152
- confidence?: number;
153
- modelId: string;
154
- modelProfile: string;
155
- reasoningLevel: string;
156
- source: "configured" | "inherited" | "router";
157
- };
158
143
  eventId?: string;
159
144
  failureCode?: ConversationTurnFailureCode;
160
145
  failureReason?: ConversationTurnFailureReason;
@@ -176,6 +161,7 @@ export type Conversation = {
176
161
  annotations?: ConversationSummaryReport["annotations"];
177
162
  sidebarAnnotations?: ConversationSummaryReport["sidebarAnnotations"];
178
163
  archivedAt?: string | null;
164
+ pinnedAt?: string | null;
179
165
  auxiliaryCosts?: ConversationSummaryReport["auxiliaryCosts"];
180
166
  channel?: string;
181
167
  channelName?: string;
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { CONVERSATION_DETAIL_VERSION_HEADER } from "@sentry/junior/api/schema";
2
3
  import type { MiddlewareHandler } from "hono";
3
4
 
4
- /** Revalidate dashboard detail reads only after the route checks viewer access. */
5
+ /** Revalidate mock detail reads on content, after the route checks viewer access. */
5
6
  export const revalidateConversation: MiddlewareHandler = async (
6
7
  context,
7
8
  next,
@@ -15,15 +16,12 @@ export const revalidateConversation: MiddlewareHandler = async (
15
16
  // generatedAt is the read time, not a change to the Conversation. The mailbox
16
17
  // has its own live watermark and is never handled by this middleware.
17
18
  const { generatedAt: _generatedAt, ...content } = report;
18
- const etag = `W/"${createHash("sha256").update(JSON.stringify(content)).digest("hex")}"`;
19
+ const version = createHash("sha256")
20
+ .update(JSON.stringify(content))
21
+ .digest("hex");
19
22
  context.header("cache-control", "private, no-store");
20
- context.header("etag", etag);
21
- const validators = context.req.header("if-none-match")?.split(",");
22
- if (
23
- validators?.some(
24
- (value) => value.trim().replace(/^W\//, "") === etag.slice(2),
25
- )
26
- ) {
23
+ context.header(CONVERSATION_DETAIL_VERSION_HEADER, version);
24
+ if (context.req.header(CONVERSATION_DETAIL_VERSION_HEADER) === version) {
27
25
  context.res = new Response(null, {
28
26
  status: 304,
29
27
  headers: context.res.headers,