@sentry/junior-dashboard 0.267.0 → 0.269.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 (59) hide show
  1. package/README.md +7 -105
  2. package/dist/app.js +47 -8
  3. package/dist/client.js +74 -78
  4. package/dist/index.js +47 -8
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +4 -4
  7. package/src/api/schema.ts +2 -1
  8. package/src/app.ts +11 -7
  9. package/src/client/App.tsx +20 -4
  10. package/src/client/api.ts +4 -19
  11. package/src/client/components/AGENTS.md +65 -0
  12. package/src/client/components/ActorAvatar.tsx +1 -1
  13. package/src/client/components/ParticipantAvatarStack.tsx +3 -1
  14. package/src/client/components/ProfileMenu.tsx +88 -29
  15. package/src/client/components/UserInputForm.tsx +283 -0
  16. package/src/client/components/charts/AGENTS.md +15 -0
  17. package/src/client/components/layout/DashboardHeader.tsx +13 -0
  18. package/src/client/components/ui/button.tsx +11 -1
  19. package/src/client/conversations/AGENTS.md +42 -0
  20. package/src/client/conversations/ConversationHomeList.tsx +6 -0
  21. package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
  22. package/src/client/conversations/ConversationPage.tsx +4 -27
  23. package/src/client/conversations/ConversationSidebar.tsx +6 -0
  24. package/src/client/conversations/ConversationWorkspace.tsx +4 -0
  25. package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
  26. package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
  27. package/src/client/conversations/TranscriptMessageView.tsx +3 -0
  28. package/src/client/conversations/TranscriptToolView.tsx +13 -1
  29. package/src/client/conversations/TranscriptTurnContextView.tsx +142 -144
  30. package/src/client/conversations/turnContext.ts +15 -0
  31. package/src/client/format.ts +28 -28
  32. package/src/client/pages/SettingsPage.tsx +103 -3
  33. package/src/client/pages/automations/AGENTS.md +34 -0
  34. package/src/client/pages/automations/AutomationActions.tsx +18 -20
  35. package/src/client/pages/automations/AutomationEditor.tsx +3 -4
  36. package/src/client/pages/automations/AutomationOverview.tsx +3 -3
  37. package/src/client/pages/automations/AutomationStatus.tsx +2 -2
  38. package/src/client/pages/code/RepositorySections.tsx +1 -0
  39. package/src/client/pages/code/RepositoryTabs.tsx +3 -0
  40. package/src/client/pages/system/AGENTS.md +12 -0
  41. package/src/client/primaryNav.ts +15 -2
  42. package/src/client/review-desk/ConversationReviewBar.tsx +68 -0
  43. package/src/client/review-desk/ReviewDeskPage.tsx +297 -0
  44. package/src/client/review-desk/ReviewItemActions.tsx +165 -0
  45. package/src/client/review-desk/ReviewItemCard.tsx +47 -0
  46. package/src/client/review-desk/ReviewItemDetail.tsx +123 -0
  47. package/src/client/review-desk/queries.ts +173 -0
  48. package/src/client/review-desk/reviewItemFormat.ts +61 -0
  49. package/src/mock-reporting/fixtures.ts +10 -0
  50. package/src/mock-reporting/routes.ts +20 -0
  51. package/src/shell.ts +3 -0
  52. package/src/tailwind.css +1 -1
  53. package/src/client/components/README.md +0 -215
  54. package/src/client/components/charts/README.md +0 -38
  55. package/src/client/conversations/README.md +0 -159
  56. package/src/client/conversations/UserInputRequestCard.tsx +0 -244
  57. package/src/client/conversations/userInputRequest.ts +0 -56
  58. package/src/client/pages/automations/README.md +0 -97
  59. package/src/client/pages/system/README.md +0 -23
@@ -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}
@@ -9,6 +9,7 @@ import { memo, type ReactNode } from "react";
9
9
 
10
10
  import {
11
11
  formatMessageTimestamp,
12
+ transcriptMessageActor,
12
13
  transcriptMessageActorLabel,
13
14
  transcriptRoleKind,
14
15
  } from "../format";
