@sentry/junior-dashboard 0.258.0 → 0.260.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 +5 -3
- package/dist/app.js +88 -37
- package/dist/chunks/github-light-QHW3AEMR.js +1 -0
- package/dist/client.js +86 -50
- package/dist/index.js +88 -37
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +0 -1
- package/src/client/code.tsx +10 -4
- package/src/client/components/ProfileMenu.tsx +3 -0
- package/src/client/components/README.md +12 -2
- package/src/client/components/ThemeSwitcher.tsx +48 -0
- package/src/client/components/charts/ActivityChart.tsx +6 -7
- package/src/client/components/charts/InputCacheChart.tsx +1 -1
- package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
- package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
- package/src/client/components/charts/cache-input.ts +11 -3
- package/src/client/components/layout/DashboardHeader.tsx +2 -9
- package/src/client/conversations/ConversationBrief.tsx +35 -9
- package/src/client/conversations/ConversationHomeList.tsx +7 -1
- package/src/client/conversations/ConversationMeta.tsx +19 -7
- package/src/client/conversations/ConversationTranscript.tsx +8 -11
- package/src/client/conversations/EventDetails.tsx +9 -5
- package/src/client/conversations/README.md +6 -0
- package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
- package/src/client/conversations/TranscriptEventView.tsx +48 -9
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
- package/src/client/conversations/TranscriptText.tsx +28 -20
- package/src/client/conversations/conversationActivityPreview.ts +6 -0
- package/src/client/conversations/eventTranscript.ts +4 -1
- package/src/client/conversations/transcriptMentions.tsx +27 -10
- package/src/client/pages/admin/AdminPage.tsx +14 -5
- package/src/client/pages/admin/ServiceAccountsSection.tsx +231 -0
- package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
- package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
- package/src/client/pages/code/CodeActivityChart.tsx +4 -4
- package/src/client/pages/dev/ComponentsPage.tsx +7 -3
- package/src/client/pages/dev/ConversationFixture.tsx +29 -0
- package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
- package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
- package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
- package/src/client/pages/people/ContributionGrid.tsx +8 -8
- package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
- package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
- package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
- package/src/client/pages/system/PluginBarChart.tsx +12 -6
- package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
- package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
- package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
- package/src/client/pages/system/WorkspaceList.tsx +6 -1
- package/src/client/pages/system/WorkspacesPage.tsx +18 -12
- package/src/client/pages/system/workspaceDraft.ts +8 -0
- package/src/client/theme.ts +52 -0
- package/src/client/types.ts +8 -1
- package/src/mock-reporting/fixtures.ts +48 -35
- package/src/shell.ts +4 -1
- package/src/tailwind.css +109 -0
- package/src/theme.ts +49 -0
- package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
|
@@ -6,8 +6,18 @@ the policy. `pnpm dashboard-style:check` enforces the token rules below.
|
|
|
6
6
|
|
|
7
7
|
## Foundations
|
|
8
8
|
|
|
9
|
-
Tokens live in the `@theme` block of `src/tailwind.css`.
|
|
10
|
-
|
|
9
|
+
Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
|
|
10
|
+
token so each theme can redefine it.
|
|
11
|
+
|
|
12
|
+
- **Themes.** Dark is the default. The light theme redefines every token under
|
|
13
|
+
`:root[data-theme="light"]`. It also mirrors the light status and chart
|
|
14
|
+
shades (50-400) to their dark pair, so `text-rose-300` stays readable on the
|
|
15
|
+
light canvas. The head script in `src/theme.ts` sets `data-theme` before first
|
|
16
|
+
paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
|
|
17
|
+
the choice for this browser. Use the `light:` variant only for one-off values
|
|
18
|
+
that no token covers, such as provider brand colors.
|
|
19
|
+
- **Charts.** Use `var(--color-*)` for series colors in SVG attributes, not hex
|
|
20
|
+
values, so they follow the theme.
|
|
11
21
|
|
|
12
22
|
- **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
|
|
13
23
|
`bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Monitor, Moon, Sun, type LucideIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { DashboardThemeChoice } from "../../theme";
|
|
4
|
+
import { cn } from "../styles";
|
|
5
|
+
import { useDashboardThemeChoice } from "../theme";
|
|
6
|
+
import { ToggleButton } from "./Button";
|
|
7
|
+
|
|
8
|
+
const themeChoices = [
|
|
9
|
+
{ Icon: Monitor, label: "System", value: "system" },
|
|
10
|
+
{ Icon: Sun, label: "Light", value: "light" },
|
|
11
|
+
{ Icon: Moon, label: "Dark", value: "dark" },
|
|
12
|
+
] as const satisfies ReadonlyArray<{
|
|
13
|
+
Icon: LucideIcon;
|
|
14
|
+
label: string;
|
|
15
|
+
value: DashboardThemeChoice;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/** Choose the dashboard theme for this browser: system, light, or dark. */
|
|
19
|
+
export function ThemeSwitcher(props: { className?: string }) {
|
|
20
|
+
const [choice, setChoice] = useDashboardThemeChoice();
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn("flex items-center justify-between gap-3", props.className)}
|
|
24
|
+
>
|
|
25
|
+
<span className="font-sans text-sm font-semibold text-dashboard-text">
|
|
26
|
+
Theme
|
|
27
|
+
</span>
|
|
28
|
+
<div
|
|
29
|
+
aria-label="Theme"
|
|
30
|
+
className="flex gap-0.5 rounded-lg border border-dashboard-border p-0.5"
|
|
31
|
+
role="group"
|
|
32
|
+
>
|
|
33
|
+
{themeChoices.map(({ Icon, label, value }) => (
|
|
34
|
+
<ToggleButton
|
|
35
|
+
aria-label={`${label} theme`}
|
|
36
|
+
key={value}
|
|
37
|
+
onClick={() => setChoice(value)}
|
|
38
|
+
pressed={choice === value}
|
|
39
|
+
title={label}
|
|
40
|
+
variant="segment"
|
|
41
|
+
>
|
|
42
|
+
<Icon aria-hidden="true" size={14} strokeWidth={2} />
|
|
43
|
+
</ToggleButton>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -248,7 +248,7 @@ export function ActivityChartGrid(props: {
|
|
|
248
248
|
return (
|
|
249
249
|
<g key={ratio}>
|
|
250
250
|
<line
|
|
251
|
-
stroke="
|
|
251
|
+
stroke="var(--color-dashboard-border-subtle)"
|
|
252
252
|
strokeDasharray="3 5"
|
|
253
253
|
x1={props.layout.left}
|
|
254
254
|
x2={props.layout.width - props.layout.right}
|
|
@@ -332,9 +332,6 @@ export function activityChartAverage(values: readonly number[]): number {
|
|
|
332
332
|
export const chartAverageLabelClassName =
|
|
333
333
|
"fill-dashboard-text-solid font-sans tabular-nums leading-none";
|
|
334
334
|
|
|
335
|
-
/** Solid chip behind average labels so series colors never wash out the text. */
|
|
336
|
-
const chartAverageLabelChip = "rgba(9, 12, 14, 0.92)";
|
|
337
|
-
|
|
338
335
|
/**
|
|
339
336
|
* Opt-in horizontal average across the plotted buckets.
|
|
340
337
|
* Renders a dashed guide plus a compact "value / day" label on the right.
|
|
@@ -380,7 +377,7 @@ export function ActivityChartAverageLine(props: {
|
|
|
380
377
|
return (
|
|
381
378
|
<g aria-label={`average ${label}`} pointerEvents="none">
|
|
382
379
|
<line
|
|
383
|
-
stroke={props.stroke ?? "
|
|
380
|
+
stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
|
|
384
381
|
strokeDasharray="4 4"
|
|
385
382
|
strokeOpacity={0.85}
|
|
386
383
|
strokeWidth={1.25}
|
|
@@ -389,8 +386,10 @@ export function ActivityChartAverageLine(props: {
|
|
|
389
386
|
y1={y}
|
|
390
387
|
y2={y}
|
|
391
388
|
/>
|
|
389
|
+
{/* Solid chip so series colors never wash out the label text. */}
|
|
392
390
|
<rect
|
|
393
|
-
fill=
|
|
391
|
+
fill="var(--color-dashboard-bg-elevated)"
|
|
392
|
+
fillOpacity={0.92}
|
|
394
393
|
height={chipHeight}
|
|
395
394
|
rx={3}
|
|
396
395
|
width={chipWidth}
|
|
@@ -399,7 +398,7 @@ export function ActivityChartAverageLine(props: {
|
|
|
399
398
|
/>
|
|
400
399
|
<text
|
|
401
400
|
className={chartAverageLabelClassName}
|
|
402
|
-
fill="
|
|
401
|
+
fill="var(--color-dashboard-text-solid)"
|
|
403
402
|
fontSize={fontSize}
|
|
404
403
|
textAnchor="middle"
|
|
405
404
|
x={chipX + chipWidth / 2}
|
|
@@ -49,7 +49,7 @@ function compactDuration(value: number): string {
|
|
|
49
49
|
function tokenChart(): ChartConfig {
|
|
50
50
|
return {
|
|
51
51
|
axisFormat: formatCompactNumber,
|
|
52
|
-
color: "
|
|
52
|
+
color: "var(--color-cyan-400)",
|
|
53
53
|
format: formatCompactNumber,
|
|
54
54
|
metric: "tokens",
|
|
55
55
|
title: "Token usage",
|
|
@@ -61,7 +61,7 @@ function supportingCharts(): ChartConfig[] {
|
|
|
61
61
|
return [
|
|
62
62
|
{
|
|
63
63
|
axisFormat: compactCurrency,
|
|
64
|
-
color: "
|
|
64
|
+
color: "var(--color-amber-400)",
|
|
65
65
|
format: (value) => formatCostSummary({ total: value }),
|
|
66
66
|
metric: "costUsd",
|
|
67
67
|
title: "Model spend",
|
|
@@ -69,7 +69,7 @@ function supportingCharts(): ChartConfig[] {
|
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
axisFormat: compactDuration,
|
|
72
|
-
color: "
|
|
72
|
+
color: "var(--color-violet-400)",
|
|
73
73
|
format: formatDuration,
|
|
74
74
|
metric: "durationMs",
|
|
75
75
|
title: "Runtime",
|
|
@@ -14,7 +14,7 @@ import { Card } from "../layout/Card";
|
|
|
14
14
|
import { formatActivityChartAverage, formatCompactNumber } from "../../format";
|
|
15
15
|
import type { WorkspaceUsageDay } from "./workspaceUsage";
|
|
16
16
|
|
|
17
|
-
const USAGE_COLOR = "
|
|
17
|
+
const USAGE_COLOR = "var(--color-violet-400)";
|
|
18
18
|
|
|
19
19
|
/** Plot successful Workspace switches for one recipe over a trailing window. */
|
|
20
20
|
export function WorkspaceUsageChart(props: {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { ConversationMetricDay } from "@sentry/junior/api/schema";
|
|
2
2
|
|
|
3
3
|
export const CACHE_INPUT_SERIES = [
|
|
4
|
-
{
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
{
|
|
5
|
+
key: "cachedInputTokens",
|
|
6
|
+
label: "From cache",
|
|
7
|
+
color: "var(--color-cyan-400)",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: "cacheCreationTokens",
|
|
11
|
+
label: "Written to cache",
|
|
12
|
+
color: "var(--color-amber-400)",
|
|
13
|
+
},
|
|
14
|
+
{ key: "inputTokens", label: "Uncached", color: "var(--color-violet-400)" },
|
|
7
15
|
] as const;
|
|
8
16
|
|
|
9
17
|
export type CacheInput = Pick<
|
|
@@ -14,11 +14,7 @@ import {
|
|
|
14
14
|
MobileSecondaryNavigationSlot,
|
|
15
15
|
useRegisterOpenMobileNavigation,
|
|
16
16
|
} from "./DashboardChrome";
|
|
17
|
-
import {
|
|
18
|
-
cn,
|
|
19
|
-
dashboardContainerClass,
|
|
20
|
-
dashboardInteractiveTextClass,
|
|
21
|
-
} from "../../styles";
|
|
17
|
+
import { cn, dashboardInteractiveTextClass } from "../../styles";
|
|
22
18
|
|
|
23
19
|
export type DashboardHeaderNavItem = {
|
|
24
20
|
key: string;
|
|
@@ -37,7 +33,6 @@ const focusableSelector = [
|
|
|
37
33
|
|
|
38
34
|
/** Render the primary dashboard shell header and optional mobile nav sheet. */
|
|
39
35
|
export function DashboardHeader(props: {
|
|
40
|
-
compact?: boolean;
|
|
41
36
|
/** Back target used on mobile conversation detail. */
|
|
42
37
|
mobileBackTo?: string;
|
|
43
38
|
mobileNavigationOpen: boolean;
|
|
@@ -151,12 +146,10 @@ export function DashboardHeader(props: {
|
|
|
151
146
|
<header className="relative bg-dashboard-bg">
|
|
152
147
|
<div
|
|
153
148
|
className={cn(
|
|
154
|
-
|
|
155
|
-
"grid grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:gap-x-5 md:gap-y-3 md:px-4 md:py-4",
|
|
149
|
+
"grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:gap-x-5 md:gap-y-3 md:px-7 md:py-3",
|
|
156
150
|
props.profile
|
|
157
151
|
? "md:grid-cols-[auto_minmax(0,1fr)_auto]"
|
|
158
152
|
: "md:grid-cols-[auto_minmax(0,1fr)]",
|
|
159
|
-
props.compact ? "md:px-7 md:py-3" : "md:px-8",
|
|
160
153
|
)}
|
|
161
154
|
>
|
|
162
155
|
{conversationMode && props.mobileBackTo ? (
|
|
@@ -5,6 +5,7 @@ import type { ConversationDetailReport } from "@sentry/junior/api/schema";
|
|
|
5
5
|
import { Detail, DetailList } from "../components/DetailList";
|
|
6
6
|
import { StatusChip, type StatusChipTone } from "../components/StatusChip";
|
|
7
7
|
import { formatTime } from "../format";
|
|
8
|
+
import { MentionPeopleProvider, renderMentionText } from "./transcriptMentions";
|
|
8
9
|
|
|
9
10
|
type ConversationBriefReport = NonNullable<ConversationDetailReport["brief"]>;
|
|
10
11
|
|
|
@@ -24,13 +25,24 @@ const outcomeTones: Record<BriefContent["outcome"]["status"], StatusChipTone> =
|
|
|
24
25
|
export function ConversationBrief(props: {
|
|
25
26
|
brief: ConversationBriefReport;
|
|
26
27
|
variant?: "summary" | "full";
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<MentionPeopleProvider mentions={props.brief.mentions ?? []}>
|
|
31
|
+
<BriefContentView brief={props.brief} variant={props.variant} />
|
|
32
|
+
</MentionPeopleProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function BriefContentView(props: {
|
|
37
|
+
brief: ConversationBriefReport;
|
|
38
|
+
variant?: "summary" | "full";
|
|
27
39
|
}) {
|
|
28
40
|
const { content } = props.brief;
|
|
29
41
|
if (props.variant === "summary") {
|
|
30
42
|
return (
|
|
31
43
|
<div className="grid min-w-0 gap-4">
|
|
32
44
|
<p className="m-0 text-sm leading-relaxed text-dashboard-text">
|
|
33
|
-
{content.summary}
|
|
45
|
+
<BriefText text={content.summary} />
|
|
34
46
|
</p>
|
|
35
47
|
{content.openDecisions.length > 0 ? (
|
|
36
48
|
<div className="grid gap-2">
|
|
@@ -58,12 +70,16 @@ export function ConversationBrief(props: {
|
|
|
58
70
|
</span>
|
|
59
71
|
</div>
|
|
60
72
|
<p className="m-0 text-sm leading-relaxed text-dashboard-text">
|
|
61
|
-
{content.summary}
|
|
73
|
+
<BriefText text={content.summary} />
|
|
62
74
|
</p>
|
|
63
75
|
<BriefSection title="Intent & outcome">
|
|
64
76
|
<DetailList>
|
|
65
|
-
<Detail label="Intent">
|
|
66
|
-
|
|
77
|
+
<Detail label="Intent">
|
|
78
|
+
<BriefText text={content.intent} />
|
|
79
|
+
</Detail>
|
|
80
|
+
<Detail label="Outcome">
|
|
81
|
+
<BriefText text={content.outcome.text} />
|
|
82
|
+
</Detail>
|
|
67
83
|
</DetailList>
|
|
68
84
|
</BriefSection>
|
|
69
85
|
{content.decisions.length || content.openDecisions.length ? (
|
|
@@ -169,11 +185,13 @@ function BriefDecisions(props: { decisions: BriefContent["decisions"] }) {
|
|
|
169
185
|
>
|
|
170
186
|
{decision.kind}
|
|
171
187
|
</StatusChip>
|
|
172
|
-
<span className="min-w-0">
|
|
188
|
+
<span className="min-w-0">
|
|
189
|
+
<BriefText text={decision.text} />
|
|
190
|
+
</span>
|
|
173
191
|
</div>
|
|
174
192
|
{decision.by ? (
|
|
175
193
|
<div className="mt-0.5 text-xs text-dashboard-text-muted">
|
|
176
|
-
by · {decision.by}
|
|
194
|
+
by · <BriefText text={decision.by} />
|
|
177
195
|
</div>
|
|
178
196
|
) : null}
|
|
179
197
|
</li>
|
|
@@ -189,10 +207,12 @@ function BriefOpenDecisions(props: {
|
|
|
189
207
|
<ul className="m-0 grid list-none gap-2 p-0">
|
|
190
208
|
{props.decisions.map((decision, index) => (
|
|
191
209
|
<li key={`${decision.text}:${index}`}>
|
|
192
|
-
<div>
|
|
210
|
+
<div>
|
|
211
|
+
<BriefText text={decision.text} />
|
|
212
|
+
</div>
|
|
193
213
|
{decision.owner ? (
|
|
194
214
|
<div className="mt-1 text-xs text-dashboard-text-muted">
|
|
195
|
-
Owner: {decision.owner}
|
|
215
|
+
Owner: <BriefText text={decision.owner} />
|
|
196
216
|
</div>
|
|
197
217
|
) : null}
|
|
198
218
|
</li>
|
|
@@ -205,12 +225,18 @@ function BriefList(props: { items: string[] }) {
|
|
|
205
225
|
return (
|
|
206
226
|
<ul className="my-0 grid gap-1.5 pl-4">
|
|
207
227
|
{props.items.map((item, index) => (
|
|
208
|
-
<li key={`${item}:${index}`}>
|
|
228
|
+
<li key={`${item}:${index}`}>
|
|
229
|
+
<BriefText text={item} />
|
|
230
|
+
</li>
|
|
209
231
|
))}
|
|
210
232
|
</ul>
|
|
211
233
|
);
|
|
212
234
|
}
|
|
213
235
|
|
|
236
|
+
function BriefText(props: { text: string }) {
|
|
237
|
+
return <>{renderMentionText(props.text, "brief")}</>;
|
|
238
|
+
}
|
|
239
|
+
|
|
214
240
|
function displayLabel(value: string): string {
|
|
215
241
|
return value.replaceAll("_", " ");
|
|
216
242
|
}
|
|
@@ -22,6 +22,7 @@ import { ConversationArchiveNotices } from "./ConversationArchiveNotices";
|
|
|
22
22
|
import { ConversationListStatusIcon } from "./ConversationListStatusIcon";
|
|
23
23
|
import { conversationPath } from "./conversationRoutes";
|
|
24
24
|
import { useArchiveConversation } from "./queries";
|
|
25
|
+
import { MentionText } from "./transcriptMentions";
|
|
25
26
|
import {
|
|
26
27
|
buildConversationSections,
|
|
27
28
|
type ConversationSection,
|
|
@@ -265,7 +266,12 @@ function ConversationCard(props: {
|
|
|
265
266
|
</div>
|
|
266
267
|
{conversation.activityPreview ? (
|
|
267
268
|
<p className="relative z-[1] m-0 line-clamp-2 font-sans text-sm leading-relaxed text-dashboard-text-subtle pointer-events-none">
|
|
268
|
-
|
|
269
|
+
<MentionText
|
|
270
|
+
mentions={conversation.activityPreview.mentions ?? []}
|
|
271
|
+
text={formatConversationActivityPreview(
|
|
272
|
+
conversation.activityPreview.text,
|
|
273
|
+
)}
|
|
274
|
+
/>
|
|
269
275
|
</p>
|
|
270
276
|
) : (
|
|
271
277
|
<p className="relative z-[1] m-0 font-sans text-sm leading-relaxed text-dashboard-text-muted pointer-events-none">
|
|
@@ -167,7 +167,7 @@ export function ConversationSidebarAnnotations(props: {
|
|
|
167
167
|
{useMobileFacepile ? (
|
|
168
168
|
<SidebarAnnotationIconFacepile
|
|
169
169
|
annotations={mobileFacepileAnnotations}
|
|
170
|
-
// Discs sit on the sidebar panel / row surface
|
|
170
|
+
// Discs sit on the sidebar panel / row surface.
|
|
171
171
|
cutoutColor="var(--color-dashboard-surface-panel)"
|
|
172
172
|
/>
|
|
173
173
|
) : (
|
|
@@ -343,21 +343,33 @@ type SidebarAnnotationIconName = NonNullable<
|
|
|
343
343
|
>;
|
|
344
344
|
|
|
345
345
|
const SIDEBAR_ICON_PRESENTATION = {
|
|
346
|
-
"circle-dot": {
|
|
346
|
+
"circle-dot": {
|
|
347
|
+
className: "text-[#3fb950] light:text-[#1a7f37]",
|
|
348
|
+
Icon: CircleDot,
|
|
349
|
+
label: "Open",
|
|
350
|
+
},
|
|
347
351
|
"circle-dashed": {
|
|
348
|
-
className: "text-[#8c959f]",
|
|
352
|
+
className: "text-[#8c959f] light:text-[#59636e]",
|
|
349
353
|
Icon: CircleDashed,
|
|
350
354
|
label: "Draft",
|
|
351
355
|
},
|
|
352
|
-
"circle-x": {
|
|
353
|
-
|
|
356
|
+
"circle-x": {
|
|
357
|
+
className: "text-[#f85149] light:text-[#cf222e]",
|
|
358
|
+
Icon: CircleX,
|
|
359
|
+
label: "Closed",
|
|
360
|
+
},
|
|
361
|
+
"git-merge": {
|
|
362
|
+
className: "text-[#a371f7] light:text-[#8250df]",
|
|
363
|
+
Icon: GitMerge,
|
|
364
|
+
label: "Merged",
|
|
365
|
+
},
|
|
354
366
|
"git-pull-request": {
|
|
355
|
-
className: "text-[#3fb950]",
|
|
367
|
+
className: "text-[#3fb950] light:text-[#1a7f37]",
|
|
356
368
|
Icon: GitPullRequest,
|
|
357
369
|
label: "Open pull request",
|
|
358
370
|
},
|
|
359
371
|
"triangle-alert": {
|
|
360
|
-
className: "text-[#d29922]",
|
|
372
|
+
className: "text-[#d29922] light:text-[#9a6700]",
|
|
361
373
|
Icon: TriangleAlert,
|
|
362
374
|
label: "Needs attention",
|
|
363
375
|
},
|
|
@@ -43,7 +43,6 @@ import { transcriptEmptyClass } from "./transcriptStyles";
|
|
|
43
43
|
import { entryMatchesSearch, useTranscriptSearch } from "./transcriptSearch";
|
|
44
44
|
import { TranscriptRow, TranscriptRows } from "./TranscriptRows";
|
|
45
45
|
import { TranscriptTimestampProvider } from "./TranscriptTimestamp";
|
|
46
|
-
import { TranscriptMentionPeopleProvider } from "./transcriptMentions";
|
|
47
46
|
|
|
48
47
|
type TranscriptEntry = ReturnType<typeof groupTranscriptMessages>[number];
|
|
49
48
|
type TranscriptContextEntry = Extract<TranscriptEntry, { kind: "context" }>;
|
|
@@ -86,16 +85,14 @@ export const ConversationTranscriptView = memo(
|
|
|
86
85
|
|
|
87
86
|
return (
|
|
88
87
|
<TranscriptTimestampProvider>
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</section>
|
|
98
|
-
</TranscriptMentionPeopleProvider>
|
|
88
|
+
<section className="min-w-0 pt-1">
|
|
89
|
+
<SegmentEvents
|
|
90
|
+
onOpenSubagentTranscript={props.onOpenSubagentTranscript}
|
|
91
|
+
conversation={props.conversation}
|
|
92
|
+
messages={messages}
|
|
93
|
+
responding={props.responding}
|
|
94
|
+
/>
|
|
95
|
+
</section>
|
|
99
96
|
</TranscriptTimestampProvider>
|
|
100
97
|
);
|
|
101
98
|
},
|
|
@@ -10,6 +10,7 @@ import { EventUsage } from "./EventUsage";
|
|
|
10
10
|
/** Present event content as readable sections, with the exact report available on demand. */
|
|
11
11
|
export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
12
12
|
const data = event.data;
|
|
13
|
+
const mentions = event.mentions ?? [];
|
|
13
14
|
const model = eventLogModel(event);
|
|
14
15
|
const { usage, ...callAttributes } = event.modelCall ?? {};
|
|
15
16
|
let content: ReactNode;
|
|
@@ -54,7 +55,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
54
55
|
) : null}
|
|
55
56
|
{summary ? (
|
|
56
57
|
<DetailSection title="Continuation summary">
|
|
57
|
-
<TranscriptText text={summary} />
|
|
58
|
+
<TranscriptText mentions={mentions} text={summary} />
|
|
58
59
|
</DetailSection>
|
|
59
60
|
) : null}
|
|
60
61
|
</>
|
|
@@ -70,7 +71,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
70
71
|
{redacted ? (
|
|
71
72
|
<RedactedMarker />
|
|
72
73
|
) : (
|
|
73
|
-
<TranscriptText text={text ?? ""} />
|
|
74
|
+
<TranscriptText mentions={mentions} text={text ?? ""} />
|
|
74
75
|
)}
|
|
75
76
|
</DetailSection>
|
|
76
77
|
</>
|
|
@@ -112,7 +113,7 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
112
113
|
{part.redacted ? (
|
|
113
114
|
<RedactedMarker />
|
|
114
115
|
) : (
|
|
115
|
-
<TranscriptText text={part.text ?? ""} />
|
|
116
|
+
<TranscriptText mentions={mentions} text={part.text ?? ""} />
|
|
116
117
|
)}
|
|
117
118
|
</DetailSection>
|
|
118
119
|
));
|
|
@@ -124,12 +125,15 @@ export function EventDetails({ event }: { event: ConversationReportEvent }) {
|
|
|
124
125
|
<DetailValue value={metadata} />
|
|
125
126
|
<DetailSection title={presentation.title}>
|
|
126
127
|
{presentation.preview ? (
|
|
127
|
-
<TranscriptText text={presentation.preview} />
|
|
128
|
+
<TranscriptText mentions={mentions} text={presentation.preview} />
|
|
128
129
|
) : null}
|
|
129
130
|
{presentation.details?.map((detail, index) => (
|
|
130
131
|
<DetailSection key={index} title={detail.title}>
|
|
131
132
|
{detail.description ? (
|
|
132
|
-
<TranscriptText
|
|
133
|
+
<TranscriptText
|
|
134
|
+
mentions={mentions}
|
|
135
|
+
text={detail.description}
|
|
136
|
+
/>
|
|
133
137
|
) : null}
|
|
134
138
|
{detail.content ? <DetailValue value={detail.content} /> : null}
|
|
135
139
|
{detail.metadata ? (
|
|
@@ -34,6 +34,12 @@ shared controls and overlays.
|
|
|
34
34
|
a person, so it stays collapsed behind "Show prompt".
|
|
35
35
|
- `TranscriptAttachments` owns wrapping for both message attachments and file
|
|
36
36
|
deliveries. `ImageAttachment` owns image previews.
|
|
37
|
+
- Display text keeps Slack tokens such as `<@U123>` and `<#C123|name>`. The
|
|
38
|
+
API resolves the people in that text. It returns them as `mentions` next to
|
|
39
|
+
the object that owns the text: an event, an activity preview, a Brief, or an
|
|
40
|
+
Automation. Render this text with `TranscriptText` or `MentionText`. Both
|
|
41
|
+
require `mentions`, so a caller cannot forget them. Do not resolve names in
|
|
42
|
+
the dashboard, and do not rewrite tokens into links on the server.
|
|
37
43
|
- `TranscriptTurnContextView` owns context content. The shared `Drawer` owns
|
|
38
44
|
dismissal, focus trapping, focus return, scroll locking, and panel layout.
|
|
39
45
|
|
|
@@ -44,7 +44,11 @@ export function TranscriptContextEventView(props: {
|
|
|
44
44
|
Continuation summary
|
|
45
45
|
</summary>
|
|
46
46
|
<div className="mt-1.5 whitespace-pre-wrap text-xs leading-relaxed text-dashboard-text-muted">
|
|
47
|
-
<TranscriptText
|
|
47
|
+
<TranscriptText
|
|
48
|
+
mentions={props.part.mentions ?? []}
|
|
49
|
+
role="system"
|
|
50
|
+
text={event.summary}
|
|
51
|
+
/>
|
|
48
52
|
</div>
|
|
49
53
|
</details>
|
|
50
54
|
) : null}
|
|
@@ -1,28 +1,67 @@
|
|
|
1
1
|
import { ChevronRight } from "lucide-react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MentionText } from "./transcriptMentions";
|
|
4
4
|
import { messageRawText } from "./transcriptRenderModel";
|
|
5
5
|
import { HighlightText } from "./transcriptSearch";
|
|
6
6
|
import { RedactedMarker } from "./TranscriptRedacted";
|
|
7
7
|
import type { TranscriptViewMessage } from "../types";
|
|
8
8
|
import { TranscriptSummary } from "./TranscriptSummary";
|
|
9
9
|
|
|
10
|
+
function EventTypeChip(props: { eventType: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<span className="break-all rounded-md border border-dashboard-border px-1.5 py-0.5 font-mono text-2xs leading-none text-dashboard-text-muted">
|
|
13
|
+
<HighlightText text={props.eventType} />
|
|
14
|
+
</span>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Heading for several Events that arrived in one message. */
|
|
19
|
+
function batchSummary(events: readonly { label?: string }[]): string {
|
|
20
|
+
// Name the resource only when every Event is about it.
|
|
21
|
+
const label = events[0]?.label;
|
|
22
|
+
const shared = label && events.every((event) => event.label === label);
|
|
23
|
+
return `${events.length} events${shared ? ` · ${label}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
/** Render an Event Message whose heading expands its full text. */
|
|
11
27
|
export function TranscriptEventView(props: { message: TranscriptViewMessage }) {
|
|
12
28
|
const text = messageRawText(props.message);
|
|
13
29
|
const redacted = props.message.parts.some(
|
|
14
30
|
(part) => part.type === "text" && part.redacted,
|
|
15
31
|
);
|
|
16
|
-
const
|
|
32
|
+
const events = props.message.events ?? [];
|
|
33
|
+
const mentions = props.message.mentions ?? [];
|
|
34
|
+
const isBatch = events.length > 1;
|
|
35
|
+
const summary = isBatch
|
|
36
|
+
? batchSummary(events)
|
|
37
|
+
: (props.message.trustedSummary ?? props.message.eventType ?? "");
|
|
17
38
|
const heading = (
|
|
18
|
-
<span className="flex min-
|
|
19
|
-
<span className="min-w-0
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{props.message.eventType ? (
|
|
23
|
-
<span className="break-all rounded-md border border-dashboard-border px-1.5 py-0.5 font-mono text-2xs leading-none text-dashboard-text-muted">
|
|
24
|
-
<HighlightText text={props.message.eventType} />
|
|
39
|
+
<span className="flex min-w-0 flex-col gap-1">
|
|
40
|
+
<span className="flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
41
|
+
<span className="min-w-0 break-words font-display text-sm font-semibold leading-6 text-dashboard-text">
|
|
42
|
+
<MentionText mentions={mentions} text={summary} />
|
|
25
43
|
</span>
|
|
44
|
+
{!isBatch && props.message.eventType ? (
|
|
45
|
+
<EventTypeChip eventType={props.message.eventType} />
|
|
46
|
+
) : null}
|
|
47
|
+
</span>
|
|
48
|
+
{isBatch ? (
|
|
49
|
+
<ul className="flex min-w-0 flex-col gap-1">
|
|
50
|
+
{events.map((event, index) => (
|
|
51
|
+
<li
|
|
52
|
+
key={index}
|
|
53
|
+
className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-5 text-dashboard-text-muted"
|
|
54
|
+
>
|
|
55
|
+
<span className="min-w-0 break-words">
|
|
56
|
+
<MentionText
|
|
57
|
+
mentions={mentions}
|
|
58
|
+
text={event.trustedSummary ?? event.eventType}
|
|
59
|
+
/>
|
|
60
|
+
</span>
|
|
61
|
+
<EventTypeChip eventType={event.eventType} />
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
26
65
|
) : null}
|
|
27
66
|
</span>
|
|
28
67
|
);
|
|
@@ -54,7 +54,12 @@ export const TranscriptMessageView = memo(
|
|
|
54
54
|
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-2">
|
|
55
55
|
{props.message.parts.map((part, index) =>
|
|
56
56
|
part.type === "text" ? (
|
|
57
|
-
<TranscriptText
|
|
57
|
+
<TranscriptText
|
|
58
|
+
key={index}
|
|
59
|
+
mentions={props.message.mentions ?? []}
|
|
60
|
+
role={role}
|
|
61
|
+
text={part.text ?? ""}
|
|
62
|
+
/>
|
|
58
63
|
) : null,
|
|
59
64
|
)}
|
|
60
65
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatMessageTimestamp } from "../format";
|
|
2
2
|
import type { TranscriptViewStructuredEventPart } from "../types";
|
|
3
|
+
import { MentionText } from "./transcriptMentions";
|
|
3
4
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
4
5
|
import { TranscriptSummary } from "./TranscriptSummary";
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ export function TranscriptStructuredEventView(props: {
|
|
|
14
15
|
const presentation = props.part.presentation;
|
|
15
16
|
const timestamp = formatMessageTimestamp(props.timestamp);
|
|
16
17
|
const details = presentation.details ?? [];
|
|
18
|
+
const mentions = props.part.mentions ?? [];
|
|
17
19
|
// The rail icon box is h-6. A leading-6 title shares its center line.
|
|
18
20
|
const surfaceClass = "min-w-0 font-sans text-xs";
|
|
19
21
|
// A lone detail often repeats the event title, such as "Loaded AGENTS.md"
|
|
@@ -32,7 +34,7 @@ export function TranscriptStructuredEventView(props: {
|
|
|
32
34
|
)}
|
|
33
35
|
{detail.description ? (
|
|
34
36
|
<div className="mt-1 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
|
|
35
|
-
<
|
|
37
|
+
<MentionText mentions={mentions} text={detail.description} />
|
|
36
38
|
</div>
|
|
37
39
|
) : null}
|
|
38
40
|
{detail.content ? (
|
|
@@ -64,7 +66,7 @@ export function TranscriptStructuredEventView(props: {
|
|
|
64
66
|
</div>
|
|
65
67
|
{presentation.preview ? (
|
|
66
68
|
<div className="mt-0.5 truncate text-xs text-dashboard-text-muted">
|
|
67
|
-
<
|
|
69
|
+
<MentionText mentions={mentions} text={presentation.preview} />
|
|
68
70
|
</div>
|
|
69
71
|
) : null}
|
|
70
72
|
</div>
|