@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.
- package/README.md +7 -105
- package/dist/app.js +47 -8
- package/dist/client.js +74 -78
- package/dist/index.js +47 -8
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +2 -1
- package/src/app.ts +11 -7
- package/src/client/App.tsx +20 -4
- package/src/client/api.ts +4 -19
- package/src/client/components/AGENTS.md +65 -0
- package/src/client/components/ActorAvatar.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +3 -1
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/button.tsx +11 -1
- package/src/client/conversations/AGENTS.md +42 -0
- package/src/client/conversations/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +4 -27
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +3 -0
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +142 -144
- package/src/client/conversations/turnContext.ts +15 -0
- package/src/client/format.ts +28 -28
- package/src/client/pages/SettingsPage.tsx +103 -3
- package/src/client/pages/automations/AGENTS.md +34 -0
- package/src/client/pages/automations/AutomationActions.tsx +18 -20
- package/src/client/pages/automations/AutomationEditor.tsx +3 -4
- package/src/client/pages/automations/AutomationOverview.tsx +3 -3
- package/src/client/pages/automations/AutomationStatus.tsx +2 -2
- package/src/client/pages/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/pages/system/AGENTS.md +12 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +68 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +297 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +123 -0
- package/src/client/review-desk/queries.ts +173 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/mock-reporting/fixtures.ts +10 -0
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -215
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/UserInputRequestCard.tsx +0 -244
- package/src/client/conversations/userInputRequest.ts +0 -56
- package/src/client/pages/automations/README.md +0 -97
- 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 {
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
22
|
-
import {
|
|
23
|
-
|
|
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
|
-
/**
|
|
28
|
-
export type
|
|
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
|
-
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
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({
|
|
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
|
-
<
|
|
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="
|
|
145
|
-
<
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
161
|
-
const
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
<
|
|
207
|
-
{props.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
273
|
+
// Each row links to the memory's page, which shows its details.
|
|
274
|
+
function MemoryRecall(props: { memories: MemoryRecallContent["memories"] }) {
|
|
281
275
|
return (
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
package/src/client/format.ts
CHANGED
|
@@ -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
|
-
/**
|
|
23
|
-
function
|
|
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
|
|
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:
|
|
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:
|
|
153
|
+
timeZone: getDashboardTimeZone(),
|
|
170
154
|
})
|
|
171
155
|
: date.toLocaleTimeString(undefined, {
|
|
172
|
-
timeZone:
|
|
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:
|
|
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)}`;
|