UI: richer toasts, log viewer scroll fix, multi-goal projects, active panel issue context

Improve activity toasts with actor names, issue identifiers, and
action links. Fix LogViewer auto-scroll to work with scrollable
parent containers instead of only window. Add issue context display
to ActiveAgentsPanel run cards. Support multi-goal selection in
NewProjectDialog. Update GoalDetail to match multi-goal project
linking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 15:48:42 -06:00
parent 82da8739c1
commit 65f09a1a9d
7 changed files with 337 additions and 75 deletions

View File

@@ -1,8 +1,9 @@
import { useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import type { LiveEvent } from "@paperclip/shared";
import type { Issue, LiveEvent } from "@paperclip/shared";
import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats";
import { issuesApi } from "../api/issues";
import { getUIAdapter } from "../adapters";
import type { TranscriptEntry } from "../adapters";
import { queryKeys } from "../lib/queryKeys";
@@ -152,6 +153,20 @@ export function ActiveAgentsPanel({ companyId }: ActiveAgentsPanelProps) {
});
const runs = liveRuns ?? [];
const { data: issues } = useQuery({
queryKey: queryKeys.issues.list(companyId),
queryFn: () => issuesApi.list(companyId),
enabled: runs.length > 0,
});
const issueById = useMemo(() => {
const map = new Map<string, Issue>();
for (const issue of issues ?? []) {
map.set(issue.id, issue);
}
return map;
}, [issues]);
const runById = useMemo(() => new Map(runs.map((r) => [r.id, r])), [runs]);
const activeRunIds = useMemo(() => new Set(runs.map((r) => r.id)), [runs]);
@@ -290,6 +305,7 @@ export function ActiveAgentsPanel({ companyId }: ActiveAgentsPanelProps) {
<AgentRunCard
key={run.id}
run={run}
issue={run.issueId ? issueById.get(run.issueId) : undefined}
feed={feedByRun.get(run.id) ?? []}
/>
))}
@@ -298,7 +314,15 @@ export function ActiveAgentsPanel({ companyId }: ActiveAgentsPanelProps) {
);
}
function AgentRunCard({ run, feed }: { run: LiveRunForIssue; feed: FeedItem[] }) {
function AgentRunCard({
run,
issue,
feed,
}: {
run: LiveRunForIssue;
issue?: Issue;
feed: FeedItem[];
}) {
const bodyRef = useRef<HTMLDivElement>(null);
const recent = feed.slice(-20);
@@ -331,6 +355,20 @@ function AgentRunCard({ run, feed }: { run: LiveRunForIssue; feed: FeedItem[] })
</Link>
</div>
{run.issueId && (
<div className="px-3 py-1.5 border-b border-border/40 text-xs flex items-center gap-1 min-w-0">
<span className="text-muted-foreground mr-1">Working on:</span>
<Link
to={`/issues/${run.issueId}`}
className="text-blue-400 hover:text-blue-300 hover:underline min-w-0 truncate"
title={issue?.title ? `${issue?.identifier ?? run.issueId.slice(0, 8)} - ${issue.title}` : issue?.identifier ?? run.issueId.slice(0, 8)}
>
{issue?.identifier ?? run.issueId.slice(0, 8)}
{issue?.title ? ` - ${issue.title}` : ""}
</Link>
</div>
)}
<div ref={bodyRef} className="max-h-[180px] overflow-y-auto p-2 font-mono text-[11px] space-y-1">
{recent.length === 0 && (
<div className="text-xs text-muted-foreground">Waiting for output...</div>

View File

@@ -130,7 +130,7 @@ export function NewIssueDialog() {
title: `${issue.identifier ?? "Issue"} created`,
body: issue.title,
tone: "success",
action: { label: "View issue", href: `/issues/${issue.id}` },
action: { label: `View ${issue.identifier ?? "issue"}`, href: `/issues/${issue.id}` },
});
},
});

View File

