@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
@@ -7,18 +7,22 @@ export type ConversationSection = {
7
7
  };
8
8
 
9
9
  const DAY_MS = 24 * 60 * 60 * 1000;
10
- // Sorts before every time section.
10
+ // Pinned sorts first, then Needs attention, then every time section.
11
+ const PINNED_SECTION = { key: "pinned", label: "Pinned", order: -2 };
11
12
  const ATTENTION_SECTION = {
12
13
  key: "attention",
13
14
  label: "Needs attention",
14
- order: Number.NEGATIVE_INFINITY,
15
+ order: -1,
15
16
  };
16
17
 
17
18
  /**
18
- * Group conversations into Needs attention, then by last activity: Today,
19
- * Yesterday, Previous 7 days, Previous 30 days, then one section per month.
19
+ * Group conversations into Pinned, then Needs attention, then by last
20
+ * activity: Today, Yesterday, Previous 7 days, Previous 30 days, then one
21
+ * section per month.
20
22
  *
21
- * Conversations in `attentionIds` go to Needs attention, newest first.
23
+ * A pinned conversation shows only in Pinned, even when it needs attention.
24
+ * Conversations in `attentionIds` go to Needs attention. Each section lists
25
+ * the newest first.
22
26
  */
23
27
  export function buildConversationSections(
24
28
  conversations: Conversation[],
@@ -35,14 +39,16 @@ export function buildConversationSections(
35
39
  );
36
40
 
37
41
  for (const conversation of sorted) {
38
- const section = options.attentionIds?.has(conversation.id)
39
- ? ATTENTION_SECTION
40
- : conversationSection(
41
- activityTime(conversation),
42
- nowDay,
43
- options.nowMs,
44
- options.timeZone,
45
- );
42
+ const section = conversation.pinnedAt
43
+ ? PINNED_SECTION
44
+ : options.attentionIds?.has(conversation.id)
45
+ ? ATTENTION_SECTION
46
+ : conversationSection(
47
+ activityTime(conversation),
48
+ nowDay,
49
+ options.nowMs,
50
+ options.timeZone,
51
+ );
46
52
  const existing = sections.get(section.key);
47
53
  if (existing) {
48
54
  existing.conversations.push(conversation);
@@ -65,16 +71,16 @@ export function attentionConversationIds(
65
71
  }
66
72
 
67
73
  /**
68
- * Sort attention conversations to the front. The sort is stable, so each group
69
- * keeps the feed order. Paginated lists call this before paging, so page 1
70
- * holds every attention conversation.
74
+ * Sort pinned, then attention conversations to the front. The sort is stable,
75
+ * so each group keeps the feed order. Paginated lists call this before paging,
76
+ * so page 1 holds every pinned and attention conversation.
71
77
  */
72
- export function attentionFirst(
78
+ export function pinnedAndAttentionFirst(
73
79
  conversations: Conversation[],
74
80
  attentionIds: ReadonlySet<string>,
75
81
  ): Conversation[] {
76
82
  const rank = (conversation: Conversation) =>
77
- attentionIds.has(conversation.id) ? 0 : 1;
83
+ conversation.pinnedAt ? 0 : attentionIds.has(conversation.id) ? 1 : 2;
78
84
  return [...conversations].sort((left, right) => rank(left) - rank(right));
79
85
  }
80
86
 
@@ -0,0 +1,61 @@
1
+ import { useState, useSyncExternalStore } from "react";
2
+
3
+ const OPEN_KEY = "junior:conversation:sidebar-open";
4
+ const WIDTH_KEY = "junior:conversation:sidebar-width";
5
+ const WIDE_QUERY = "(min-width: 768px)";
6
+
7
+ /** Pixel limits for the conversation list beside a selected conversation. */
8
+ export const SIDEBAR_WIDTH = { default: 320, max: 480, min: 240 } as const;
9
+
10
+ /** Remember whether the conversation list is open in this browser. */
11
+ export function useConversationSidebarOpen() {
12
+ const [open, setOpen] = useState(() => readStorage(OPEN_KEY) !== "false");
13
+
14
+ function updateOpen(next: boolean) {
15
+ setOpen(next);
16
+ writeStorage(OPEN_KEY, String(next));
17
+ }
18
+
19
+ return [open, updateOpen] as const;
20
+ }
21
+
22
+ /** Read the conversation list width saved in this browser. */
23
+ export function readSidebarWidth(): number {
24
+ const saved = Number(readStorage(WIDTH_KEY));
25
+ if (!Number.isFinite(saved) || saved <= 0) return SIDEBAR_WIDTH.default;
26
+ return Math.min(SIDEBAR_WIDTH.max, Math.max(SIDEBAR_WIDTH.min, saved));
27
+ }
28
+
29
+ /** Save the conversation list width without a React render per drag step. */
30
+ export function saveSidebarWidth(width: number) {
31
+ writeStorage(WIDTH_KEY, String(Math.round(width)));
32
+ }
33
+
34
+ /** Report whether the viewport has room for the list beside the transcript. */
35
+ export function useSidebarFits(): boolean {
36
+ return useSyncExternalStore(
37
+ (onChange) => {
38
+ const media = window.matchMedia(WIDE_QUERY);
39
+ media.addEventListener("change", onChange);
40
+ return () => media.removeEventListener("change", onChange);
41
+ },
42
+ () => window.matchMedia(WIDE_QUERY).matches,
43
+ () => false,
44
+ );
45
+ }
46
+
47
+ function readStorage(key: string): string | null {
48
+ try {
49
+ return window.localStorage.getItem(key);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function writeStorage(key: string, value: string) {
56
+ try {
57
+ window.localStorage.setItem(key, value);
58
+ } catch {
59
+ // The layout still works for this page when browser storage is unavailable.
60
+ }
61
+ }
@@ -10,31 +10,12 @@ import {
10
10
  summarizeCost,
11
11
  } from "../format";
12
12
 
13
- /** Read recorded event configuration, including reports from before model context was added. */
14
- export function eventLogModel(
15
- event: ConversationReportEvent,
16
- ): ConversationReportEvent["model"] {
17
- if (event.model) return event.model;
18
- const data = event.data;
19
- if (data.type === "turn_routed" || data.type === "handoff") {
20
- return {
21
- modelId: data.modelId,
22
- modelProfile: data.modelProfile,
23
- reasoningLevel: data.reasoningLevel,
24
- };
25
- }
26
- return undefined;
27
- }
28
-
29
- /** Keep call costs distinct from router cost and never repeat usage on tool results. */
13
+ /** Show call usage once and never repeat it on tool results. */
30
14
  export function eventLogUsage(event: ConversationReportEvent): string {
31
15
  const usage = event.modelCall?.usage;
32
16
  const cost = summarizeCost(usage);
33
17
  const fields = [
34
18
  cost ? `${formatCostBreakdown(cost)} call` : undefined,
35
- event.data.type === "turn_routed" && event.data.costUsd !== undefined
36
- ? `${formatCostBreakdown({ total: event.data.costUsd })} router`
37
- : undefined,
38
19
  ...(
39
20
  [
40
21
  ["inputTokens", "in"],
@@ -77,8 +58,6 @@ export function eventLogSummary(data: ConversationReportEventData): string {
77
58
  return data.state === "failed"
78
59
  ? `${data.state} · ${data.failureCode}${data.failureReason ? ` · ${data.failureReason}` : ""}`
79
60
  : `${data.state} · ${data.turnId}`;
80
- case "turn_routed":
81
- return `Route selected · ${data.source}${data.confidence !== undefined ? ` · ${Math.round(data.confidence * 100)}% confidence` : ""}`;
82
61
  case "turn_context":
83
62
  return `${data.pluginName} · ${data.kind} · v${data.version}`;
84
63
  case "guardian_action_reviewed":
@@ -91,8 +70,6 @@ export function eventLogSummary(data: ConversationReportEventData): string {
91
70
  .join(", ");
92
71
  case "compaction":
93
72
  return data.summary ?? "Agent history compacted";
94
- case "handoff":
95
- return "Handoff · New configuration applied";
96
73
  case "subagent":
97
74
  return `${data.agentName ?? data.subagentKind} · ${data.status} · ${data.childConversationId}`;
98
75
  }
@@ -132,16 +109,10 @@ export function eventLogRows(
132
109
  events: readonly ConversationReportEvent[],
133
110
  ): EventLogRow[] {
134
111
  return events.map((event) => {
135
- const model = eventLogModel(event);
136
112
  return {
137
113
  event,
138
114
  summary: eventLogSummary(event.data).replace(/\s+/g, " ").slice(0, 300),
139
- metadata: [
140
- model?.modelId,
141
- model?.modelProfile,
142
- model?.reasoningLevel && `${model.reasoningLevel} reasoning`,
143
- eventLogUsage(event),
144
- ]
115
+ metadata: [event.model?.modelId, eventLogUsage(event)]
145
116
  .filter(Boolean)
146
117
  .join(" · "),
147
118
  };
@@ -51,9 +51,6 @@ function specialToolIds(events: ConversationReportEvent[]): Set<string> {
51
51
  const ids = new Set<string>();
52
52
  for (const event of events) {
53
53
  const data = event.data;
54
- if (data.type === "handoff" && data.triggeringToolCallId) {
55
- ids.add(data.triggeringToolCallId);
56
- }
57
54
  if (data.type === "subagent" && data.parentToolCallId) {
58
55
  ids.add(data.parentToolCallId);
59
56
  }
@@ -243,6 +240,7 @@ export function transcriptMessagesFromEvents(
243
240
  ...(data.explicitMention !== undefined
244
241
  ? { explicitMention: data.explicitMention }
245
242
  : undefined),
243
+ ...(data.quiet ? { quiet: data.quiet } : undefined),
246
244
  ...(data.reviewItemChanges
247
245
  ? { reviewItemChanges: data.reviewItemChanges }
248
246
  : undefined),
@@ -308,22 +306,6 @@ export function transcriptMessagesFromEvents(
308
306
  continue;
309
307
  }
310
308
 
311
- if (data.type === "turn_routed") {
312
- const message = latestUserMessageByTurn.get(data.turnId);
313
- if (message) {
314
- message.route = {
315
- modelProfile: data.modelProfile,
316
- modelId: data.modelId,
317
- reasoningLevel: data.reasoningLevel,
318
- ...(data.confidence !== undefined
319
- ? { confidence: data.confidence }
320
- : undefined),
321
- source: data.source,
322
- };
323
- }
324
- continue;
325
- }
326
-
327
309
  if (data.type === "turn_context") {
328
310
  const message = latestUserMessageByTurn.get(data.turnId);
329
311
  if (message) {
@@ -437,34 +419,19 @@ export function transcriptMessagesFromEvents(
437
419
  continue;
438
420
  }
439
421
 
440
- if (data.type === "compaction" || data.type === "handoff") {
422
+ if (data.type === "compaction") {
441
423
  messages.push(
442
424
  eventMessage(event, "system", [
443
425
  {
444
426
  type: "context_event",
445
427
  ...(event.mentions ? { mentions: event.mentions } : undefined),
446
- event:
447
- data.type === "handoff"
448
- ? {
449
- type: data.type,
450
- createdAt: event.createdAt,
451
- modelId: data.modelId,
452
- modelProfile: data.modelProfile,
453
- ...(data.summary ? { summary: data.summary } : undefined),
454
- ...(data.reasoningLevel
455
- ? { reasoningLevel: data.reasoningLevel }
456
- : undefined),
457
- }
458
- : {
459
- type: data.type,
460
- createdAt: event.createdAt,
461
- ...(data.modelId ? { modelId: data.modelId } : undefined),
462
- ...(data.modelProfile
463
- ? { modelProfile: data.modelProfile }
464
- : undefined),
465
- ...(data.summary ? { summary: data.summary } : undefined),
466
- ...(data.details ? { details: data.details } : undefined),
467
- },
428
+ event: {
429
+ type: data.type,
430
+ createdAt: event.createdAt,
431
+ ...(data.modelId ? { modelId: data.modelId } : undefined),
432
+ ...(data.summary ? { summary: data.summary } : undefined),
433
+ ...(data.details ? { details: data.details } : undefined),
434
+ },
468
435
  },
469
436
  ]),
470
437
  );