@sentry/junior-dashboard 0.274.0 → 0.276.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 (52) hide show
  1. package/dist/app.js +28 -9
  2. package/dist/client.js +115 -92
  3. package/dist/index.js +28 -9
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +8 -3
  7. package/src/client/App.tsx +11 -0
  8. package/src/client/components/Metric.tsx +0 -32
  9. package/src/client/components/ProfileMenu.tsx +56 -4
  10. package/src/client/components/layout/DashboardChrome.tsx +3 -3
  11. package/src/client/components/layout/DashboardHeader.tsx +41 -4
  12. package/src/client/components/layout/NavRail.tsx +43 -40
  13. package/src/client/conversations/ConversationComposer.tsx +42 -26
  14. package/src/client/conversations/ConversationDetails.tsx +149 -166
  15. package/src/client/conversations/ConversationHeader.tsx +126 -132
  16. package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
  17. package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
  18. package/src/client/conversations/ConversationMemories.tsx +118 -78
  19. package/src/client/conversations/ConversationMeta.tsx +94 -84
  20. package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
  21. package/src/client/conversations/ConversationPage.tsx +197 -422
  22. package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
  23. package/src/client/conversations/ConversationSidebar.tsx +5 -7
  24. package/src/client/conversations/ConversationTranscript.tsx +37 -8
  25. package/src/client/conversations/ConversationWatches.tsx +37 -15
  26. package/src/client/conversations/PendingMailboxStack.tsx +11 -1
  27. package/src/client/conversations/QuickQuestion.tsx +476 -0
  28. package/src/client/conversations/TelemetryMetrics.tsx +2 -15
  29. package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
  30. package/src/client/conversations/TranscriptEventView.tsx +25 -2
  31. package/src/client/conversations/TranscriptMemoryPopover.tsx +179 -0
  32. package/src/client/conversations/TranscriptMessageView.tsx +46 -16
  33. package/src/client/conversations/TranscriptRows.tsx +9 -2
  34. package/src/client/conversations/TranscriptView.tsx +15 -32
  35. package/src/client/conversations/UsageCard.tsx +4 -18
  36. package/src/client/conversations/conversationMemory.ts +138 -0
  37. package/src/client/conversations/conversationSections.ts +18 -14
  38. package/src/client/conversations/eventTranscript.ts +8 -0
  39. package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
  40. package/src/client/review-desk/ReviewItemActions.tsx +1 -1
  41. package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
  42. package/src/client/review-desk/ReviewItemContext.tsx +41 -23
  43. package/src/client/review-desk/reviewItemFormat.ts +16 -0
  44. package/src/client/sentry.ts +77 -0
  45. package/src/client/types.ts +6 -0
  46. package/src/client.tsx +3 -0
  47. package/src/mock-reporting/watches.ts +2 -0
  48. package/src/shell.ts +27 -2
  49. package/src/tailwind.css +12 -19
  50. package/src/client/components/ActiveIndicator.tsx +0 -14
  51. package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
  52. package/src/client/review-desk/ConversationReviewBar.tsx +0 -85
@@ -1,17 +1,12 @@
1
+ import { ArrowLeft } from "lucide-react";
1
2
  import { useState, type ReactNode } from "react";
3
+ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
2
4
 
3
5
  import { DetailsPanel, useDetailsPanel } from "../components/DetailsPanel";
