@sentry/junior-dashboard 0.265.0 → 0.266.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/dist/app.js +2 -2
- package/dist/client.js +48 -48
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +10 -10
- package/src/client/components/README.md +17 -1
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +66 -0
- package/src/client/components/ui/empty.tsx +32 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +44 -29
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/PendingMailboxStack.tsx +5 -10
- package/src/client/conversations/README.md +3 -0
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +34 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +45 -38
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/mock-reporting/watches.ts +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { Loader2Icon } from "lucide-react";
|
|
3
|
+
import { cn } from "@dashboard/lib/utils";
|
|
4
|
+
|
|
5
|
+
/** Render a loading spinner. It stops spinning when the user prefers reduced motion. */
|
|
6
|
+
function Spinner({ className, ...props }: ComponentProps<"svg">) {
|
|
7
|
+
return (
|
|
8
|
+
<Loader2Icon
|
|
9
|
+
data-slot="spinner"
|
|
10
|
+
role="status"
|
|
11
|
+
aria-label="Loading"
|
|
12
|
+
className={cn(
|
|
13
|
+
"size-4 animate-spin motion-reduce:animate-none",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Spinner };
|
|
@@ -37,6 +37,19 @@ function TableBody({ className, ...props }: ComponentProps<"tbody">) {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function TableFooter({ className, ...props }: ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t border-dashboard-border font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function TableRow({ className, ...props }: ComponentProps<"tr">) {
|
|
41
54
|
return (
|
|
42
55
|
<tr
|
|
@@ -77,4 +90,12 @@ function TableCell({ className, ...props }: ComponentProps<"td">) {
|
|
|
77
90
|
);
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
export {
|
|
93
|
+
export {
|
|
94
|
+
Table,
|
|
95
|
+
TableHeader,
|
|
96
|
+
TableBody,
|
|
97
|
+
TableFooter,
|
|
98
|
+
TableHead,
|
|
99
|
+
TableRow,
|
|
100
|
+
TableCell,
|
|
101
|
+
};
|
|
@@ -132,7 +132,7 @@ function TooltipContent({
|
|
|
132
132
|
collisionPadding={8}
|
|
133
133
|
side={side}
|
|
134
134
|
sideOffset={sideOffset}
|
|
135
|
-
className="isolate z-50 data-anchor-hidden:
|
|
135
|
+
className="isolate z-50 data-anchor-hidden:invisible"
|
|
136
136
|
>
|
|
137
137
|
<TooltipPrimitive.Popup
|
|
138
138
|
data-slot="tooltip-content"
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
CircleDot,
|
|
8
8
|
CirclePause,
|
|
9
9
|
CircleX,
|
|
10
|
-
LoaderCircle,
|
|
11
10
|
LockKeyhole,
|
|
12
11
|
type LucideIcon,
|
|
13
12
|
} from "lucide-react";
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
} from "@sentry/junior/api/schema";
|
|
18
17
|
|
|
19
18
|
import type { StatusChipTone } from "../components/StatusChip";
|
|
19
|
+
import { Spinner } from "../components/ui/spinner";
|
|
20
20
|
import {
|
|
21
21
|
formatConversationCostTotal,
|
|
22
22
|
formatRuntime,
|
|
@@ -115,7 +115,6 @@ const statusIcons: Partial<Record<string, LucideIcon>> = {
|
|
|
115
115
|
error: CircleX,
|
|
116
116
|
failed: CircleX,
|
|
117
117
|
paused: CirclePause,
|
|
118
|
-
running: LoaderCircle,
|
|
119
118
|
};
|
|
120
119
|
|
|
121
120
|
const statusIconToneClass: Record<StatusChipTone, string> = {
|
|
@@ -129,7 +128,10 @@ const statusIconToneClass: Record<StatusChipTone, string> = {
|
|
|
129
128
|
|
|
130
129
|
/** Mark a child agent status with one icon and an audible label. */
|
|
131
130
|
function ChildAgentStatusIcon(props: { status: string }) {
|
|
132
|
-
const Icon =
|
|
131
|
+
const Icon =
|
|
132
|
+
props.status === "running"
|
|
133
|
+
? Spinner
|
|
134
|
+
: (statusIcons[props.status] ?? Circle);
|
|
133
135
|
return (
|
|
134
136
|
<span className="mt-px grid size-4 shrink-0 place-items-center">
|
|
135
137
|
<Icon
|
|
@@ -137,8 +139,6 @@ function ChildAgentStatusIcon(props: { status: string }) {
|
|
|
137
139
|
className={cn(
|
|
138
140
|
"size-3.5",
|
|
139
141
|
statusIconToneClass[subagentStatusTone(props.status)],
|
|
140
|
-
props.status === "running" &&
|
|
141
|
-
"animate-spin motion-reduce:animate-none",
|
|
142
142
|
)}
|
|
143
143
|
/>
|
|
144
144
|
<span className="sr-only">{props.status}</span>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { Ellipsis, Menu } from "lucide-react";
|
|
11
11
|
|
|
12
12
|
import { SearchInput } from "../components/SearchInput";
|
|
13
|
+
import { Button } from "../components/ui/button";
|
|
13
14
|
import { cn } from "../styles";
|
|
14
15
|
import {
|
|
15
16
|
MobileHeaderActionsPortal,
|
|
@@ -149,17 +150,17 @@ export function ConversationHeader(props: {
|
|
|
149
150
|
view={props.view}
|
|
150
151
|
/>
|
|
151
152
|
{openMobileNavigation ? (
|
|
152
|
-
<
|
|
153
|
-
className="mt-0.5
|
|
153
|
+
<Button
|
|
154
|
+
className="mt-0.5"
|
|
154
155
|
onClick={() => {
|
|
155
156
|
setMobileMenuOpen(false);
|
|
156
157
|
openMobileNavigation();
|
|
157
158
|
}}
|
|
158
|
-
|
|
159
|
+
size="menu"
|
|
159
160
|
>
|
|
160
161
|
<Menu aria-hidden="true" size={16} strokeWidth={2} />
|
|
161
162
|
App menu
|
|
162
|
-
</
|
|
163
|
+
</Button>
|
|
163
164
|
) : null}
|
|
164
165
|
</div>
|
|
165
166
|
) : null}
|
|
@@ -235,19 +235,16 @@ function MenuActionButton(props: {
|
|
|
235
235
|
pressed?: boolean;
|
|
236
236
|
}) {
|
|
237
237
|
return (
|
|
238
|
-
<
|
|
238
|
+
<Button
|
|
239
239
|
aria-label={props.label}
|
|
240
240
|
aria-pressed={props.pressed}
|
|
241
|
-
className={
|
|
242
|
-
"flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none",
|
|
243
|
-
props.pressed && "bg-dashboard-fill-strong",
|
|
244
|
-
)}
|
|
241
|
+
className={props.pressed ? "bg-dashboard-fill-strong" : undefined}
|
|
245
242
|
onClick={props.onClick}
|
|
246
|
-
|
|
243
|
+
size="menu"
|
|
247
244
|
>
|
|
248
245
|
{props.children}
|
|
249
246
|
<span>{props.label}</span>
|
|
250
|
-
</
|
|
247
|
+
</Button>
|
|
251
248
|
);
|
|
252
249
|
}
|
|
253
250
|
|
|
@@ -262,16 +259,15 @@ function ArchiveConversationButton(
|
|
|
262
259
|
const Icon = props.archived ? ArchiveRestore : Archive;
|
|
263
260
|
if (props.layout === "menu") {
|
|
264
261
|
return (
|
|
265
|
-
<
|
|
262
|
+
<Button
|
|
266
263
|
aria-label={label}
|
|
267
|
-
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
268
264
|
disabled={props.disabled}
|
|
269
265
|
onClick={props.onClick}
|
|
270
|
-
|
|
266
|
+
size="menu"
|
|
271
267
|
>
|
|
272
268
|
<Icon aria-hidden="true" size={16} strokeWidth={2} />
|
|
273
269
|
<span>{label}</span>
|
|
274
|
-
</
|
|
270
|
+
</Button>
|
|
275
271
|
);
|
|
276
272
|
}
|
|
277
273
|
return (
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./conversationSections";
|
|
20
20
|
import { EmptyTelemetry } from "../components/EmptyTelemetry";
|
|
21
21
|
import { SearchInput } from "../components/SearchInput";
|
|
22
|
+
import { Button } from "../components/ui/button";
|
|
22
23
|
import { Skeleton } from "../components/ui/skeleton";
|
|
23
24
|
import {
|
|
24
25
|
Tooltip,
|
|
@@ -112,14 +113,14 @@ export function ConversationSidebar(props: {
|
|
|
112
113
|
<Tooltip>
|
|
113
114
|
<TooltipTrigger
|
|
114
115
|
render={
|
|
115
|
-
<
|
|
116
|
+
<Button
|
|
116
117
|
aria-label="New conversation"
|
|
117
|
-
className="
|
|
118
|
+
className="size-7"
|
|
118
119
|
onClick={props.onNewConversation}
|
|
119
|
-
|
|
120
|
+
size="icon"
|
|
120
121
|
>
|
|
121
122
|
<SquarePen aria-hidden="true" size={15} />
|
|
122
|
-
</
|
|
123
|
+
</Button>
|
|
123
124
|
}
|
|
124
125
|
/>
|
|
125
126
|
<TooltipContent size="compact">New conversation</TooltipContent>
|
|
@@ -13,6 +13,16 @@ import {
|
|
|
13
13
|
totalConversationCost,
|
|
14
14
|
} from "../format";
|
|
15
15
|
import { ShimmerText } from "../components/ShimmerText";
|
|
16
|
+
import {
|
|
17
|
+
Table,
|
|
18
|
+
TableBody,
|
|
19
|
+
TableCell,
|
|
20
|
+
TableFooter,
|
|
21
|
+
TableHead,
|
|
22
|
+
TableHeader,
|
|
23
|
+
TableRow,
|
|
24
|
+
} from "../components/ui/table";
|
|
25
|
+
import { cn } from "../styles";
|
|
16
26
|
import type { Conversation } from "../types";
|
|
17
27
|
import { liveModelId } from "./ConversationMeta";
|
|
18
28
|
import {
|
|
@@ -70,7 +80,7 @@ function tokenTypeParts(
|
|
|
70
80
|
return parts.length ? parts : undefined;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
const CELL = "text-right align-top font-sans tabular-nums";
|
|
83
|
+
const CELL = "px-0 text-right align-top font-sans tabular-nums";
|
|
74
84
|
|
|
75
85
|
/**
|
|
76
86
|
* One usage table. Every table in the breakdown shares column widths, so the
|
|
@@ -85,85 +95,118 @@ function BreakdownTable(props: {
|
|
|
85
95
|
total?: { cost: string; count: string; label: string };
|
|
86
96
|
}) {
|
|
87
97
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
>
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
<Table
|
|
99
|
+
aria-label={props.label}
|
|
100
|
+
className="min-w-72 table-fixed text-xs leading-relaxed"
|
|
101
|
+
>
|
|
102
|
+
<colgroup>
|
|
103
|
+
<col />
|
|
104
|
+
<col className="w-20" />
|
|
105
|
+
<col className="w-20" />
|
|
106
|
+
</colgroup>
|
|
107
|
+
<TableHeader>
|
|
108
|
+
<TableRow className="border-dashboard-border">
|
|
109
|
+
<TableHead
|
|
110
|
+
scope="col"
|
|
111
|
+
className="px-0 py-2 leading-relaxed font-normal"
|
|
112
|
+
>
|
|
113
|
+
{props.label}
|
|
114
|
+
</TableHead>
|
|
115
|
+
<TableHead
|
|
116
|
+
scope="col"
|
|
117
|
+
className="px-0 py-2 text-right leading-relaxed font-normal"
|
|
118
|
+
>
|
|
119
|
+
{props.countLabel}
|
|
120
|
+
</TableHead>
|
|
121
|
+
<TableHead
|
|
122
|
+
scope="col"
|
|
123
|
+
className="px-0 py-2 text-right leading-relaxed font-normal"
|
|
124
|
+
>
|
|
125
|
+
Cost
|
|
126
|
+
</TableHead>
|
|
127
|
+
</TableRow>
|
|
128
|
+
</TableHeader>
|
|
129
|
+
{props.rows.map((row) => {
|
|
130
|
+
const padding = row.parts ? "pt-2 pb-0.5" : "py-2";
|
|
131
|
+
return (
|
|
132
|
+
<TableBody
|
|
113
133
|
className="border-b border-dashboard-border-subtle last-of-type:border-b-0"
|
|
114
134
|
key={row.key}
|
|
115
135
|
>
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
{/* The tbody draws the group border, so its rows have none. */}
|
|
137
|
+
<TableRow className="border-0">
|
|
138
|
+
<TableHead
|
|
118
139
|
scope="row"
|
|
119
|
-
className={
|
|
140
|
+
className={cn(
|
|
141
|
+
"pl-0 pr-3 align-top leading-relaxed font-normal text-dashboard-text",
|
|
142
|
+
padding,
|
|
143
|
+
)}
|
|
120
144
|
>
|
|
121
145
|
<span className="block truncate">{row.label}</span>
|
|
122
|
-
</
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<td className={`${CELL} ${row.parts ? "pt-2 pb-0.5" : "py-2"}`}>
|
|
127
|
-
{row.cost}
|
|
128
|
-
</td>
|
|
129
|
-
</tr>
|
|
146
|
+
</TableHead>
|
|
147
|
+
<TableCell className={cn(CELL, padding)}>{row.count}</TableCell>
|
|
148
|
+
<TableCell className={cn(CELL, padding)}>{row.cost}</TableCell>
|
|
149
|
+
</TableRow>
|
|
130
150
|
{row.parts?.map((part, index, parts) => {
|
|
131
|
-
const
|
|
151
|
+
const partPadding =
|
|
152
|
+
index === parts.length - 1 ? "pt-0 pb-2" : "py-0";
|
|
132
153
|
return (
|
|
133
|
-
<
|
|
134
|
-
className="text-2xs leading-5
|
|
154
|
+
<TableRow
|
|
155
|
+
className="border-0 text-2xs leading-5"
|
|
135
156
|
key={part.label}
|
|
136
157
|
>
|
|
137
|
-
<
|
|
158
|
+
<TableHead
|
|
138
159
|
scope="row"
|
|
139
|
-
className={
|
|
160
|
+
className={cn(
|
|
161
|
+
"truncate pl-3 pr-3 align-top text-2xs leading-5 font-normal text-dashboard-text-faint",
|
|
162
|
+
partPadding,
|
|
163
|
+
)}
|
|
140
164
|
>
|
|
141
165
|
{part.label}
|
|
142
|
-
</
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
</TableHead>
|
|
167
|
+
<TableCell
|
|
168
|
+
className={cn(
|
|
169
|
+
CELL,
|
|
170
|
+
"text-dashboard-text-faint",
|
|
171
|
+
partPadding,
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{part.count}
|
|
175
|
+
</TableCell>
|
|
176
|
+
<TableCell
|
|
177
|
+
className={cn(
|
|
178
|
+
CELL,
|
|
179
|
+
"text-dashboard-text-faint",
|
|
180
|
+
partPadding,
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
{part.cost}
|
|
184
|
+
</TableCell>
|
|
185
|
+
</TableRow>
|
|
146
186
|
);
|
|
147
187
|
})}
|
|
148
|
-
</
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
</
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
</TableBody>
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
191
|
+
{props.total ? (
|
|
192
|
+
<TableFooter className="font-semibold">
|
|
193
|
+
<TableRow>
|
|
194
|
+
<TableHead
|
|
195
|
+
scope="row"
|
|
196
|
+
className="px-0 py-2 leading-relaxed font-semibold text-dashboard-text"
|
|
197
|
+
>
|
|
198
|
+
{props.total.label}
|
|
199
|
+
</TableHead>
|
|
200
|
+
<TableCell className="px-0 py-2 text-right font-sans tabular-nums">
|
|
201
|
+
{props.total.count}
|
|
202
|
+
</TableCell>
|
|
203
|
+
<TableCell className="px-0 py-2 text-right font-sans tabular-nums">
|
|
204
|
+
{props.total.cost}
|
|
205
|
+
</TableCell>
|
|
206
|
+
</TableRow>
|
|
207
|
+
</TableFooter>
|
|
208
|
+
) : null}
|
|
209
|
+
</Table>
|
|
167
210
|
);
|
|
168
211
|
}
|
|
169
212
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
2
|
import {
|
|
3
3
|
Bot,
|
|
4
|
+
ChevronRight,
|
|
4
5
|
CircleDot,
|
|
5
6
|
Eye,
|
|
6
7
|
FolderGit2,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
Timer,
|
|
15
16
|
Users,
|
|
16
17
|
} from "lucide-react";
|
|
17
|
-
import type
|
|
18
|
+
import { useState, type ReactNode } from "react";
|
|
18
19
|
import {
|
|
19
20
|
conversationWatchesReportSchema,
|
|
20
21
|
type ConversationWatch,
|
|
@@ -22,7 +23,8 @@ import {
|
|
|
22
23
|
|
|
23
24
|
import { formatTime } from "../format";
|
|
24
25
|
import { fetchDashboardJson } from "../http";
|
|
25
|
-
import {
|
|
26
|
+
import { dashboardDisclosureChevronClass } from "../styles";
|
|
27
|
+
import { DetailsList, DetailsRow } from "./DetailsList";
|
|
26
28
|
|
|
27
29
|
/** Load the active Watches that one Conversation owns. */
|
|
28
30
|
export function useConversationWatches(conversationId: string) {
|
|
@@ -39,8 +41,8 @@ export function useConversationWatches(conversationId: string) {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Show what can wake this Conversation:
|
|
43
|
-
* the reason
|
|
44
|
+
* Show what can wake this Conversation: one line per active Watch. A line
|
|
45
|
+
* expands to show the reason and the event types that deliver to it.
|
|
44
46
|
*/
|
|
45
47
|
export function ConversationWatches(props: { watches: ConversationWatch[] }) {
|
|
46
48
|
return (
|
|
@@ -66,65 +68,81 @@ const resourceIcons: Partial<Record<string, LucideIcon>> = {
|
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Show one Watch
|
|
70
|
-
*
|
|
71
|
+
* Show one Watch as a single line with its resource. Clicking the line shows
|
|
72
|
+
* why the Watch exists and which events wake the Conversation. The expiry is
|
|
73
|
+
* an internal limit, so the row does not show it.
|
|
71
74
|
*/
|
|
72
75
|
function WatchRow(props: { watch: ConversationWatch }) {
|
|
73
76
|
const { watch } = props;
|
|
77
|
+
const [expanded, setExpanded] = useState(false);
|
|
74
78
|
const Icon = watch.redacted
|
|
75
79
|
? LockKeyhole
|
|
76
80
|
: (resourceIcons[watch.resourceType] ?? Eye);
|
|
77
|
-
const isTimer = watch.firesAt !== undefined;
|
|
78
|
-
const until = watch.firesAt ?? watch.expiresAt;
|
|
79
81
|
const resource = `${watch.namespace} ${watch.resourceType.replaceAll("_", " ")}`;
|
|
80
|
-
// A timer has one event, and
|
|
81
|
-
const events =
|
|
82
|
+
// A timer has one event, and its fire time already says when it wakes.
|
|
83
|
+
const events = watch.firesAt ? [] : watch.events;
|
|
82
84
|
const filters = Object.entries(watch.match ?? {});
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
+
const row = (
|
|
85
86
|
<DetailsRow
|
|
87
|
+
expanded={expanded}
|
|
86
88
|
icon={<Icon aria-hidden="true" className="size-3.5" />}
|
|
87
89
|
meta={
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
<span className="grid size-5 place-items-center">
|
|
91
|
+
<ChevronRight
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
className={dashboardDisclosureChevronClass}
|
|
94
|
+
/>
|
|
95
|
+
</span>
|
|
93
96
|
}
|
|
97
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
94
98
|
title={watch.label ?? `Private ${resource}`}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
if (!expanded) return row;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{row}
|
|
106
|
+
{/* Indent under the title: icon column, column gap, and row padding. */}
|
|
107
|
+
<div className="grid min-w-0 gap-1.5 pb-2 pl-9 pr-1.5 pt-0.5 text-xs leading-4 text-dashboard-text-muted">
|
|
108
|
+
{watch.intent ? (
|
|
109
|
+
<p className="m-0 break-words">{watch.intent}</p>
|
|
110
|
+
) : null}
|
|
111
|
+
{watch.firesAt ? (
|
|
112
|
+
<p className="m-0">
|
|
113
|
+
Fires{" "}
|
|
114
|
+
<time dateTime={watch.firesAt}>
|
|
115
|
+
{formatTime(watch.firesAt, {
|
|
116
|
+
dateStyle: "medium",
|
|
117
|
+
timeStyle: "short",
|
|
118
|
+
})}
|
|
119
|
+
</time>
|
|
120
|
+
</p>
|
|
121
|
+
) : null}
|
|
122
|
+
{events.length > 0 || filters.length > 0 ? (
|
|
123
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
124
|
+
{events.map((event) => (
|
|
125
|
+
<WatchChip key={event}>
|
|
126
|
+
{watchEventLabel(event, watch.resourceType)}
|
|
127
|
+
</WatchChip>
|
|
128
|
+
))}
|
|
129
|
+
{filters.map(([key, value]) => (
|
|
130
|
+
<WatchChip filter key={`match:${key}`}>
|
|
131
|
+
{key.replaceAll("_", " ")}: {[value].flat().join(", ")}
|
|
132
|
+
</WatchChip>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
{watch.childAgent ? (
|
|
137
|
+
<p className="m-0 flex min-w-0 items-center gap-1">
|
|
138
|
+
<Bot aria-hidden="true" className="size-3 shrink-0" />
|
|
139
|
+
<span className="truncate">
|
|
140
|
+
via {watch.childAgent.name ?? "subagent"}
|
|
123
141
|
</span>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
</p>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
</>
|
|
128
146
|
);
|
|
129
147
|
}
|
|
130
148
|
|
|
@@ -153,13 +171,3 @@ function watchEventLabel(event: string, resourceType: string): string {
|
|
|
153
171
|
parts[0] === resourceType && parts.length > 1 ? parts.slice(1) : parts;
|
|
154
172
|
return rest.join(" ").replaceAll("_", " ");
|
|
155
173
|
}
|
|
156
|
-
|
|
157
|
-
/** Format the time until a Watch fires or expires as one compact unit. */
|
|
158
|
-
function formatRemaining(value: string): string {
|
|
159
|
-
const ms = Math.max(0, Date.parse(value) - Date.now());
|
|
160
|
-
const minutes = Math.round(ms / 60_000);
|
|
161
|
-
if (minutes < 60) return `${Math.max(1, minutes)}m`;
|
|
162
|
-
const hours = Math.round(minutes / 60);
|
|
163
|
-
if (hours < 48) return `${hours}h`;
|
|
164
|
-
return `${Math.round(hours / 24)}d`;
|
|
165
|
-
}
|