@sentry/junior-dashboard 0.266.0 → 0.268.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 (96) hide show
  1. package/dist/app.js +85 -12
  2. package/dist/client.js +94 -64
  3. package/dist/index.js +85 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +55 -53
  9. package/src/client/components/DetailsPanel.tsx +12 -13
  10. package/src/client/components/Drawer.tsx +46 -23
  11. package/src/client/components/ImageAttachment.tsx +1 -1
  12. package/src/client/components/ParticipantAvatarStack.tsx +6 -2
  13. package/src/client/components/README.md +15 -2
  14. package/src/client/components/UserInputForm.tsx +283 -0
  15. package/src/client/components/layout/DashboardHeader.tsx +13 -0
  16. package/src/client/components/ui/alert.tsx +1 -1
  17. package/src/client/components/ui/button-group.tsx +59 -0
  18. package/src/client/components/ui/button.tsx +11 -1
  19. package/src/client/components/ui/dialog.tsx +29 -35
  20. package/src/client/components/ui/drawer.tsx +140 -0
  21. package/src/client/components/ui/empty.tsx +30 -1
  22. package/src/client/components/ui/separator.tsx +22 -0
  23. package/src/client/components/ui/useSheet.ts +5 -1
  24. package/src/client/conversations/ComposerImages.tsx +66 -14
  25. package/src/client/conversations/ConversationComposer.tsx +84 -12
  26. package/src/client/conversations/ConversationDetails.tsx +3 -27
  27. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  28. package/src/client/conversations/ConversationHomeList.tsx +6 -0
  29. package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
  30. package/src/client/conversations/ConversationPage.tsx +160 -2
  31. package/src/client/conversations/ConversationSidebar.tsx +6 -0
  32. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  33. package/src/client/conversations/ConversationWorkspace.tsx +4 -0
  34. package/src/client/conversations/DetailsList.tsx +33 -0
  35. package/src/client/conversations/EventDetails.tsx +46 -19
  36. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  37. package/src/client/conversations/PendingMailboxStack.tsx +310 -112
  38. package/src/client/conversations/README.md +30 -13
  39. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  40. package/src/client/conversations/TranscriptActivityGroup.tsx +27 -19
  41. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  42. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  43. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  44. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  45. package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
  46. package/src/client/conversations/TranscriptMessageView.tsx +10 -1
  47. package/src/client/conversations/TranscriptSummary.tsx +5 -31
  48. package/src/client/conversations/TranscriptToolView.tsx +13 -1
  49. package/src/client/conversations/TranscriptTurnContextView.tsx +174 -161
  50. package/src/client/conversations/TranscriptView.tsx +7 -3
  51. package/src/client/conversations/UserInputRequestCard.tsx +39 -0
  52. package/src/client/conversations/conversationOutbox.ts +46 -0
  53. package/src/client/conversations/eventTime.ts +13 -1
  54. package/src/client/conversations/eventTranscript.ts +12 -5
  55. package/src/client/conversations/queries.ts +127 -26
  56. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  57. package/src/client/conversations/turnContext.ts +15 -0
  58. package/src/client/conversations/userInputRequest.ts +56 -0
  59. package/src/client/format.ts +31 -8
  60. package/src/client/pages/SettingsPage.tsx +103 -3
  61. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  62. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  63. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  64. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  65. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  66. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  67. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  68. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  69. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  70. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  71. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  72. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  73. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  74. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  75. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  76. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  77. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  78. package/src/client/pages/automations/README.md +56 -46
  79. package/src/client/pages/code/RepositorySections.tsx +1 -0
  80. package/src/client/pages/code/RepositoryTabs.tsx +3 -0
  81. package/src/client/primaryNav.ts +15 -2
  82. package/src/client/review-desk/ConversationReviewBar.tsx +62 -0
  83. package/src/client/review-desk/ReviewDeskPage.tsx +277 -0
  84. package/src/client/review-desk/ReviewItemActions.tsx +165 -0
  85. package/src/client/review-desk/ReviewItemCard.tsx +47 -0
  86. package/src/client/review-desk/ReviewItemDetail.tsx +115 -0
  87. package/src/client/review-desk/queries.ts +164 -0
  88. package/src/client/review-desk/reviewItemFormat.ts +61 -0
  89. package/src/client/types.ts +3 -0
  90. package/src/mock-reporting/fixtures.ts +54 -2
  91. package/src/mock-reporting/routes.ts +20 -0
  92. package/src/shell.ts +3 -0
  93. package/src/client/conversations/transcriptStyles.ts +0 -10
  94. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  95. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  96. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -3,7 +3,11 @@ import { useState } from "react";