@@ -130,8 +131,10 @@ function messageAction(
130
131
  if (message.role === "user" && message.contexts?.length) {
131
132
  return (
132
133
  <TranscriptTurnContextView
134
+ actor={transcriptMessageActor(conversation, message)}
133
135
  contexts={message.contexts}
134
136
  messageId={message.messageId}
137
+ timestamp={message.timestamp}
135
138
  />
136
139
  );
137
140
  }
@@ -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,10 +8,12 @@ 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";
15
+ import { participantName } from "../components/ParticipantAvatarStack";
12
16
  import { Button } from "../components/ui/button";
13
- import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";
14
17
  import { Drawer } from "../components/Drawer";
15
18
  import {
16
19
  Tooltip,
@@ -18,18 +21,26 @@ import {
18
21
  TooltipTrigger,
19
22
  } from "../components/ui/tooltip";
20
23
  import type { TranscriptViewTurnContext } from "../types";
21
- import { formatMessageTimestamp, formatTime } from "../format";
22
- import { memoryRecallContent, type MemoryRecallContent } from "./turnContext";
23
- 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";
24
32
  import { HighlightText } from "./transcriptSearch";
25
- import { TranscriptSummaryTrigger } from "./TranscriptSummary";
26
33
 
27
- /** The turn context that a page panel shows, keyed by its message. */
28
- export type TurnContextSelection = {
34
+ /** What one turn context view shows: who spoke, when, and the context. */
35
+ export type TurnContext = {
36
+ actor?: ActorIdentity;
29
37
  contexts: TranscriptViewTurnContext[];
30
- id: string;
38
+ timestamp?: number;
31
39
  };
32
40
 
41
+ /** The turn context that a page panel shows, keyed by its message. */
42
+ export type TurnContextSelection = TurnContext & { id: string };
43
+
33
44
  /** Let the page that owns a panel show turn context there. */
34
45
  export type TurnContextPanel = {
35
46
  openId?: string;
@@ -61,10 +72,9 @@ export function TurnContextPanelProvider(props: {
61
72
  * keys context by `messageId`, so every control for one message, including a
62
73
  * remounted one, shows the same open state.
63
74
  */
64
- export function TranscriptTurnContextView(props: {
65
- contexts: TranscriptViewTurnContext[];
66
- messageId?: string;
67
- }) {
75
+ export function TranscriptTurnContextView(
76
+ props: TurnContext & { messageId?: string },
77
+ ) {
68
78
  const panel = useContext(TurnContextPanelContext);
69
79
  const [open, setOpen] = useState(false);
70
80
  const instanceId = useId();
@@ -90,7 +100,12 @@ export function TranscriptTurnContextView(props: {
90
100
  data-turn-context-id={id}
91
101
  onClick={() => {
92
102
  if (!panel) return setOpen(true);
93
- panel.onToggle({ contexts: props.contexts, id });
103
+ panel.onToggle({
104
+ actor: props.actor,
105
+ contexts: props.contexts,
106
+ id,
107
+ timestamp: props.timestamp,
108
+ });
94
109
  }}
95
110
  size="icon"
96
111
  >
@@ -117,7 +132,11 @@ export function TranscriptTurnContextView(props: {
117
132
  openKey={instanceId}
118
133
  titleId={instanceId}
119
134
  >
120
- <TurnContextList contexts={props.contexts} />
135
+ <TurnContextBody
136
+ actor={props.actor}
137
+ contexts={props.contexts}
138
+ timestamp={props.timestamp}
139
+ />
121
140
  </Drawer>,
122
141
  document.body,
123
142
  )
@@ -127,10 +146,7 @@ export function TranscriptTurnContextView(props: {
127
146
  }
128
147
 
129
148
  /** Show turn context in the page panel: an aside, or a sheet on small screens. */
130
- export function TurnContextDetails(props: {
131
- contexts: TranscriptViewTurnContext[];
132
- onClose(): void;
133
- }) {
149
+ export function TurnContextDetails(props: TurnContext & { onClose(): void }) {
134
150
  return (
135
151
  <DetailsPanel
136
152
  label="Turn context"
@@ -141,151 +157,133 @@ export function TurnContextDetails(props: {
141
157
  </h2>
142
158
  }
143
159
  >
144
- <div className="px-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
145
- <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
+ />
146
166
  </div>
147
167
  </DetailsPanel>
148
168
  );
149
169
  }
150
170
 
151
- function TurnContextList(props: { contexts: TranscriptViewTurnContext[] }) {
152
- return props.contexts.map((context, index) => (
153
- <TurnContext
154
- context={context}
155
- key={`${context.pluginName}:${context.kind}:${context.version}:${index}`}
156
- />
157
- ));
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
+ );
158
191
  }
159
192
 
160
- function TurnContext(props: { context: TranscriptViewTurnContext }) {
161
- const memory = memoryRecallContent(props.context);
162
-
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);
163
201
  return (
164
- <section className="border-b border-dashboard-border-strong py-5 last:border-b-0">
165
- <div className="mb-4 flex items-start justify-between gap-4">
166
- <div className="flex min-w-0 items-center gap-2.5">
167
- {memory ? (
168
- <Brain
169
- aria-hidden="true"
170
- className="shrink-0 text-dashboard-accent-text/60"
171
- size={15}
172
- />
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>
173
216
  ) : (
174
- <Braces
175
- aria-hidden="true"
176
- className="shrink-0 text-dashboard-accent-text/60"
177
- size={15}
178
- />
217
+ <span className="min-w-0 truncate text-sm font-medium text-dashboard-text">
218
+ {name}
219
+ </span>
179
220
  )}
180
- <h3 className="m-0 truncate text-sm font-semibold text-dashboard-text">
181
- {memory ? "Recalled memories" : props.context.pluginName}
182
- </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}
183
229
  </div>
184
- <span className="shrink-0 text-xs text-dashboard-text-muted">
185
- {props.context.kind} · v{props.context.version}
186
- </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}
187
238
  </div>
188
-
189
- {memory ? (
190
- <MemoryRecall
191
- loadedAt={props.context.loadedAt}
192
- memories={memory.memories}
193
- />
194
- ) : (
195
- <GenericContext context={props.context} />
196
- )}
197
239
  </section>
198
240
  );
199
241
  }
200
242
 
201
- function MemoryRecall(props: {
202
- loadedAt: string;
203
- memories: MemoryRecallContent["memories"];
204
- }) {
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
+ }
205
262
  return (
206
- <div className="overflow-hidden rounded-lg border border-dashboard-border-strong">
207
- {props.memories.map((memory, index) => (
208
- <Collapsible
209
- className="border-t border-dashboard-border-strong first:border-t-0"
210
- key={memory.id}
211
- >
212
- <TranscriptSummaryTrigger className="group flex items-start gap-2.5">
213
- <ChevronRight
214
- aria-hidden="true"
215
- className={cn(
216
- dashboardDisclosureChevronClass,
217
- "mt-0.5 text-dashboard-text-muted",
218
- )}
219
- />
220
- <span className="min-w-0 flex-1">
221
- <span className="flex items-center justify-between gap-3">
222
- <span className="text-xs font-medium text-dashboard-text-muted">
223
- Memory {index + 1}
224
- </span>
225
- <span className="shrink-0 text-xs text-dashboard-text-muted">
226
- {memory.kind} · {memory.scope}
227
- </span>
228
- </span>
229
- <span className="mt-1 block truncate text-sm text-dashboard-text-muted">
230
- <HighlightText text={memory.content} />
231
- </span>
232
- </span>
233
- </TranscriptSummaryTrigger>
234
-
235
- <CollapsibleContent hiddenUntilFound>
236
- <div className="border-t border-dashboard-border bg-dashboard-fill-faint px-4 py-4">
237
- <div className="whitespace-pre-wrap text-sm leading-6 text-dashboard-text">
238
- <HighlightText text={memory.content} />
239
- </div>
240
-
241
- <dl className="mt-4 grid gap-2 text-xs">
242
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
243
- <dt className="text-dashboard-text-muted">Memory ID</dt>
244
- <dd className="m-0 break-all font-mono text-dashboard-text-muted">
245
- <HighlightText text={memory.id} />
246
- </dd>
247
- </div>
248
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
249
- <dt className="text-dashboard-text-muted">Observed</dt>
250
- <dd className="m-0 text-dashboard-text-muted">
251
- {formatTime(new Date(memory.observedAtMs).toISOString())}
252
- </dd>
253
- </div>
254
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
255
- <dt className="text-dashboard-text-muted">Scope</dt>
256
- <dd className="m-0 text-dashboard-text-muted">
257
- {memory.scope}
258
- </dd>
259
- </div>
260
- <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-start gap-3">
261
- <dt className="text-dashboard-text-muted">Kind</dt>
262
- <dd className="m-0 text-dashboard-text-muted">
263
- {memory.kind}
264
- </dd>
265
- </div>
266
- </dl>
267
- </div>
268
- </CollapsibleContent>
269
- </Collapsible>
270
- ))}
271
- <p className="m-0 border-t border-dashboard-border-strong px-3 py-2 text-xs text-dashboard-text-muted">
272
- {props.memories.length}{" "}
273
- {props.memories.length === 1 ? "memory" : "memories"} · Loaded{" "}
274
- {formatMessageTimestamp(Date.parse(props.loadedAt))}
275
- </p>
276
- </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>
277
270
  );
278
271
  }
279
272
 
280
- 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"] }) {
281
275
  return (
282
- <div>
283
- <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">
284
- <HighlightText text={JSON.stringify(props.context.content, null, 2)} />
285
- </pre>
286
- <p className="m-0 mt-3 text-xs text-dashboard-text-muted">
287
- Loaded {formatMessageTimestamp(Date.parse(props.context.loadedAt))}
288
- </p>
289
- </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>
290
288
  );
291
289
  }
@@ -33,3 +33,18 @@ export function memoryRecallContent(context: TranscriptViewTurnContext) {
33
33
  const parsed = memoryRecallContentSchema.safeParse(context.content);
34
34
  return parsed.success ? parsed.data : undefined;
35
35
  }
36
+
37
+ const userCustomizationContentSchema = z.object({ text: z.string() }).strict();
38
+
39
+ /** Parse the saved user customization that the runtime added to one Turn. */
40
+ export function userCustomizationContent(context: TranscriptViewTurnContext) {
41
+ if (
42
+ context.pluginName !== "junior" ||
43
+ context.kind !== "user_customization" ||
44
+ context.version !== 1
45
+ ) {
46
+ return undefined;
47
+ }
48
+ const parsed = userCustomizationContentSchema.safeParse(context.content);
49
+ return parsed.success ? parsed.data : undefined;
50
+ }
@@ -19,8 +19,8 @@ import { getDashboardAgentName } from "./agentName";
19
19
  import { formatDuration } from "./components/Duration";
20
20
  import { conversationTranscriptMessages } from "./conversations/eventTranscript";
21
21
 
22
- /** Detect the viewer's own local timezone as the default display timezone. */
23
- function browserTimeZone(): string {
22
+ /** Read the viewer's own browser timezone, used for absolute timestamps and charts. */
23
+ export function getDashboardTimeZone(): string {
24
24
  try {
25
25
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
26
26
  } catch {
@@ -28,22 +28,6 @@ function browserTimeZone(): string {
28
28
  }
29
29
  }
30
30
 
31
- let dashboardTimeZone = browserTimeZone();
32
-
33
- /** Apply an operator-configured timezone override from the config API, if any. Otherwise the viewer's own browser timezone is used. */
34
- export function setDashboardTimeZone(timeZone: string | undefined): void {
35
- dashboardTimeZone = timeZone || browserTimeZone();
36
- }
37
-
38
- /** Read the dashboard display timezone used for absolute timestamps and charts. */
39
- export function getDashboardTimeZone(): string {
40
- return dashboardTimeZone;
41
- }
42
-
43
- function displayTimeZone(): string {
44
- return dashboardTimeZone;
45
- }
46
-
47
31
  function isActiveSummary(summary: ConversationSummaryReport): boolean {
48
32
  return summary.status === "active";
49
33
  }
@@ -69,7 +53,7 @@ function parseTime(value: string | undefined): number | null {
69
53
  return Number.isFinite(time) ? time : null;
70
54
  }
71
55
 
72
- /** Format absolute dashboard timestamps in the configured display timezone. */
56
+ /** Format absolute dashboard timestamps in the viewer's browser timezone. */
73
57
  export function formatTime(
74
58
  value: string | undefined,
75
59
  options?: Intl.DateTimeFormatOptions,
@@ -78,7 +62,7 @@ export function formatTime(
78
62
  if (time == null) return "none";
79
63
  return new Date(time).toLocaleString(undefined, {
80
64
  ...options,
81
- timeZone: displayTimeZone(),
65
+ timeZone: getDashboardTimeZone(),
82
66
  });
83
67
  }
84
68
 
@@ -166,10 +150,10 @@ export function formatMessageTimestamp(
166
150
  ? date.toLocaleString(undefined, {
167
151
  dateStyle: "medium",
168
152
  timeStyle: "medium",
169
- timeZone: displayTimeZone(),
153
+ timeZone: getDashboardTimeZone(),
170
154
  })
171
155
  : date.toLocaleTimeString(undefined, {
172
- timeZone: displayTimeZone(),
156
+ timeZone: getDashboardTimeZone(),
173
157
  });
174
158
  }
175
159
 
@@ -210,7 +194,7 @@ export function formatTranscriptTimestampDetails(value: number): {
210
194
  return {
211
195
  local: date.toLocaleString(undefined, {
212
196
  ...options,
213
- timeZone: displayTimeZone(),
197
+ timeZone: getDashboardTimeZone(),
214
198
  }),
215
199
  utc: date.toLocaleString(undefined, { ...options, timeZone: "UTC" }),
216
200
  };
@@ -360,6 +344,21 @@ export type MessageSummary = {
360
344
  total: number;
361
345
  };
362
346
 
347
+ /**
348
+ * Return the person who wrote one transcript user message. Child agent input
349
+ * has no person. Other messages fall back to the conversation actor.
350
+ */
351
+ export function transcriptMessageActor(
352
+ conversation: ConversationTranscript,
353
+ message: TranscriptViewMessage,
354
+ ): ActorIdentity | undefined {
355
+ if (transcriptRoleKind(message.role) !== "user") return undefined;
356
+ if (message.agentInvocationId) return undefined;
357
+ if (actorLabel(message.actorIdentity)) return message.actorIdentity;
358
+ if (actorLabel(conversation.actorIdentity)) return conversation.actorIdentity;
359
+ return undefined;
360
+ }
361
+
363
362
  /** Return the best available actor label for one transcript message. */
364
363
  export function transcriptMessageActorLabel(
365
364
  conversation: ConversationTranscript,
@@ -371,11 +370,7 @@ export function transcriptMessageActorLabel(
371
370
  // The conversation actor started the child agent but did not write its
372
371
  // input, so do not fall back to that actor here.
373
372
  if (message.agentInvocationId) return "Agent invocation";
374
- return (
375
- actorLabel(message.actorIdentity) ??
376
- actorLabel(conversation.actorIdentity) ??
377
- "User"
378
- );
373
+ return actorLabel(transcriptMessageActor(conversation, message)) ?? "User";
379
374
  }
380
375
  if (kind === "system") return "System";
381
376
  if (kind === "tool") return "Tool";
@@ -786,6 +781,11 @@ export function peoplePath(email: string): string {
786
781
  return `/people/${encodeURIComponent(email)}`;
787
782
  }
788
783
 
784
+ /** Build the dashboard route for one memory's details. */
785
+ export function memoryPath(memoryId: string): string {
786
+ return `/memories/${encodeURIComponent(memoryId)}`;
787
+ }
788
+
789
789
  /** Build the canonical detail route for a persisted public location. */
790
790
  export function locationPath(locationId: string): string {
791
791
  return `/system/locations/${encodeURIComponent(locationId)}`;