@sentry/junior-dashboard 0.277.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 (45) hide show
  1. package/dist/app.js +30 -41
  2. package/dist/client.js +90 -90
  3. package/dist/index.js +30 -41
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +1 -1
  7. package/src/client/App.tsx +9 -4
  8. package/src/client/components/DetailsPanel.tsx +1 -1
  9. package/src/client/components/ui/resizable.tsx +50 -0
  10. package/src/client/conversations/AutoArchiveToggle.tsx +50 -14
  11. package/src/client/conversations/ConversationHeader.tsx +6 -2
  12. package/src/client/conversations/ConversationHeaderActions.tsx +4 -0
  13. package/src/client/conversations/ConversationMeta.tsx +2 -7
  14. package/src/client/conversations/ConversationPage.tsx +155 -72
  15. package/src/client/conversations/ConversationSidebar.tsx +2 -1
  16. package/src/client/conversations/ConversationTranscript.tsx +11 -8
  17. package/src/client/conversations/ConversationWorkspace.tsx +129 -41
  18. package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
  19. package/src/client/conversations/EventDetails.tsx +2 -62
  20. package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
  21. package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
  22. package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
  23. package/src/client/conversations/TranscriptView.tsx +89 -20
  24. package/src/client/conversations/conversationSidebarLayout.ts +61 -0
  25. package/src/client/conversations/eventLog.ts +2 -31
  26. package/src/client/conversations/eventTranscript.ts +9 -42
  27. package/src/client/conversations/queries.ts +76 -15
  28. package/src/client/conversations/transcript.ts +46 -16
  29. package/src/client/conversations/transcriptBottomPinning.ts +40 -24
  30. package/src/client/conversations/transcriptReadPosition.ts +31 -7
  31. package/src/client/conversations/transcriptRenderModel.ts +1 -14
  32. package/src/client/conversations/transcriptSearch.tsx +3 -11
  33. package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
  34. package/src/client/http.ts +5 -5
  35. package/src/client/markdownExport.ts +32 -42
  36. package/src/client/pages/memory/MemoryDetails.tsx +6 -33
  37. package/src/client/pages/memory/MemoryPage.tsx +7 -92
  38. package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
  39. package/src/client/pages/memory/memoryDashboard.ts +2 -38
  40. package/src/client/pages/memory/memoryRecord.ts +1 -6
  41. package/src/client/types.ts +18 -33
  42. package/src/conversation-cache.ts +6 -8
  43. package/src/mock-reporting/fixtures.ts +23 -33
  44. package/src/mock-reporting/routes.ts +3 -1
  45. 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
  {
@@ -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;
@@ -1,4 +1,5 @@
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
5
  /** Revalidate mock detail reads on content, after the route checks viewer access. */
@@ -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,
@@ -476,16 +476,27 @@ function activeConversation(nowMs: number): ConversationDetailReport {
476
476
  ],
477
477
  },
478
478
  }),
479
- reportEvent(3, iso(Date.parse(startedAt), 9_000), {
480
- type: "turn_routed",
481
- turnId: "active-turn",
482
- modelProfile: "handoff",
483
- modelId: "openai/gpt-5.6-sol",
484
- reasoningLevel: "high",
485
- confidence: 0.93,
486
- source: "router",
487
- costUsd: 0.0012,
488
- }),
479
+ reportEvent(
480
+ 3,
481
+ iso(Date.parse(startedAt), 9_000),
482
+ {
483
+ type: "assistant_message",
484
+ parts: [
485
+ {
486
+ type: "reasoning",
487
+ text: "Compare the latest deploy with the previous baseline.",
488
+ },
489
+ ],
490
+ },
491
+ {
492
+ model: { modelId: "openai/gpt-5.6-sol" },
493
+ modelCall: {
494
+ provider: "vercel-ai-gateway",
495
+ api: "openai-responses",
496
+ stopReason: "toolUse",
497
+ },
498
+ },
499
+ ),
489
500
  reportEvent(
490
501
  4,
491
502
  iso(Date.parse(startedAt), 10_000),
@@ -500,11 +511,7 @@ function activeConversation(nowMs: number): ConversationDetailReport {
500
511
  ],
501
512
  },
