@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
@@ -4,6 +4,8 @@ import {
4
4
  ArchiveRestore,
5
5
  Info,
6
6
  MessagesSquare,
7
+ Pin,
8
+ PinOff,
7
9
  ScrollText,
8
10
  Search,
9
11
  X,
@@ -28,12 +30,20 @@ export type ConversationArchiveAction = {
28
30
  pending: boolean;
29
31
  };
30
32
 
33
+ export type ConversationPinAction = {
34
+ disabled: boolean;
35
+ error: boolean;
36
+ onClick(): void;
37
+ pinned: boolean;
38
+ };
39
+
31
40
  /**
32
41
  * Show conversation actions in the `⋯` menu. On desktop the title bar shows
33
42
  * search, share, and details, so the menu leaves them out.
34
43
  */
35
44
  export function ConversationHeaderActions(props: {
36
45
  archive: ConversationArchiveAction;
46
+ pin: ConversationPinAction;
37
47
  conversationId: string;
38
48
  copyAction?: ReactNode;
39
49
  detailsOpen: boolean;
@@ -83,6 +93,17 @@ export function ConversationHeaderActions(props: {
83
93
  />
84
94
  ) : null}
85
95
  {props.copyAction}
96
+ <MenuActionButton
97
+ disabled={props.pin.disabled}
98
+ label={props.pin.pinned ? "Unpin" : "Pin"}
99
+ onClick={props.pin.onClick}
100
+ >
101
+ {props.pin.pinned ? (
102
+ <PinOff aria-hidden="true" size={16} strokeWidth={2} />
103
+ ) : (
104
+ <Pin aria-hidden="true" size={16} strokeWidth={2} />
105
+ )}
106
+ </MenuActionButton>
86
107
  <ArchiveConversationButton {...props.archive} />
87
108
  {props.sentryConversationUrl ? (
88
109
  <a
@@ -158,6 +179,8 @@ function ViewModeItem(props: {
158
179
  /** Shared icon control used by conversation and subagent transcript headers. */
159
180
  export function HeaderIconButton(props: {
160
181
  children: ReactNode;
182
+ controls?: string;
183
+ expanded?: boolean;
161
184
  ref?: Ref<HTMLButtonElement>;
162
185
  label: string;
163
186
  onClick(): void;
@@ -171,6 +194,8 @@ export function HeaderIconButton(props: {
171
194
  render={
172
195
  <Button
173
196
  ref={props.ref}
197
+ aria-controls={props.controls}
198
+ aria-expanded={props.expanded}
174
199
  aria-label={props.label}
175
200
  aria-pressed={props.pressed}
176
201
  className={cn(
@@ -211,6 +236,7 @@ export function TranscriptSearchToggle(props: {
211
236
 
212
237
  function MenuActionButton(props: {
213
238
  children: ReactNode;
239
+ disabled?: boolean;
214
240
  label: string;
215
241
  onClick(): void;
216
242
  pressed?: boolean;
@@ -220,6 +246,7 @@ function MenuActionButton(props: {
220
246
  aria-label={props.label}
221
247
  aria-pressed={props.pressed}
222
248
  className={props.pressed ? "bg-dashboard-fill-strong" : undefined}
249
+ disabled={props.disabled}
223
250
  onClick={props.onClick}
224
251
  size="menu"
225
252
  >
@@ -20,7 +20,7 @@ import { ConversationSidebarAnnotations } from "./ConversationMeta";
20
20
  import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
21
21
  import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
22
22
  import { conversationPath } from "./conversationRoutes";
23
- import { useArchiveConversation } from "./queries";
23
+ import { useArchiveConversation } from "./conversationArchive";
24
24
  import { DisplayText } from "../display-text/DisplayText";
25
25
  import {
26
26
  attentionConversationIds,
@@ -1,5 +1,4 @@
1
- import { ChevronRight } from "lucide-react";
2
- import type { ConversationDetailReport } from "@sentry/junior/api/schema";
1
+ import { ArrowRight, BookmarkPlus, Brain, ChevronRight } from "lucide-react";
3
2
 
4
3
  import { dashboardDisclosureChevronClass } from "../styles";
5
4
  import type {
@@ -7,6 +6,7 @@ import type {
7
6
  ConversationMemoryList,
8
7
  } from "./conversationMemory";
9
8
  import { DetailsCard } from "./DetailsList";
9
+ import { PrivateMemoryIcon } from "./MemoryIcons";
10
10
 
11
11
  /** Count the distinct memories that a Conversation recalled and captured. */
12
12
  function conversationMemoryCounts(list: ConversationMemoryList) {
@@ -48,28 +48,38 @@ export function ConversationMemoryCard(props: {
48
48
  );
49
49
  }
50
50
 
51
- /**
52
- * Map each loaded Turn to the prompt that started it, so a memory can say
53
- * where it came from. A Turn starts from its last user input message.
54
- */
55
- export function turnPrompts(
56
- events: ConversationDetailReport["events"] | undefined,
57
- ): Map<string, string> {
58
- const texts = new Map<string, string>();
59
- const prompts = new Map<string, string>();
60
- for (const { data } of events ?? []) {
61
- if (data.type === "message" && data.role === "user" && data.text) {
62
- texts.set(data.messageId, data.text);
63
- }
64
- if (data.type === "turn_lifecycle" && data.state === "started") {
65
- const prompt = data.inputMessageIds
66
- ?.map((id) => texts.get(id))
67
- .filter((text) => text !== undefined)
68
- .at(-1);
69
- if (prompt) prompts.set(data.turnId, prompt);
70
- }
71
- }
72
- return prompts;
51
+ /** One memory with its full text, its visibility, and a jump to its Turn. */
52
+ function MemoryCard(props: {
53
+ entry: ConversationMemoryEntry;
54
+ onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
55
+ }) {
56
+ const { entry } = props;
57
+ const { turnId } = entry;
58
+ return (
59
+ <li className="grid overflow-hidden rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel">
60
+ {/* Break long URLs so the card keeps the panel width. */}
61
+ <p className="m-0 whitespace-pre-wrap px-3 py-2.5 text-sm leading-relaxed text-dashboard-text [overflow-wrap:anywhere]">
62
+ {entry.memory.content}
63
+ </p>
64
+ <div className="flex min-h-8 items-center justify-between gap-2 border-t border-dashboard-border-subtle bg-dashboard-fill-faint px-3 py-1 text-xs text-dashboard-text-muted">
65
+ {entry.memory.visibility === "private" ? (
66
+ <PrivateMemoryIcon />
67
+ ) : (
68
+ <span />
69
+ )}
70
+ {turnId ? (
71
+ <button
72
+ className="-mr-1.5 inline-flex cursor-pointer items-center gap-1 rounded-md border-0 bg-transparent px-1.5 py-1 font-sans text-xs font-medium text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
73
+ onClick={() => props.onSelect({ ...entry, turnId })}
74
+ type="button"
75
+ >
76
+ Go to message
77
+ <ArrowRight aria-hidden="true" size={13} />
78
+ </button>
79
+ ) : null}
80
+ </div>
81
+ </li>
82
+ );
73
83
  }
74
84
 
75
85
  /**
@@ -79,52 +89,39 @@ export function turnPrompts(
79
89
  export function ConversationMemoryEntries(props: {
80
90
  entries: ConversationMemoryEntry[];
81
91
  onSelect(entry: ConversationMemoryEntry & { turnId: string }): void;
82
- prompts: Map<string, string>;
83
92
  }) {
93
+ const groups = (["recalled", "captured"] as const)
94
+ .map((source) => ({
95
+ entries: props.entries.filter((entry) => entry.source === source),
96
+ source,
97
+ }))
98
+ .filter((group) => group.entries.length > 0);
84
99
  return (
85
- <ol className="m-0 grid list-none gap-2 p-0">
86
- {props.entries.map((entry) => {
87
- const { turnId } = entry;
88
- const prompt = turnId ? props.prompts.get(turnId) : undefined;
89
- const source =
90
- entry.source === "recalled"
91
- ? prompt
92
- ? `Recalled for “${prompt}”`
93
- : "Recalled"
94
- : prompt
95
- ? `Captured after “${prompt}”`
96
- : "Captured";
97
- const body = (
98
- <>
99
- <p className="m-0 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
100
- {entry.memory.content}
101
- </p>
102
- <span className="flex min-w-0 items-center gap-2 text-xs text-dashboard-text-muted">
103
- <span className="min-w-0 truncate">{source}</span>
104
- {entry.memory.visibility === "private" ? (
105
- <span className="shrink-0">· Private</span>
106
- ) : null}
107
- </span>
108
- </>
109
- );
110
- const className =
111
- "grid w-full gap-1.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5 text-left font-sans";
112
- return (
113
- <li key={`${entry.source}:${entry.memory.id}`}>
114
- {turnId ? (
115
- <button
116
- className={`${className} cursor-pointer transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus`}
117
- onClick={() => props.onSelect({ ...entry, turnId })}
118
- type="button"
119
- >
120
- {body}
121
- </button>
100
+ <div className="grid gap-5">
101
+ {groups.map((group) => (
102
+ <section className="grid gap-2" key={group.source}>
103
+ <h3 className="m-0 flex items-center gap-1.5 font-sans text-xs font-semibold text-dashboard-text">
104
+ {group.source === "recalled" ? (
105
+ <Brain aria-hidden="true" size={13} strokeWidth={1.8} />
122
106
  ) : (
123
- <div className={className}>{body}</div>
107
+ <BookmarkPlus aria-hidden="true" size={13} strokeWidth={1.8} />
124
108
  )}
125
- </li>
126
- );
127
- })}
128
- </ol>
109
+ {group.source === "recalled" ? "Recalled" : "Captured"}
110
+ <span className="font-normal tabular-nums text-dashboard-text-muted">
111
+ {group.entries.length}
112
+ </span>
113
+ </h3>
114
+ <ol className="m-0 grid list-none gap-2 p-0">
115
+ {group.entries.map((entry) => (
116
+ <MemoryCard
117
+ entry={entry}
118
+ key={`${entry.source}:${entry.memory.id}`}
119
+ onSelect={props.onSelect}
120
+ />
121
+ ))}
122
+ </ol>
123
+ </section>
124
+ ))}
125
+ </div>
129
126
  );
130
127
  }
@@ -469,7 +469,7 @@ function SourceTask(props: {
469
469
  );
470
470
  }
471
471
 
472
- /** Resolve the model still running an open turn, including mid-turn handoffs. */
472
+ /** Resolve the model that an open turn last called. */
473
473
  export function liveModelId(
474
474
  detail: ConversationDetailReport | undefined,
475
475
  ): string | undefined {
@@ -484,12 +484,7 @@ export function liveModelId(
484
484
  if (openTurns.size === 0) modelId = undefined;
485
485
  continue;
486
486
  }
487
- if (openTurns.size === 0) continue;
488
- if (data.type === "turn_routed" && openTurns.has(data.turnId)) {
489
- modelId = data.modelId;
490
- continue;
491
- }
492
- if (data.type === "handoff") modelId = data.modelId;
487
+ if (openTurns.size > 0 && event.model) modelId = event.model.modelId;
493
488
  }
494
489
  return openTurns.size > 0 ? modelId : undefined;
495
490
  }