3
3
 
4
4
  import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
5
5
  import { dashboardDisclosureChevronClass } from "../styles";
6
- import { formatMessageTimestamp, transcriptMessageActorLabel } from "../format";
6
+ import {
7
+ formatMessageTimestamp,
8
+ transcriptMessageActor,
9
+ transcriptMessageActorLabel,
10
+ } from "../format";
7
11
  import type { ConversationTranscript, TranscriptViewMessage } from "../types";
8
12
  import {
9
13
  TranscriptHeadingMeta,
@@ -61,8 +65,13 @@ export function TranscriptMessageContextView(props: {
61
65
  {!props.redacted && props.message.contexts?.length ? (
62
66
  <div className="mt-2">
63
67
  <TranscriptTurnContextView
68
+ actor={transcriptMessageActor(
69
+ props.conversation,
70
+ props.message,
71
+ )}
64
72
  contexts={props.message.contexts}
65
73
  messageId={props.message.messageId}
74
+ timestamp={props.message.timestamp}
66
75
  />
67
76
  </div>
68
77
  ) : null}
@@ -93,8 +102,10 @@ export function TranscriptMessageContextView(props: {
93
102
  {!props.redacted && props.message.contexts?.length ? (
94
103
  <div className="mt-2">
95
104
  <TranscriptTurnContextView
105
+ actor={transcriptMessageActor(props.conversation, props.message)}
96
106
  contexts={props.message.contexts}
97
107
  messageId={props.message.messageId}
108
+ timestamp={props.message.timestamp}
98
109
  />
99
110
  </div>
100
111
  ) : null}
@@ -1,11 +1,15 @@
1
1
  import { ForkConversationButton } from "./ForkConversationButton";
2
- import { TranscriptAttachments } from "./TranscriptAttachment";
2
+ import {
3
+ TranscriptAttachments,
4
+ TranscriptLocalImages,
5
+ } from "./TranscriptAttachment";
3
6
  import { ObjectCard } from "./ObjectCard";
4
7
  import { AutomationCard } from "../components/AutomationCard";
5
8
  import { memo, type ReactNode } from "react";
6
9
 
7
10
  import {
8
11
  formatMessageTimestamp,
12
+ transcriptMessageActor,
9
13
  transcriptMessageActorLabel,
10
14
  transcriptRoleKind,
11
15
  } from "../format";
@@ -69,6 +73,9 @@ export const TranscriptMessageView = memo(
69
73
  conversationId={props.conversation.conversationId}
70
74
  />
71
75
  ) : null}
76
+ {props.message.images?.length ? (
77
+ <TranscriptLocalImages images={props.message.images} />
78
+ ) : null}
72
79
  {props.message.cards?.map((card) => {
73
80
  switch (card.kind) {
74
81
  case "object":
@@ -124,8 +131,10 @@ function messageAction(
124
131
  if (message.role === "user" && message.contexts?.length) {
125
132
  return (
126
133
  <TranscriptTurnContextView
134
+ actor={transcriptMessageActor(conversation, message)}
127
135
  contexts={message.contexts}
128
136
  messageId={message.messageId}
137
+ timestamp={message.timestamp}
129
138
  />
130
139
  );
131
140
  }
@@ -1,37 +1,12 @@
1
- import type { ComponentProps, ComponentPropsWithoutRef } from "react";
1
+ import type { ComponentProps } from "react";
2
2
 
3
3
  import { CollapsibleTrigger } from "../components/ui/collapsible";
4
4
  import { cn } from "../styles";
5
5
 
6
- const summaryClass =
7
- "cursor-pointer rounded-md px-2 py-1.5 transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus";
8
-
9
- /** Keep hover and keyboard focus around a padded transcript cell. */
10
- export function TranscriptSummary({
11
- className,
12
- flush = false,
13
- ...props
14
- }: ComponentPropsWithoutRef<"summary"> & {
15
- /** Extend padding around text that already sits on the transcript edge. */
16
- flush?: boolean;
17
- }) {
18
- return (
19
- <summary
20
- {...props}
21
- className={cn(
22
- summaryClass,
23
- "list-none [&::-webkit-details-marker]:hidden",
24
- flush && "-mx-2 -my-1.5",
25
- className,
26
- )}
27
- />
28
- );
29
- }
30
-
31
6
  /**
32
- * Give a `Collapsible` trigger the same padding, hover, and focus as
33
- * `TranscriptSummary`. The caller sets the display. Put only inline elements,
34
- * such as `span`, inside the trigger.
7
+ * Give a `Collapsible` trigger the padding, hover, and keyboard focus of a
8
+ * transcript disclosure. The caller sets the display. Put only inline
9
+ * elements, such as `span`, inside the trigger.
35
10
  */
36
11
  export function TranscriptSummaryTrigger({
37
12
  className,
@@ -46,8 +21,7 @@ export function TranscriptSummaryTrigger({
46
21
  <CollapsibleTrigger
47
22
  {...props}
48
23
  className={cn(
49
- summaryClass,
50
- "text-left disabled:cursor-default disabled:hover:bg-transparent",
24
+ "cursor-pointer rounded-md px-2 py-1.5 text-left transition-colors hover:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus disabled:cursor-default disabled:hover:bg-transparent",
51
25
  flush ? "-mx-2 -my-1.5 w-[calc(100%+1rem)]" : "w-full",
52
26
  className,
53
27
  )}
@@ -10,6 +10,8 @@ import {
10
10
  } from "../format";
11
11
  import type { TranscriptViewToolCallPart } from "../types";
12
12
  import { ShimmerText } from "../components/ShimmerText";
13
+ import { TranscriptReviewItemCard } from "../review-desk/ReviewItemCard";
14
+ import { reviewItemIdFromToolCall } from "../review-desk/reviewItemFormat";
13
15
  import { cn } from "../styles";
14
16
  import { ToolFrame } from "./ToolFrame";
15
17
  import { toolCallPreview } from "./toolCallPreview";
@@ -64,7 +66,17 @@ export const TranscriptToolView = memo(function TranscriptToolView(props: {
64
66
  </ToolFrame>
65
67
  );
66
68
 
67
- return <div className="min-w-0">{frame}</div>;
69
+ const reviewItemId = reviewItemIdFromToolCall(props.part);
70
+ return (
71
+ <div className="min-w-0">
72
+ {frame}
73
+ {reviewItemId ? (
74
+ <div className="mt-2">
75
+ <TranscriptReviewItemCard itemId={reviewItemId} />
76
+ </div>
77
+ ) : null}
78
+ </div>
79
+ );
68
80
  });
69
81
 
70
82
  function ToolSignature(props: {
@@ -1,4 +1,5 @@
1
- import { Brain, Braces, ChevronRight, Layers } from "lucide-react";
1
+ import type { ActorIdentity } from "@sentry/junior/api/schema";
2
+ import { Layers } from "lucide-react";
2
3
  import {
3
4
  createContext,
4
5
  useContext,
@@ -7,23 +8,39 @@ import {
7
8
  type ReactNode,
8
9
  } from "react";
9
10
  import { createPortal } from "react-dom";
11
+ import { Link } from "react-router";
10
12
 
13
+ import { ActorAvatar } from "../components/ActorAvatar";
11
14
  import { DetailsPanel } from "../components/DetailsPanel";
12
- import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
15
+ import { participantName } from "../components/ParticipantAvatarStack";
16
+ import { Button } from "../components/ui/button";
13
17
  import { Drawer } from "../components/Drawer";
18
+ import {
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipTrigger,
22
+ } from "../components/ui/tooltip";
14
23
  import type { TranscriptViewTurnContext } from "../types";
15
- import { formatMessageTimestamp, formatTime } from "../format";
16
- import { memoryRecallContent, type MemoryRecallContent } from "./turnContext";
17
- import { cn, dashboardDisclosureChevronClass } from "../styles";
24
+ import { formatMessageTimestamp, memoryPath, peoplePath } from "../format";
25
+ import {
26
+ memoryRecallContent,
27
+ userCustomizationContent,
28
+ type MemoryRecallContent,
29
+ } from "./turnContext";
30
+ import { cn } from "../styles";
31
+ import { DetailsCard } from "./DetailsList";
18
32
  import { HighlightText } from "./transcriptSearch";
19
- import { TranscriptSummaryTrigger } from "./TranscriptSummary";
20
33
 
21
- /** The turn context that a page panel shows, keyed by its message. */
22
- export type TurnContextSelection = {
34
+ /** What one turn context view shows: who spoke, when, and the context. */
35
+ export type TurnContext = {
36
+ actor?: ActorIdentity;
23
37
  contexts: TranscriptViewTurnContext[];
24
- id: string;
38
+ timestamp?: number;
25
39
  };
26
40
 
41
+ /** The turn context that a page panel shows, keyed by its message. */
42
+ export type TurnContextSelection = TurnContext & { id: string };
43
+
27
44
  /** Let the page that owns a panel show turn context there. */
28
45
  export type TurnContextPanel = {
29
46
  openId?: string;
@@ -55,10 +72,9 @@ export function TurnContextPanelProvider(props: {
55
72
  * keys context by `messageId`, so every control for one message, including a
56
73
  * remounted one, shows the same open state.
57
74
  */
58
- export function TranscriptTurnContextView(props: {
59
- contexts: TranscriptViewTurnContext[];
60
- messageId?: string;
61
- }) {
75
+ export function TranscriptTurnContextView(
76
+ props: TurnContext & { messageId?: string },
77
+ ) {
62
78
  const panel = useContext(TurnContextPanelContext);
63
79
  const [open, setOpen] = useState(false);
64
80
  const instanceId = useId();
@@ -67,24 +83,38 @@ export function TranscriptTurnContextView(props: {
67
83
 
68
84
  return (
69
85
  <>
70
- <button
71
- aria-haspopup={panel ? undefined : "dialog"}
72
- aria-expanded={expanded}
73
- aria-label="View turn context"
74
- className={cn(
75
- "inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:bg-dashboard-fill-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus",
76
- expanded && "bg-dashboard-fill-hover text-dashboard-text",
77
- )}
78
- data-turn-context-id={id}
79
- onClick={() => {
80
- if (!panel) return setOpen(true);
81
- panel.onToggle({ contexts: props.contexts, id });
82
- }}
83
- title="View context"
84
- type="button"
85
- >
86
- <Layers aria-hidden="true" size={14} strokeWidth={1.8} />
87
- </button>
86
+ <Tooltip>
87
+ <TooltipTrigger
88
+ // The open panel keeps focus here. Close the tooltip on click so
89
+ // Escape reaches the panel.
90
+ closeOnClick
91
+ render={
92
+ <Button
93
+ aria-haspopup={panel ? undefined : "dialog"}
94
+ aria-expanded={expanded}
95
+ aria-label="View turn context"
96
+ className={cn(
97
+ "size-6 shrink-0",
98
+ expanded && "bg-dashboard-fill-strong text-dashboard-text",
99
+ )}
100
+ data-turn-context-id={id}
101
+ onClick={() => {
102
+ if (!panel) return setOpen(true);
103
+ panel.onToggle({
104
+ actor: props.actor,
105
+ contexts: props.contexts,
106
+ id,
107
+ timestamp: props.timestamp,
108
+ });
109
+ }}
110
+ size="icon"
111
+ >
112
+ <Layers aria-hidden="true" size={14} strokeWidth={1.8} />
113
+ </Button>
114
+ }
115
+ />
116
+ <TooltipContent size="compact">View turn context</TooltipContent>
117
+ </Tooltip>
88
118
 
89
119
  {!panel && open
90
120
  ? createPortal(
@@ -102,7 +132,11 @@ export function TranscriptTurnContextView(props: {
102
132
  openKey={instanceId}
103
133
  titleId={instanceId}
104
134
  >
105
- <TurnContextList contexts={props.contexts} />
135
+ <TurnContextBody
136
+ actor={props.actor}
137
+ contexts={props.contexts}
138
+ timestamp={props.timestamp}
139
+ />
106
140
  </Drawer>,
107
141
  document.body,
108
142
  )
@@ -112,10 +146,7 @@ export function TranscriptTurnContextView(props: {
112
146
  }
113
147
 
114
148
  /** Show turn context in the page panel: an aside, or a sheet on small screens. */
115
- export function TurnContextDetails(props: {
116
- contexts: TranscriptViewTurnContext[];
117
- onClose(): void;
118
- }) {
149
+ export function TurnContextDetails(props: TurnContext & { onClose(): void }) {
119
150
  return (
120
151
  <DetailsPanel
121
152
  label="Turn context"
@@ -126,151 +157,133 @@ export function TurnContextDetails(props: {
126
157
  </h2>
127
158
  }
128
159
  >
129
- <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
130
- <TurnContextList contexts={props.contexts} />
160
+ <div className="p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
161
+ <TurnContextBody
162
+ actor={props.actor}
163
+ contexts={props.contexts}
164
+ timestamp={props.timestamp}
165
+ />
131
166
  </div>
132
167
  </DetailsPanel>
133
168
  );
134
169
  }
135
170
 
136
- function TurnContextList(props: { contexts: TranscriptViewTurnContext[] }) {
137
- return props.contexts.map((context, index) => (
138
- <TurnContext
139
- context={context}
140
- key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
141
- />
142
- ));
171
+ /** Show who spoke, then their customization, then plugin context. */
172
+ function TurnContextBody(props: TurnContext) {
173
+ const contexts = [...props.contexts].sort(
174
+ (a, b) =>
175
+ Number(Boolean(userCustomizationContent(b))) -
176
+ Number(Boolean(userCustomizationContent(a))),
177
+ );
178
+ return (
179
+ <div className="grid min-w-0 gap-3">
180
+ {props.actor ? (
181
+ <TurnContextActor actor={props.actor} timestamp={props.timestamp} />
182
+ ) : null}
183
+ {contexts.map((context, index) => (
184
+ <TurnContextCard
185
+ context={context}
186
+ key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
187
+ />
188
+ ))}
189
+ </div>
190
+ );
143
191
  }
144
192
 
145
- function TurnContext(props: { context: TranscriptViewTurnContext }) {
146
- const memory = memoryRecallContent(props.context);
147
-
193
+ function TurnContextActor(props: { actor: ActorIdentity; timestamp?: number }) {
194
+ const name = participantName(props.actor);
195
+ const email = props.actor.email?.trim();
196
+ const handle = props.actor.slackUserName?.trim();
197
+ const meta = [
198
+ email && email !== name ? email : undefined,
199
+ handle && handle !== name ? `@${handle}` : undefined,
200
+ ].filter(Boolean);
148
201
  return (
149
- <section className="border-b border-dashboard-border-strong py-5 last:border-b-0">
150
- <div className="mb-4 flex items-start justify-between gap-4">
151
- <div className="flex min-w-0 items-center gap-2.5">
152
- {memory ? (
153
- <Brain
154
- aria-hidden="true"
155
- className="shrink-0 text-dashboard-accent-text/60"
156
- size={15}
157
- />
202
+ <section
203
+ aria-label="Speaker"
204
+ className="flex min-w-0 items-center gap-3 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface-panel p-3"
205
+ >
206
+ <ActorAvatar name={name} size="detail" />
207
+ <div className="min-w-0 flex-1">
208
+ <div className="flex min-w-0 items-baseline justify-between gap-3">
209
+ {email ? (
210
+ <Link
211
+ className="min-w-0 truncate text-sm font-medium text-dashboard-text no-underline hover:underline"
212
+ to={peoplePath(email)}
213
+ >
214
+ {name}
215
+ </Link>
158
216
  ) : (
159
- <Braces
160
- aria-hidden="true"
161
- className="shrink-0 text-dashboard-accent-text/60"
162
- size={15}
163
- />
217
+ <span className="min-w-0 truncate text-sm font-medium text-dashboard-text">
218
+ {name}
219
+ </span>
164
220
  )}
165
- <h3 className="m-0 truncate text-sm font-semibold text-dashboard-text">
166
- {memory ? "Recalled memories" : props.context.pluginName}
167
- </h3>
221
+ {props.timestamp !== undefined ? (
222
+ <time
223
+ className="shrink-0 text-xs tabular-nums text-dashboard-text-muted"
224
+ dateTime={new Date(props.timestamp).toISOString()}
225
+ >
226
+ {formatMessageTimestamp(props.timestamp, true)}
227
+ </time>
228
+ ) : null}
168
229
  </div>
169
- <span className="shrink-0 text-xs text-dashboard-text-muted">
170
- {props.context.kind} · v{props.context.version}
171
- </span>
230
+ {meta.length ? (
231
+ <p
232
+ className="m-0 mt-0.5 truncate text-xs text-dashboard-text-muted"
233
+ title={props.actor.slackUserId}
234
+ >
235
+ {meta.join(" · ")}
236
+ </p>
237
+ ) : null}
172
238
  </div>
173
-
174
- {memory ? (
175
- <MemoryRecall
176
- loadedAt={props.context.loadedAt}
177
- memories={memory.memories}
178
- />
179
- ) : (
180
- <GenericContext context={props.context} />
181
- )}
182
239
  </section>
183
240
  );
184
241
  }
185
242
 
186
- function MemoryRecall(props: {
187
- loadedAt: string;
188
- memories: MemoryRecallContent["memories"];
189
- }) {
243
+ function TurnContextCard(props: { context: TranscriptViewTurnContext }) {
244
+ const memory = memoryRecallContent(props.context);
245
+ if (memory) {
246
+ return (
247
+ <DetailsCard count={memory.memories.length} title="Recalled memories">
248
+ <MemoryRecall memories={memory.memories} />
249
+ </DetailsCard>
250
+ );
251
+ }
252
+ const customization = userCustomizationContent(props.context);
253
+ if (customization) {
254
+ return (
255
+ <DetailsCard title="User customization">
256
+ <p className="m-0 whitespace-pre-wrap text-sm leading-relaxed text-dashboard-text">
257
+ <HighlightText text={customization.text} />
258
+ </p>
259
+ </DetailsCard>
260
+ );
261
+ }
190
262
  return (
191
- <div className="overflow-hidden rounded-lg border border-dashboard-border-strong">
192
- {props.memories.map((memory, index) => (
193
- <Collapsible
194
- className="border-t border-dashboard-border-strong first:border-t-0"
195
- key={memory.id}
196
- >
197
- <TranscriptSummaryTrigger className="group flex items-start gap-2.5">
198
- <ChevronRight
199
- aria-hidden="true"
200
- className={cn(
201
- dashboardDisclosureChevronClass,
202
- "mt-0.5 text-dashboard-text-muted",
203
- )}
204
- />
205
- <span className="min-w-0 flex-1">
206
- <span className="flex items-center justify-between gap-3">
207
- <span className="text-xs font-medium text-dashboard-text-muted">
208
- Memory {index + 1}
209
- </span>
210
- <span className="shrink-0 text-xs text-dashboard-text-muted">
211
- {memory.kind} · {memory.scope}
212
- </span>
213
- </span>
214
- <span className="mt-1 block truncate text-sm text-dashboard-text-muted">
215
- <HighlightText text={memory.content} />
216
- </span>
217
- </span>
218
- </TranscriptSummaryTrigger>
219
-
220
- <CollapsibleContent hiddenUntilFound>
221
- <div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
222
- <div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
223
- <HighlightText text={memory.content} />
224
- </div>
225
-
226
- <dl className="mt-4 grid gap-2 text-xs">
227
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
228
- <dt className="text-dashboard-text-muted">Memory ID</dt>
229
- <dd className="m-0 break-all font-mono text-dashboard-text-muted">
230
- <HighlightText text={memory.id} />
231
- </dd>
232
- </div>
233
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
234
- <dt className="text-dashboard-text-muted">Observed</dt>
235
- <dd className="m-0 text-dashboard-text-muted">
236
- {formatTime(new Date(memory.observedAtMs).toISOString())}
237
- </dd>
238
- </div>
239
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
240
- <dt className="text-dashboard-text-muted">Scope</dt>
241
- <dd className="m-0 text-dashboard-text-muted">
242
- {memory.scope}
243
- </dd>
244
- </div>
245
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
246
- <dt className="text-dashboard-text-muted">Kind</dt>
247
- <dd className="m-0 text-dashboard-text-muted">
248
- {memory.kind}
249
- </dd>
250
- </div>
251
- </dl>
252
- </div>
253
- </CollapsibleContent>
254
- </Collapsible>
255
- ))}
256
- <p className="m-0 border-t border-dashboard-border-strong px-3 py-2 text-xs text-dashboard-text-muted">
257
- {props.memories.length}{" "}
258
- {props.memories.length === 1 ? "memory" : "memories"} · Loaded{" "}
259
- {formatMessageTimestamp(Date.parse(props.loadedAt))}
260
- </p>
261
- </div>
263
+ <DetailsCard
264
+ title={`${props.context.pluginName} · ${props.context.kind} v${props.context.version}`}
265
+ >
266
+ <pre className="m-0 overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed">
267
+ <HighlightText text={JSON.stringify(props.context.content, null, 2)} />
268
+ </pre>
269
+ </DetailsCard>
262
270
  );
263
271
  }
264
272
 
265
- function GenericContext(props: { context: TranscriptViewTurnContext }) {
273
+ // Each row links to the memory's page, which shows its details.
274
+ function MemoryRecall(props: { memories: MemoryRecallContent["memories"] }) {
266
275
  return (
267
- <div>
268
- <pre className="m-0 overflow-x-auto whitespace-pre-wrap break-words rounded-lg bg-dashboard-fill-soft p-3 text-xs leading-relaxed text-dashboard-text-muted">
269
- <HighlightText text={JSON.stringify(props.context.content, null, 2)} />
270
- </pre>
271
- <p className="m-0 mt-3 text-xs text-dashboard-text-muted">
272
- Loaded {formatMessageTimestamp(Date.parse(props.context.loadedAt))}
273
- </p>
274
- </div>
276
+ <ul className="-mx-1.5 -mb-1.5 -mt-1 grid list-none gap-0.5 p-0">
277
+ {props.memories.map((memory) => (
278
+ <li key={memory.id}>
279
+ <Link
280
+ className="line-clamp-2 rounded-md px-1.5 py-1.5 text-sm leading-5 text-dashboard-text no-underline transition-colors hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dashboard-focus"
281
+ to={memoryPath(memory.id)}
282
+ >
283
+ <HighlightText text={memory.content} />
284
+ </Link>
285
+ </li>
286
+ ))}
287
+ </ul>
275
288
  );
276
289
  }
@@ -1,10 +1,12 @@
1
1
  import { useState } from "react";
2
+ import type { ConversationMailboxMessage } from "./conversationOutbox";
2
3
  import { ArrowDownToLine } from "lucide-react";
3
4
 
4
5
  import type {
5
6
  ConversationTranscript,
6
7
  TranscriptViewSubagentPart,
7
8
  } from "../types";
9
+ import { EmptyTelemetry } from "../components/EmptyTelemetry";
8
10
  import { Button } from "../components/ui/button";
9
11
  import {
10
12
  Tooltip,
@@ -20,7 +22,6 @@ import {
20
22
  usePinnedTranscriptBottom,
21
23
  } from "./transcriptBottomPinning";
22
24
  import type { TranscriptViewMode } from "./transcriptRenderModel";
23
- import { transcriptEmptyClass } from "./transcriptStyles";
24
25
  import { TranscriptSearchProvider } from "./transcriptSearch";
25
26
  import {
26
27
  TurnContextPanelProvider,
@@ -37,6 +38,8 @@ export function Transcript(props: {
37
38
  pinRequestVersion?: number;
38
39
  onLoadPreviousPage?: () => void;
39
40
  onSelectedEventSeqChange?(seq: number | undefined): void;
41
+ /** The reader's own sends to show at the end until history has them. */
42
+ pendingMessages?: readonly ConversationMailboxMessage[];
40
43
  responding?: boolean;
41
44
  search?: string;
42
45
  onSearchChange?(value: string): void;
@@ -86,9 +89,9 @@ export function Transcript(props: {
86
89
 
87
90
  if (!props.transcript) {
88
91
  return (
89
- <div className={transcriptEmptyClass()}>
92
+ <EmptyTelemetry>
90
93
  No transcript is available for this conversation.
91
- </div>
94
+ </EmptyTelemetry>
92
95
  );
93
96
  }
94
97
 
@@ -139,6 +142,7 @@ export function Transcript(props: {
139
142
  <ConversationTranscriptView
140
143
  onOpenSubagentTranscript={props.onOpenSubagentTranscript}
141
144
  conversation={props.transcript}
145
+ pendingMessages={props.pendingMessages}
142
146
  responding={props.responding ?? props.live ?? false}
143
147
  />
144
148
  </TurnContextPanelProvider>
@@ -0,0 +1,39 @@
1
+ import { formatUserInputAnswer } from "@sentry/junior/api/schema";
2
+
3
+ import { UserInputForm } from "../components/UserInputForm";
4
+ import { TranscriptText } from "./TranscriptText";
5
+ import type { OpenUserInputRequest } from "./userInputRequest";
6
+
7
+ /**
8
+ * Show Junior's open request above the composer. The answer goes as the
9
+ * reader's next message. Typing a reply also answers it.
10
+ */
11
+ export function UserInputRequestCard(props: {
12
+ onAnswer(answer: string): Promise<void>;
13
+ request: OpenUserInputRequest;
14
+ }) {
15
+ return (
16
+ <section
17
+ aria-label="Junior asks"
18
+ className="mb-2 rounded-lg border border-cyan-300/20 bg-cyan-300/[0.07] px-3 py-2.5 font-sans text-sm text-dashboard-text"
19
+ >
20
+ <div className="max-h-48 overflow-y-auto">
21
+ <TranscriptText
22
+ mentions={[]}
23
+ role="assistant"
24
+ text={props.request.question}
25
+ />
26
+ </div>
27
+ {/* Above the composer, a long form scrolls so the transcript stays visible. */}
28
+ <div className="max-h-96 overflow-y-auto">
29
+ <UserInputForm
30
+ onSubmit={(answer) =>
31
+ props.onAnswer(formatUserInputAnswer(props.request, answer))
32
+ }
33
+ request={props.request}
34
+ requestId={props.request.toolCallId}
35
+ />
36
+ </div>
37
+ </section>
38
+ );
39
+ }