4
- import {
5
- Tooltip,
6
- TooltipContent,
7
- TooltipTrigger,
8
- } from "../components/ui/tooltip";
9
- import {
10
- Tabs,
11
- TabsContent,
12
- TabsList,
13
- TabsTrigger,
14
- } from "../components/ui/tabs";
6
+ import { formatRelativeTime, formatTime } from "../format";
7
+ import { ConversationReviewItems } from "../review-desk/ConversationReviewItems";
8
+ import { Button } from "../components/ui/button";
9
+ import { ConversationBrief } from "./ConversationBrief";
15
10
  import {
16
11
  ConversationChildAgents,
17
12
  useConversationChildAgents,
@@ -21,70 +16,94 @@ import {
21
16
  type ConversationLink,
22
17
  } from "./ConversationLinkedWork";
23
18
  import {
24
- ConversationMemories,
25
- useConversationMemories,
19
+ ConversationMemoryCard,
20
+ ConversationMemoryEntries,
26
21
  } from "./ConversationMemories";
22
+ import {
23
+ conversationMemoryEntries,
24
+ useConversationMemoryState,
25
+ type ConversationMemoryEntry,
26
+ } from "./conversationMemory";
27
27
  import {
28
28
  ConversationWatches,
29
29
  useConversationWatches,
30
30
  } from "./ConversationWatches";
31
31
  import { DetailsCard } from "./DetailsList";
32
- import { ShareConversationButton } from "./ShareConversationButton";
33
32
  import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
34
33
 
35
- type DetailsTab = "agents" | "summary" | "memories";
36
-
37
- /** Keep the tabs fixed while each panel scrolls to the frame edge. */
38
- const detailsPanelClass =
39
- "min-h-0 overflow-y-auto overscroll-contain rounded-none px-4 pb-[max(1rem,env(safe-area-inset-bottom))] focus-visible:-outline-offset-2";
34
+ type Brief = NonNullable<ConversationDetailReport["brief"]>;
40
35
 
41
36
  /**
42
- * Show conversation details: the brief, participants, and run facts first,
43
- * then watches, linked work, and the usage breakdown. Share and Sentry links
44
- * stay in the header so every tab can reach them.
37
+ * Show conversation details in the order a reader asks about them: what the
38
+ * work is (the Brief), what needs them, what it changed or waits on, who did
39
+ * it, and what it cost. Each fact shows here only, not in the title bar.
45
40
  */
46
41
  export function ConversationDetails(props: {
47
- brief: ReactNode;
42
+ brief?: Brief;
48
43
  conversationId: string;
44
+ /** True when a sidebar review chip opened details. */
45
+ highlightReview: boolean;
49
46
  identity: ReactNode;
50
47
  links: ConversationLink[];
51
48
  onClose(): void;
52
49
  /** Open one child agent transcript. A details sheet closes first. */
53
50
  onOpenSubagent(target: SubagentTranscriptTarget): void;
51
+ /** Show one memory where its Turn recalled or captured it. */
52
+ onSelectMemory(entry: ConversationMemoryEntry & { turnId: string }): void;
53
+ /** The prompt that started each loaded Turn. */
54
+ memoryPrompts: Map<string, string>;
54
55
  overview: ReactNode;
55
- sentryConversationUrl?: string;
56
56
  usage: ReactNode;
57
57
  }) {
58
- const [tab, setTab] = useState<DetailsTab>("summary");
59
58
  const panel = useDetailsPanel();
60
- // Load both lists with details so each tab can show its count.
61
- const memories = useConversationMemories(props.conversationId);
62
- const agents = useConversationChildAgents(props.conversationId);
63
- const watches = useConversationWatches(props.conversationId).data?.watches;
59
+ const memoryList = useConversationMemoryState()?.list;
60
+ const [memoriesOpen, setMemoriesOpen] = useState(false);
61
+ const [memoryConversationId, setMemoryConversationId] = useState(
62
+ props.conversationId,
63
+ );
64
+ if (memoryConversationId !== props.conversationId) {
65
+ setMemoryConversationId(props.conversationId);
66
+ setMemoriesOpen(false);
67
+ }
68
+ const agents = useConversationChildAgents(props.conversationId).data?.agents;
69
+ const watches =
70
+ useConversationWatches(props.conversationId).data?.watches ?? [];
64
71
 
65
- // People first, then run facts. Watches come before linked work: they say
66
- // what can wake this agent later, so readers see them without another tab.
67
- const sections = [
68
- { content: props.identity, key: "participants", title: "Participants" },
69
- { content: props.overview, key: "overview", title: undefined },
70
- {
71
- content: watches?.length ? (
72
- <ConversationWatches watches={watches} />
73
- ) : null,
74
- count: watches?.length,
75
- key: "watching",
76
- title: "Watching",
77
- },
78
- {
79
- content: props.links.length ? (
80
- <ConversationLinkedWork links={props.links} />
81
- ) : null,
82
- count: props.links.length,
83
- key: "linked-work",
84
- title: "Linked work",
85
- },
86
- { content: props.usage, key: "usage", title: "Usage" },
87
- ].filter((section) => section.content != null);
72
+ if (memoriesOpen && memoryList) {
73
+ // The list replaces details in place, so nothing stacks over the panel.
74
+ return (
75
+ <DetailsPanel
76
+ label="Conversation details"
77
+ onClose={props.onClose}
78
+ title={
79
+ <div className="-ml-2 flex min-w-0 items-center gap-1">
80
+ <Button
81
+ aria-label="Back to details"
82
+ onClick={() => setMemoriesOpen(false)}
83
+ size="icon"
84
+ >
85
+ <ArrowLeft aria-hidden="true" size={15} />
86
+ </Button>
87
+ <h2 className="m-0 truncate font-sans text-sm font-semibold text-dashboard-text">
88
+ Memories
89
+ </h2>
90
+ </div>
91
+ }
92
+ >
93
+ <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
94
+ <ConversationMemoryEntries
95
+ entries={conversationMemoryEntries(memoryList)}
96
+ onSelect={(entry) => {
97
+ // The transcript sits under a details sheet. Close it first.
98
+ if (!panel) props.onClose();
99
+ props.onSelectMemory(entry);
100
+ }}
101
+ prompts={props.memoryPrompts}
102
+ />
103
+ </div>
104
+ </DetailsPanel>
105
+ );
106
+ }
88
107
 
89
108
  return (
90
109
  <DetailsPanel
@@ -92,126 +111,90 @@ export function ConversationDetails(props: {
92
111
  title={
93
112
  <h2
94
113
  aria-label="Conversation details"
95
- className="m-0 text-base font-semibold text-dashboard-text"
114
+ className="m-0 flex min-w-0 items-center gap-1.5 font-sans text-sm"
96
115
  >
97
- Details
116
+ {props.brief ? (
117
+ <>
118
+ <span className="font-semibold text-dashboard-text">Brief</span>
119
+ <span aria-hidden="true" className="text-dashboard-text-faint">
120
+ ·
121
+ </span>
122
+ <time
123
+ className="truncate font-normal text-dashboard-text-muted"
124
+ dateTime={props.brief.updatedAt}
125
+ title={formatTime(props.brief.updatedAt, {
126
+ dateStyle: "medium",
127
+ timeStyle: "short",
128
+ })}
129
+ >
130
+ Updated {formatRelativeTime(props.brief.updatedAt)}
131
+ </time>
132
+ </>
133
+ ) : (
134
+ <span className="font-semibold text-dashboard-text">Details</span>
135
+ )}
98
136
  </h2>
99
137
  }
100
- actions={
101
- <>
102
- <ShareConversationButton
103
- key={props.conversationId}
104
- conversationId={props.conversationId}
105
- layout="icon"
106
- />
107
- {props.sentryConversationUrl ? (
108
- <Tooltip>
109
- <TooltipTrigger
110
- render={
111
- <a
112
- aria-label="Open in Sentry"
113
- className="grid size-9 place-items-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
114
- href={props.sentryConversationUrl}
115
- rel="noopener noreferrer"
116
- target="_blank"
117
- >
118
- <SentryGlyph />
119
- </a>
120
- }
121
- />
122
- <TooltipContent size="compact" side="bottom">
123
- Open in Sentry
124
- </TooltipContent>
125
- </Tooltip>
126
- ) : null}
127
- </>
128
- }
129
138
  onClose={props.onClose}
130
139
  >
131
- <Tabs
132
- className="h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-0"
133
- onValueChange={(value: DetailsTab) => setTab(value)}
134
- value={tab}
135
- >
136
- <TabsList
137
- activateOnFocus
138
- aria-label="Conversation details"
139
- className="m-4"
140
- >
141
- <TabsTrigger value="summary">
142
- <span className="truncate">Summary</span>
143
- </TabsTrigger>
144
- <TabsTrigger value="memories">
145
- <span className="truncate">Memories</span>
146
- <TabCount count={memories.data?.memories.length} />
147
- </TabsTrigger>
148
- <TabsTrigger value="agents">
149
- <span className="truncate">Subagents</span>
150
- <TabCount count={agents.data?.agents.length} />
151
- </TabsTrigger>
152
- </TabsList>
153
- <TabsContent className={detailsPanelClass} value="summary">
154
- {props.brief != null || sections.length > 0 ? (
155
- <div className="grid min-w-0 gap-3">
156
- {props.brief != null ? (
157
- <div className="min-w-0 break-words px-1 pb-1 font-sans text-sm leading-relaxed text-dashboard-text-muted">
158
- {props.brief}
159
- </div>
160
- ) : null}
161
- {sections.map((section) => (
162
- <DetailsCard
163
- count={section.count}
164
- key={section.key}
165
- title={section.title}
166
- >
167
- {section.content}
168
- </DetailsCard>
169
- ))}
170
- </div>
171
- ) : (
172
- <p className="m-0 font-sans text-sm leading-relaxed text-dashboard-text-muted">
173
- No additional conversation details.
174
- </p>
175
- )}
176
- </TabsContent>
177
- <TabsContent className={detailsPanelClass} value="memories">
178
- <ConversationMemories conversationId={props.conversationId} />
179
- </TabsContent>
180
- <TabsContent className={detailsPanelClass} value="agents">
181
- <ConversationChildAgents
182
- conversationId={props.conversationId}
183
- onOpenAgent={(target) => {
184
- // The panel stays beside the drawer. A modal sheet cannot.
185
- if (!panel) props.onClose();
186
- props.onOpenSubagent(target);
187
- }}
140
+ <div className="grid min-w-0 content-start gap-3 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3">
141
+ {props.brief ? (
142
+ <div className="min-w-0 break-words px-1 pb-1 font-sans text-sm leading-relaxed text-dashboard-text-muted">
143
+ <ConversationBrief brief={props.brief} variant="summary" />
144
+ </div>
145
+ ) : null}
146
+ <ConversationReviewItems
147
+ conversationId={props.conversationId}
148
+ highlight={props.highlightReview}
149
+ />
150
+ {props.links.length > 0 || watches.length > 0 ? (
151
+ // A Watch waits on something. It shows with linked work, so the
152
+ // work and what Junior waits on read as one list.
153
+ <DetailsCard
154
+ count={props.links.length || watches.length}
155
+ title={props.links.length > 0 ? "Linked work" : "Waiting on"}
156
+ >
157
+ {props.links.length > 0 ? (
158
+ <ConversationLinkedWork links={props.links} />
159
+ ) : null}
160
+ {props.links.length > 0 && watches.length > 0 ? (
161
+ <h4 className="m-0 mb-1.5 mt-3 font-sans text-xs font-medium text-dashboard-text-faint">
162
+ Also waiting on
163
+ </h4>
164
+ ) : null}
165
+ {watches.length > 0 ? (
166
+ <ConversationWatches watches={watches} />
167
+ ) : null}
168
+ </DetailsCard>
169
+ ) : null}
170
+ {agents?.length ? (
171
+ <DetailsCard count={agents.length} title="Subagents">
172
+ <ConversationChildAgents
173
+ conversationId={props.conversationId}
174
+ onOpenAgent={(target) => {
175
+ // The panel stays beside the drawer. A modal sheet cannot.
176
+ if (!panel) props.onClose();
177
+ props.onOpenSubagent(target);
178
+ }}
179
+ />
180
+ </DetailsCard>
181
+ ) : null}
182
+ {props.overview != null ? (
183
+ <DetailsCard>{props.overview}</DetailsCard>
184
+ ) : null}
185
+ {props.usage != null ? (
186
+ <DetailsCard title="Usage">{props.usage}</DetailsCard>
187
+ ) : null}
188
+ {props.identity != null ? (
189
+ <DetailsCard title="Participants">{props.identity}</DetailsCard>
190
+ ) : null}
191
+ {memoryList?.events.length ? (
192
+ <ConversationMemoryCard
193
+ list={memoryList}
194
+ onOpen={() => setMemoriesOpen(true)}
188
195
  />
189
- </TabsContent>
190
- </Tabs>
196
+ ) : null}
197
+ </div>
191
198
  </DetailsPanel>
192
199
  );
193
200
  }
194
-
195
- /** Show a panel's entry count. Zero and unknown counts are hidden. */
196
- function TabCount(props: { count: number | undefined }) {
197
- if (!props.count) return null;
198
- return (
199
- <span className="min-w-5 shrink-0 rounded-full bg-dashboard-fill-strong px-1.5 text-center text-xs font-semibold leading-5 tabular-nums">
200
- {props.count.toLocaleString("en-US")}
201
- </span>
202
- );
203
- }
204
-
205
- /** Draw the Sentry glyph in the current text color, sized like a header icon. */
206
- function SentryGlyph() {
207
- return (
208
- <svg
209
- aria-hidden="true"
210
- className="size-4"
211
- fill="currentColor"
212
- viewBox="0 0 24 24"
213
- >
214
- <path d="M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z" />
215
- </svg>
216
- );
217
- }