@@ -21,11 +21,12 @@ import {
Minimize2,
Target,
Calendar,
Plus,
X,
} from "lucide-react";
import { cn } from "../lib/utils";
import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor";
import { StatusBadge } from "./StatusBadge";
import type { Goal } from "@paperclip/shared";
const projectStatuses = [
{ value: "backlog", label: "Backlog" },
@@ -42,7 +43,7 @@ export function NewProjectDialog() {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [status, setStatus] = useState("planned");
const [goalId, setGoalId] = useState("");
const [goalIds, setGoalIds] = useState<string[]>([]);
const [targetDate, setTargetDate] = useState("");
const [expanded, setExpanded] = useState(false);
@@ -77,7 +78,7 @@ export function NewProjectDialog() {
setName("");
setDescription("");
setStatus("planned");
setGoalId("");
setGoalIds([]);
setTargetDate("");
setExpanded(false);
}
@@ -88,7 +89,7 @@ export function NewProjectDialog() {
name: name.trim(),
description: description.trim() || undefined,
status,
...(goalId ? { goalId } : {}),
...(goalIds.length > 0 ? { goalIds } : {}),
...(targetDate ? { targetDate } : {}),
});
}
@@ -100,7 +101,8 @@ export function NewProjectDialog() {
}
}
const currentGoal = (goals ?? []).find((g) => g.id === goalId);
const selectedGoals = (goals ?? []).filter((g) => goalIds.includes(g.id));
const availableGoals = (goals ?? []).filter((g) => !goalIds.includes(g.id));
return (
<Dialog
@@ -206,36 +208,60 @@ export function NewProjectDialog() {
</PopoverContent>
</Popover>
{/* Goal */}
{selectedGoals.map((goal) => (
<span
key={goal.id}
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-xs"
>
<Target className="h-3 w-3 text-muted-foreground" />
<span className="max-w-[160px] truncate">{goal.title}</span>
<button
className="text-muted-foreground hover:text-foreground"
onClick={() => setGoalIds((prev) => prev.filter((id) => id !== goal.id))}
aria-label={`Remove goal ${goal.title}`}
type="button"
>
<X className="h-3 w-3" />
</button>
</span>
))}
<Popover open={goalOpen} onOpenChange={setGoalOpen}>
<PopoverTrigger asChild>
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
<Target className="h-3 w-3 text-muted-foreground" />
{currentGoal ? currentGoal.title : "Goal"}
<button
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors disabled:opacity-60"
disabled={selectedGoals.length > 0 && availableGoals.length === 0}
>
{selectedGoals.length > 0 ? <Plus className="h-3 w-3 text-muted-foreground" /> : <Target className="h-3 w-3 text-muted-foreground" />}
{selectedGoals.length > 0 ? "+ Goal" : "Goal"}
</button>
</PopoverTrigger>
<PopoverContent className="w-48 p-1" align="start">
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
!goalId && "bg-accent"
)}
onClick={() => { setGoalId(""); setGoalOpen(false); }}
>
No goal
</button>
{(goals ?? []).map((g) => (
<PopoverContent className="w-56 p-1" align="start">
{selectedGoals.length === 0 && (
<button
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground"
onClick={() => setGoalOpen(false)}
>
No goal
</button>
)}
{availableGoals.map((g) => (
<button
key={g.id}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate",
g.id === goalId && "bg-accent"
)}
onClick={() => { setGoalId(g.id); setGoalOpen(false); }}
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate"
onClick={() => {
setGoalIds((prev) => [...prev, g.id]);
setGoalOpen(false);
}}
>
{g.title}
</button>
))}
{selectedGoals.length > 0 && availableGoals.length === 0 && (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
All goals already selected.
</div>
)}
</PopoverContent>
</Popover>

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, type ReactNode } from "react";
import { useQueryClient, type QueryClient } from "@tanstack/react-query";
import type { Agent, LiveEvent } from "@paperclip/shared";
import type { Agent, Issue, LiveEvent } from "@paperclip/shared";
import { useCompany } from "./CompanyContext";
import type { ToastInput } from "./ToastContext";
import { useToast } from "./ToastContext";
@@ -39,6 +39,71 @@ function truncate(text: string, max: number): string {
return text.slice(0, max - 1) + "\u2026";
}
function looksLikeUuid(value: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}
function titleCase(value: string): string {
return value
.split(" ")
.filter((part) => part.length > 0)
.map((part) => part[0]!.toUpperCase() + part.slice(1))
.join(" ");
}
function resolveActorLabel(
queryClient: QueryClient,
companyId: string,
actorType: string | null,
actorId: string | null,
): string {
if (actorType === "agent" && actorId) {
return resolveAgentName(queryClient, companyId, actorId) ?? `Agent ${shortId(actorId)}`;
}
if (actorType === "system") return "System";
if (actorType === "user" && actorId) {
if (looksLikeUuid(actorId)) return `User ${shortId(actorId)}`;
return titleCase(actorId.replace(/[_-]+/g, " "));
}
return "Someone";
}
interface IssueToastContext {
ref: string;
title: string | null;
label: string;
href: string;
}
function resolveIssueToastContext(
queryClient: QueryClient,
companyId: string,
issueId: string,
details: Record<string, unknown> | null,
): IssueToastContext {
const detailIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId));
const listIssue = queryClient
.getQueryData<Issue[]>(queryKeys.issues.list(companyId))
?.find((issue) => issue.id === issueId);
const cachedIssue = detailIssue ?? listIssue ?? null;
const ref =
readString(details?.identifier) ??
readString(details?.issueIdentifier) ??
cachedIssue?.identifier ??
`Issue ${shortId(issueId)}`;
const title =
readString(details?.title) ??
readString(details?.issueTitle) ??
cachedIssue?.title ??
null;
return {
ref,
title,
label: title ? `${ref} - ${truncate(title, 72)}` : ref,
href: `/issues/${issueId}`,
};
}
const ISSUE_TOAST_ACTIONS = new Set(["issue.created", "issue.updated", "issue.comment_added"]);
const AGENT_TOAST_STATUSES = new Set(["running", "idle", "error"]);
const TERMINAL_RUN_STATUSES = new Set(["succeeded", "failed", "timed_out", "cancelled"]);
@@ -46,17 +111,24 @@ const TERMINAL_RUN_STATUSES = new Set(["succeeded", "failed", "timed_out", "canc
function describeIssueUpdate(details: Record<string, unknown> | null): string | null {
if (!details) return null;
const changes: string[] = [];
if (typeof details.status === "string") changes.push(`status \u2192 ${details.status}`);
if (typeof details.priority === "string") changes.push(`priority \u2192 ${details.priority}`);
if (typeof details.status === "string") changes.push(`status -> ${details.status.replace(/_/g, " ")}`);
if (typeof details.priority === "string") changes.push(`priority -> ${details.priority}`);
if (typeof details.assigneeAgentId === "string") changes.push("reassigned");
else if (details.assigneeAgentId === null) changes.push("unassigned");
if (details.reopened === true) {
const from = readString(details.reopenedFrom);
changes.push(from ? `reopened from ${from.replace(/_/g, " ")}` : "reopened");
}
if (typeof details.title === "string") changes.push("title changed");
if (typeof details.description === "string") changes.push("description changed");
if (changes.length > 0) return changes.join(", ");
return null;
}
function buildActivityToast(
queryClient: QueryClient,
companyId: string,
payload: Record<string, unknown>,
nameOf: (id: string) => string | null,
): ToastInput | null {
const entityType = readString(payload.entityType);
const entityId = readString(payload.entityId);
@@ -69,43 +141,43 @@ function buildActivityToast(
return null;
}
const issueHref = `/issues/${entityId}`;
const issueTitle = details?.title && typeof details.title === "string"
? truncate(details.title, 60)
: null;
const actorName = actorType === "agent" && actorId ? nameOf(actorId) : null;
const byLine = actorName ? ` by ${actorName}` : "";
const issue = resolveIssueToastContext(queryClient, companyId, entityId, details);
const actor = resolveActorLabel(queryClient, companyId, actorType, actorId);
if (action === "issue.created") {
return {
title: `Issue created${byLine}`,
body: issueTitle ?? `Issue ${shortId(entityId)}`,
title: `${actor} created ${issue.ref}`,
body: issue.title ? truncate(issue.title, 96) : undefined,
tone: "success",
action: { label: "Open issue", href: issueHref },
action: { label: `View ${issue.ref}`, href: issue.href },
dedupeKey: `activity:${action}:${entityId}`,
};
}
if (action === "issue.updated") {
const changeDesc = describeIssueUpdate(details);
const label = issueTitle ?? `Issue ${shortId(entityId)}`;
const body = changeDesc ? `${label} \u2014 ${changeDesc}` : label;
const body = changeDesc
? issue.title
? `${truncate(issue.title, 64)} - ${changeDesc}`
: changeDesc
: issue.title
? truncate(issue.title, 96)
: issue.label;
return {
title: `Issue updated${byLine}`,
title: `${actor} updated ${issue.ref}`,
body: truncate(body, 100),
tone: "info",
action: { label: "Open issue", href: issueHref },
action: { label: `View ${issue.ref}`, href: issue.href },
dedupeKey: `activity:${action}:${entityId}`,
};
}
const commentId = readString(details?.commentId);
const issueLabel = issueTitle ?? `Issue ${shortId(entityId)}`;
return {
title: `New comment${byLine}`,
body: issueLabel,
title: `${actor} posted a comment on ${issue.ref}`,
body: issue.title ? truncate(issue.title, 96) : undefined,
tone: "info",
action: { label: "Open issue", href: issueHref },
action: { label: `View ${issue.ref}`, href: issue.href },
dedupeKey: `activity:${action}:${entityId}:${commentId ?? "na"}`,
};
}
@@ -324,7 +396,7 @@ function handleLiveEvent(
if (event.type === "activity.logged") {
invalidateActivityQueries(queryClient, expectedCompanyId, payload);
const action = readString(payload.action);
const toast = buildActivityToast(payload, nameOf);
const toast = buildActivityToast(queryClient, expectedCompanyId, payload);
if (toast) gatedPushToast(gate, pushToast, `activity:${action ?? "unknown"}`, toast);
}
}

View File

@@ -112,6 +112,60 @@ const sourceLabels: Record<string, string> = {
automation: "Automation",
};
const LIVE_SCROLL_BOTTOM_TOLERANCE_PX = 32;
type ScrollContainer = Window | HTMLElement;
function isWindowContainer(container: ScrollContainer): container is Window {
return container === window;
}
function isElementScrollContainer(element: HTMLElement): boolean {
const overflowY = window.getComputedStyle(element).overflowY;
return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
}
function findScrollContainer(anchor: HTMLElement | null): ScrollContainer {
let parent = anchor?.parentElement ?? null;
while (parent) {
if (isElementScrollContainer(parent)) return parent;
parent = parent.parentElement;
}
return window;
}
function readScrollMetrics(container: ScrollContainer): { scrollHeight: number; distanceFromBottom: number } {
if (isWindowContainer(container)) {
const pageHeight = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight,
);
const viewportBottom = window.scrollY + window.innerHeight;
return {
scrollHeight: pageHeight,
distanceFromBottom: Math.max(0, pageHeight - viewportBottom),
};
}
const viewportBottom = container.scrollTop + container.clientHeight;
return {
scrollHeight: container.scrollHeight,
distanceFromBottom: Math.max(0, container.scrollHeight - viewportBottom),
};
}
function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBehavior = "auto") {
if (isWindowContainer(container)) {
const pageHeight = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight,
);
window.scrollTo({ top: pageHeight, behavior });
return;
}
container.scrollTo({ top: container.scrollHeight, behavior });
}
type AgentDetailTab = "overview" | "configuration" | "runs" | "issues" | "costs" | "keys";
function parseAgentDetailTab(value: string | null): AgentDetailTab {
@@ -1200,9 +1254,15 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
const [logLoading, setLogLoading] = useState(!!run.logRef);
const [logError, setLogError] = useState<string | null>(null);
const [logOffset, setLogOffset] = useState(0);
const [isFollowing, setIsFollowing] = useState(true);
const [isFollowing, setIsFollowing] = useState(false);
const logEndRef = useRef<HTMLDivElement>(null);
const pendingLogLineRef = useRef("");
const scrollContainerRef = useRef<ScrollContainer | null>(null);
const isFollowingRef = useRef(false);
const lastMetricsRef = useRef<{ scrollHeight: number; distanceFromBottom: number }>({
scrollHeight: 0,
distanceFromBottom: Number.POSITIVE_INFINITY,
});
const isLive = run.status === "running" || run.status === "queued";
function appendLogContent(content: string, finalize = false) {
@@ -1250,39 +1310,86 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
}
}, [initialEvents]);
const updateFollowingState = useCallback(() => {
const viewportBottom = window.scrollY + window.innerHeight;
const pageHeight = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight,
);
const distanceFromBottom = pageHeight - viewportBottom;
const isNearBottom = distanceFromBottom <= 32;
setIsFollowing((prev) => (prev === isNearBottom ? prev : isNearBottom));
const getScrollContainer = useCallback((): ScrollContainer => {
if (scrollContainerRef.current) return scrollContainerRef.current;
const container = findScrollContainer(logEndRef.current);
scrollContainerRef.current = container;
return container;
}, []);
const updateFollowingState = useCallback(() => {
const container = getScrollContainer();
const metrics = readScrollMetrics(container);
lastMetricsRef.current = metrics;
const nearBottom = metrics.distanceFromBottom <= LIVE_SCROLL_BOTTOM_TOLERANCE_PX;
isFollowingRef.current = nearBottom;
setIsFollowing((prev) => (prev === nearBottom ? prev : nearBottom));
}, [getScrollContainer]);
useEffect(() => {
if (!isLive) return;
setIsFollowing(true);
}, [isLive, run.id]);
scrollContainerRef.current = null;
lastMetricsRef.current = {
scrollHeight: 0,
distanceFromBottom: Number.POSITIVE_INFINITY,
};
if (!isLive) {
isFollowingRef.current = false;
setIsFollowing(false);
return;
}
updateFollowingState();
}, [isLive, run.id, updateFollowingState]);
useEffect(() => {
if (!isLive) return;
const container = getScrollContainer();
updateFollowingState();
window.addEventListener("scroll", updateFollowingState, { passive: true });
if (container === window) {
window.addEventListener("scroll", updateFollowingState, { passive: true });
} else {
container.addEventListener("scroll", updateFollowingState, { passive: true });
}
window.addEventListener("resize", updateFollowingState);
return () => {
window.removeEventListener("scroll", updateFollowingState);
if (container === window) {
window.removeEventListener("scroll", updateFollowingState);
} else {
container.removeEventListener("scroll", updateFollowingState);
}
window.removeEventListener("resize", updateFollowingState);
};
}, [isLive, updateFollowingState]);
}, [isLive, run.id, getScrollContainer, updateFollowingState]);
// Auto-scroll only for live runs when following
useEffect(() => {
if (isLive && isFollowing) {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
if (!isLive || !isFollowingRef.current) return;
const container = getScrollContainer();
const previous = lastMetricsRef.current;
const current = readScrollMetrics(container);
const growth = Math.max(0, current.scrollHeight - previous.scrollHeight);
const expectedDistance = previous.distanceFromBottom + growth;
const movedAwayBy = current.distanceFromBottom - expectedDistance;
// If user moved away from bottom between updates, release auto-follow immediately.
if (movedAwayBy > LIVE_SCROLL_BOTTOM_TOLERANCE_PX) {
isFollowingRef.current = false;
setIsFollowing(false);
lastMetricsRef.current = current;
return;
}
}, [events, logLines, isLive, isFollowing]);
scrollToContainerBottom(container, "auto");
const after = readScrollMetrics(container);
lastMetricsRef.current = after;
if (!isFollowingRef.current) {
isFollowingRef.current = true;
}
setIsFollowing((prev) => (prev ? prev : true));
}, [events.length, logLines.length, isLive, getScrollContainer]);
// Fetch persisted shell log
useEffect(() => {
@@ -1463,8 +1570,11 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
variant="ghost"
size="xs"
onClick={() => {
const container = getScrollContainer();
isFollowingRef.current = true;
setIsFollowing(true);
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
scrollToContainerBottom(container, "auto");
lastMetricsRef.current = readScrollMetrics(container);
}}
>
Jump to live

View File

@@ -64,7 +64,12 @@ export function GoalDetail() {
});
const childGoals = (allGoals ?? []).filter((g) => g.parentId === goalId);
const linkedProjects = (allProjects ?? []).filter((p) => p.goalId === goalId);
const linkedProjects = (allProjects ?? []).filter((p) => {
if (!goalId) return false;
if (p.goalIds.includes(goalId)) return true;
if (p.goals.some((goalRef) => goalRef.id === goalId)) return true;
return p.goalId === goalId;
});
useEffect(() => {
setBreadcrumbs([

View File

@@ -66,6 +66,11 @@ function usageNumber(usage: Record<string, unknown> | null, ...keys: string[]) {
return 0;
}
function truncate(text: string, max: number): string {
if (text.length <= max) return text;
return text.slice(0, max - 1) + "\u2026";
}
function formatAction(action: string, details?: Record<string, unknown> | null): string {
if (action === "issue.updated" && details) {
const previous = (details._previous ?? {}) as Record<string, unknown>;
@@ -270,10 +275,13 @@ export function IssueDetail() {
mutationFn: (data: Record<string, unknown>) => issuesApi.update(issueId!, data),
onSuccess: (updated) => {
invalidateIssue();
const issueRef = updated.identifier ?? `Issue ${updated.id.slice(0, 8)}`;
pushToast({
dedupeKey: `activity:issue.updated:${updated.id}`,
title: "Issue updated",
title: `${issueRef} updated`,
body: truncate(updated.title, 96),
tone: "success",
action: { label: `View ${issueRef}`, href: `/issues/${updated.id}` },
});
},
});
@@ -281,13 +289,16 @@ export function IssueDetail() {
const addComment = useMutation({
mutationFn: ({ body, reopen }: { body: string; reopen?: boolean }) =>
issuesApi.addComment(issueId!, body, reopen),
onSuccess: () => {
onSuccess: (comment) => {
invalidateIssue();
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
const issueRef = issue?.identifier ?? (issueId ? `Issue ${issueId.slice(0, 8)}` : "Issue");
pushToast({
dedupeKey: `activity:issue.comment_added:${issueId}`,
title: "Comment posted",
dedupeKey: `activity:issue.comment_added:${issueId}:${comment.id}`,
title: `Comment posted on ${issueRef}`,
body: issue?.title ? truncate(issue.title, 96) : undefined,
tone: "success",
action: issueId ? { label: `View ${issueRef}`, href: `/issues/${issueId}` } : undefined,
});
},
});