502
513
  {
503
- model: {
504
- modelId: "openai/gpt-5.6-sol",
505
- modelProfile: "handoff",
506
- reasoningLevel: "high",
507
- },
514
+ model: { modelId: "openai/gpt-5.6-sol" },
508
515
  modelCall: {
509
516
  provider: "vercel-ai-gateway",
510
517
  api: "openai-responses",
@@ -552,11 +559,7 @@ function activeConversation(nowMs: number): ConversationDetailReport {
552
559
  ],
553
560
  },
554
561
  {
555
- model: {
556
- modelId: "openai/gpt-5.6-sol",
557
- modelProfile: "handoff",
558
- reasoningLevel: "high",
559
- },
562
+ model: { modelId: "openai/gpt-5.6-sol" },
560
563
  },
561
564
  ),
562
565
  // Mixed markdown keeps font/legibility QA honest for long assistant replies.
@@ -913,7 +916,6 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
913
916
  }),
914
917
  reportEvent(11, iso(Date.parse(startedAt), 56_000), {
915
918
  type: "compaction",
916
- modelProfile: "standard",
917
919
  modelId: "openai/gpt-5.4",
918
920
  details: {
919
921
  reason: "capacity",
@@ -1119,7 +1121,6 @@ function longConversation(nowMs: number): ConversationDetailReport {
1119
1121
  events.push(
1120
1122
  reportEvent(nextSeq, iso(Date.parse(startedAt), 53_000), {
1121
1123
  type: "compaction",
1122
- modelProfile: "standard",
1123
1124
  modelId: "openai/gpt-5.4",
1124
1125
  summary:
1125
1126
  "The release is complete. Preserve the published version and continue by checking the update pull request.",
@@ -1136,17 +1137,6 @@ function longConversation(nowMs: number): ConversationDetailReport {
1136
1137
  }),
1137
1138
  );
1138
1139
  nextSeq += 1;
1139
- events.push(
1140
- reportEvent(nextSeq, iso(Date.parse(startedAt), 90_000), {
1141
- type: "handoff",
1142
- modelProfile: "fast",
1143
- modelId: "openai/gpt-5-mini",
1144
- reasoningLevel: "medium",
1145
- summary:
1146
- "Investigate the remaining deployment checks and report any actionable failure.",
1147
- }),
1148
- );
1149
- nextSeq += 1;
1150
1140
  events.push(
1151
1141
  reportEvent(nextSeq, iso(Date.parse(startedAt), 166_000), {
1152
1142
  type: "message",
@@ -197,7 +197,9 @@ export function createMockReportingApi(): Hono<{
197
197
  if (!params.success) {
198
198
  return errorResponse("Invalid route parameters.", 400);
199
199
  }
200
- if (!query.success) {
200
+ // Mock details fit one page and have no unread cursor, so mock history
201
+ // only pages backward.
202
+ if (!query.success || !("before" in query.data)) {
201
203
  return errorResponse("Invalid query parameters.", 400);
202
204
  }
203
205
  if (!readMockConversationDetail(params.data.conversationId)) {
package/src/tailwind.css CHANGED
@@ -327,6 +327,36 @@
327
327
  }
328
328
  }
329
329
 
330
+ @utility thin-scrollbar {
331
+ --thin-scrollbar-thumb: color-mix(
332
+ in srgb,
333
+ var(--color-dashboard-scrollbar-thumb) 80%,
334
+ transparent
335
+ );
336
+
337
+ &::-webkit-scrollbar {
338
+ width: 6px;
339
+ }
340
+
341
+ &::-webkit-scrollbar-track {
342
+ background: transparent;
343
+ }
344
+
345
+ &::-webkit-scrollbar-thumb {
346
+ border-inline: 1px solid transparent;
347
+ background: var(--thin-scrollbar-thumb) padding-box;
348
+ }
349
+
350
+ &::-webkit-scrollbar-thumb:hover {
351
+ background-color: var(--color-dashboard-scrollbar-thumb-hover);
352
+ }
353
+
354
+ @supports not selector(::-webkit-scrollbar-thumb) {
355
+ scrollbar-color: var(--thin-scrollbar-thumb) transparent;
356
+ scrollbar-width: thin;
357
+ }
358
+ }
359
+
330
360
  @source "./app.ts";
331
361
  @source "./dashboardLoader.ts";
332
362
  @source "./client.tsx";