Compare commits

...

6 Commits

Author SHA1 Message Date
coderabbitai[bot]
3db2a944f7 fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-04-01 19:24:39 +00:00
Bentlybro
59192102a6 refactor(frontend): remove useMemo from useLibraryAgentList; fix useState staleness in useSitrepItems and useFleetSummary
- useLibraryAgentList: replace both useMemo calls with plain expressions;
  extract filterAgentsByStatus as a standalone helper function
- useSitrepItems: replace useState initializer with useMemo([agentIDs,
  maxItems]) so items recompute when the agent list changes
- useFleetSummary: replace useState initializer with useMemo([agentIDs])
  so fleet counts recompute when agents are added or removed
2026-04-01 19:10:03 +00:00
Bentlybro
65cca9bef8 fix(frontend): fix TS errors and AgentBriefingPanel regression
- Replace `variant="xsmall"` (not in Text component's type union) with
  `variant="small"` in PulseChips, StatsGrid, LibraryAgentCard, SitrepList
  — fixes the `check API types` CI failure
- useLibraryAgentList: expose `allAgentIDs` (unfiltered) and
  `displayedCount` (filtered count when filter is active)
- LibraryAgentList: pass `allAgentIDs` to AgentBriefingPanel so the
  sitrep covers the full fleet regardless of the active filter; pass
  `displayedCount` to the tab label so "All N" reflects the current view

Addresses Sentry comments 3023964054 and 3023964058.
2026-04-01 19:09:41 +00:00
Bentlybro
6b32e43d84 fix(frontend): wire statusFilter to agent list, fix consumePrompt, add NOTE comments
- useLibraryAgentList: accept statusFilter prop and apply client-side
  filtering using mockStatusForAgent; maps "attention"→health=attention,
  "healthy"→health=good, others match status directly
- LibraryAgentList: pass statusFilter through to useLibraryAgentList
- AutoPilotBridgeContext.consumePrompt: use `prompt !== null` instead of
  truthy check so empty string correctly clears sessionStorage
- Add NOTE comments near useState initialisers in useSitrepItems,
  useLibraryFleetSummary, useAgentStatus, and useFleetSummary explaining
  that they do not recompute on prop changes
2026-04-01 19:09:41 +00:00
Bentlybro
b73d05c23e fix(frontend): address review feedback — lint, format, and bug fixes
- Remove unused Button import in PulseChips (fixes lint CI failure)
- Fix AutoPilotBridgeContext: use Next.js router + sessionStorage instead
  of window.location.href which destroyed React state before consumption
- Add defensive handling in formatTimeAgo for invalid/future dates
- Use cn() utility in LibraryAgentCard for className consistency
- Fix prettier formatting across AgentFilterMenu, SitrepList, useAgentStatus
2026-03-31 17:22:23 +00:00
John Ababseh
8277cce835 feat(frontend): add Agent Intelligence Layer to library and home
Implements 7 new features for agent awareness across the platform:

1. Agent Briefing Panel — collapsible stats grid showing fleet-wide
   counts (running, error, listening, scheduled, idle, monthly spend)
2. Enhanced Library Cards — StatusBadge, progress bar, error messages,
   run counts, spend, and time-ago on every agent card
3. Expanded Filter & Sort — new AgentFilterMenu dropdown with status-
   based filtering (running, attention, listening, scheduled, idle)
4. AI Summary / Situation Report — prioritized SitrepList with error-
   first ranking and contextual action buttons
5. Ask AutoPilot Bridge — shared context that lets sitrep items and
   pulse chips pre-populate the Home chat with agent-specific prompts
6. Home Pulse Chips — lightweight agent status chips on the empty
   Home/Copilot screen linking back to the library
7. Contextual Action Buttons — status-aware actions (View error,
   Reconnect, Watch live, Run now, Retry) on cards and sitrep items

All features use deterministic mock data via useAgentStatus hook,
marked with TODO comments for backend API integration. Follows
existing component patterns (atoms/molecules/organisms), reuses
shadcn/ui primitives, Phosphor Icons, and platform design tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 18:12:51 +02:00
23 changed files with 1239 additions and 39 deletions

View File

@@ -13,6 +13,8 @@ import {
getSuggestionThemes,
} from "./helpers";
import { SuggestionThemes } from "./components/SuggestionThemes/SuggestionThemes";
import { PulseChips } from "../PulseChips/PulseChips";
import { usePulseChips } from "../PulseChips/usePulseChips";
interface Props {
inputLayoutId: string;
@@ -34,6 +36,7 @@ export function EmptySession({
}: Props) {
const { user } = useSupabase();
const greetingName = getGreetingName(user);
const pulseChips = usePulseChips();
const { data: suggestedPromptsResponse, isLoading: isLoadingPrompts } =
useGetV2GetSuggestedPrompts({
@@ -80,6 +83,8 @@ export function EmptySession({
Tell me about your work I&apos;ll find what to automate.
</Text>
<PulseChips chips={pulseChips} onChipClick={onSend} />
<div className="mb-6">
<motion.div
layoutId={inputLayoutId}

View File

@@ -0,0 +1,87 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { ArrowRightIcon } from "@phosphor-icons/react";
import NextLink from "next/link";
import { StatusBadge } from "@/app/(platform)/library/components/StatusBadge/StatusBadge";
import type { AgentStatus } from "@/app/(platform)/library/types";
export interface PulseChipData {
id: string;
name: string;
status: AgentStatus;
shortMessage: string;
}
interface Props {
chips: PulseChipData[];
onChipClick?: (prompt: string) => void;
}
export function PulseChips({ chips, onChipClick }: Props) {
if (chips.length === 0) return null;
return (
<div className="mb-6">
<div className="mb-3 flex items-center justify-between">
<Text variant="small-medium" className="text-zinc-600">
What&apos;s happening with your agents
</Text>
<NextLink
href="/library"
className="flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-700"
>
View all <ArrowRightIcon size={12} />
</NextLink>
</div>
<div className="flex flex-wrap gap-2">
{chips.map((chip) => (
<PulseChip key={chip.id} chip={chip} onClick={onChipClick} />
))}
</div>
</div>
);
}
interface ChipProps {
chip: PulseChipData;
onClick?: (prompt: string) => void;
}
function PulseChip({ chip, onClick }: ChipProps) {
function handleClick() {
const prompt = buildChipPrompt(chip);
onClick?.(prompt);
}
return (
<button
type="button"
onClick={handleClick}
className="flex items-center gap-2 rounded-medium border border-zinc-100 bg-white px-3 py-2 text-left transition-all hover:border-zinc-200 hover:shadow-sm"
>
<StatusBadge status={chip.status} />
<div className="min-w-0">
<Text variant="small-medium" className="truncate text-zinc-900">
{chip.name}
</Text>
<Text variant="small" className="truncate text-zinc-500">
{chip.shortMessage}
</Text>
</div>
</button>
);
}
function buildChipPrompt(chip: PulseChipData): string {
switch (chip.status) {
case "error":
return `What happened with ${chip.name}? It has an error — can you check?`;
case "running":
return `Give me a status update on ${chip.name} — what has it done so far?`;
case "idle":
return `${chip.name} hasn't run recently. Should I keep it or update and re-run it?`;
default:
return `Tell me about ${chip.name} — what's its current status?`;
}
}

View File

@@ -0,0 +1,43 @@
"use client";
import { useState } from "react";
import type { PulseChipData } from "./PulseChips";
import type { AgentStatus } from "@/app/(platform)/library/types";
/**
* Provides a prioritised list of pulse chips for the Home empty state.
* Errors → running → stale, max 5 chips.
*
* TODO: Replace with real API data from `GET /agents/summary` or similar.
*/
export function usePulseChips(): PulseChipData[] {
const [chips] = useState<PulseChipData[]>(() => MOCK_CHIPS);
return chips;
}
const MOCK_CHIPS: PulseChipData[] = [
{
id: "chip-1",
name: "Lead Finder",
status: "error" as AgentStatus,
shortMessage: "API rate limit hit",
},
{
id: "chip-2",
name: "CEO Finder",
status: "running" as AgentStatus,
shortMessage: "72% complete",
},
{
id: "chip-3",
name: "Cart Recovery",
status: "idle" as AgentStatus,
shortMessage: "No runs in 3 weeks",
},
{
id: "chip-4",
name: "Social Collector",
status: "listening" as AgentStatus,
shortMessage: "Waiting for trigger",
},
];

View File

@@ -2,14 +2,17 @@ import { Navbar } from "@/components/layout/Navbar/Navbar";
import { NetworkStatusMonitor } from "@/services/network-status/NetworkStatusMonitor";
import { ReactNode } from "react";
import { AdminImpersonationBanner } from "./admin/components/AdminImpersonationBanner";
import { AutoPilotBridgeProvider } from "@/contexts/AutoPilotBridgeContext";
export default function PlatformLayout({ children }: { children: ReactNode }) {
return (
<main className="flex h-screen w-full flex-col">
<NetworkStatusMonitor />
<Navbar />
<AdminImpersonationBanner />
<section className="flex-1">{children}</section>
</main>
<AutoPilotBridgeProvider>
<main className="flex h-screen w-full flex-col">
<NetworkStatusMonitor />
<Navbar />
<AdminImpersonationBanner />
<section className="flex-1">{children}</section>
</main>
</AutoPilotBridgeProvider>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import { CaretUpIcon, CaretDownIcon } from "@phosphor-icons/react";
import { useState } from "react";
import type { FleetSummary, AgentStatusFilter } from "../../types";
import { SitrepList } from "../SitrepItem/SitrepList";
import { StatsGrid } from "./StatsGrid";
interface Props {
summary: FleetSummary;
agentIDs: string[];
onFilterChange?: (filter: AgentStatusFilter) => void;
activeFilter?: AgentStatusFilter;
}
export function AgentBriefingPanel({
summary,
agentIDs,
onFilterChange,
activeFilter = "all",
}: Props) {
const [isCollapsed, setIsCollapsed] = useState(false);
const totalAttention = summary.error;
const headerSummary = [
summary.running > 0 && `${summary.running} running`,
totalAttention > 0 && `${totalAttention} need attention`,
summary.listening > 0 && `${summary.listening} listening`,
]
.filter(Boolean)
.join(" · ");
return (
<div className="rounded-large border border-zinc-100 bg-white p-5 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Text variant="h5">Agent Briefing</Text>
{headerSummary && (
<Text variant="small" className="text-zinc-500">
{headerSummary}
</Text>
)}
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setIsCollapsed(!isCollapsed)}
aria-label={isCollapsed ? "Expand briefing" : "Collapse briefing"}
>
{isCollapsed ? (
<CaretDownIcon size={16} />
) : (
<CaretUpIcon size={16} />
)}
</Button>
</div>
{!isCollapsed && (
<div className="mt-4 space-y-5">
<StatsGrid
summary={summary}
activeFilter={activeFilter}
onFilterChange={onFilterChange}
/>
<SitrepList agentIDs={agentIDs} />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import {
CurrencyDollarIcon,
PlayCircleIcon,
WarningCircleIcon,
EarIcon,
ClockIcon,
PauseCircleIcon,
} from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import type { FleetSummary, AgentStatusFilter } from "../../types";
interface Props {
summary: FleetSummary;
activeFilter: AgentStatusFilter;
onFilterChange?: (filter: AgentStatusFilter) => void;
}
export function StatsGrid({ summary, activeFilter, onFilterChange }: Props) {
const tiles = [
{
label: "Spend this month",
value: `$${summary.monthlySpend.toLocaleString()}`,
filter: "all" as AgentStatusFilter,
icon: CurrencyDollarIcon,
color: "text-zinc-700",
},
{
label: "Running now",
value: summary.running,
filter: "running" as AgentStatusFilter,
icon: PlayCircleIcon,
color: "text-blue-600",
},
{
label: "Needs attention",
value: summary.error,
filter: "attention" as AgentStatusFilter,
icon: WarningCircleIcon,
color: "text-red-500",
},
{
label: "Listening",
value: summary.listening,
filter: "listening" as AgentStatusFilter,
icon: EarIcon,
color: "text-purple-500",
},
{
label: "Scheduled",
value: summary.scheduled,
filter: "scheduled" as AgentStatusFilter,
icon: ClockIcon,
color: "text-yellow-600",
},
{
label: "Idle",
value: summary.idle,
filter: "idle" as AgentStatusFilter,
icon: PauseCircleIcon,
color: "text-zinc-400",
},
];
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{tiles.map((tile) => {
const Icon = tile.icon;
const isActive = activeFilter === tile.filter;
return (
<button
key={tile.label}
type="button"
onClick={() => onFilterChange?.(tile.filter)}
className={cn(
"flex flex-col gap-1 rounded-medium border p-3 text-left transition-all hover:shadow-sm",
isActive
? "border-zinc-900 bg-zinc-50"
: "border-zinc-100 bg-white",
)}
>
<div className="flex items-center gap-1.5">
<Icon size={14} className={tile.color} />
<Text variant="small" className="text-zinc-500">
{tile.label}
</Text>
</div>
<Text variant="h4" className={tile.color}>
{tile.value}
</Text>
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,53 @@
"use client";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/__legacy__/ui/select";
import { FunnelIcon } from "@phosphor-icons/react";
import type { AgentStatusFilter, FleetSummary } from "../../types";
interface Props {
value: AgentStatusFilter;
onChange: (value: AgentStatusFilter) => void;
summary: FleetSummary;
}
export function AgentFilterMenu({ value, onChange, summary }: Props) {
function handleChange(val: string) {
onChange(val as AgentStatusFilter);
}
return (
<div className="flex items-center" data-testid="agent-filter-dropdown">
<span className="hidden whitespace-nowrap text-sm sm:inline">filter</span>
<Select value={value} onValueChange={handleChange}>
<SelectTrigger className="ml-1 w-fit space-x-1 border-none px-0 text-sm underline underline-offset-4 shadow-none">
<FunnelIcon className="h-4 w-4 sm:hidden" />
<SelectValue placeholder="All Agents" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All Agents</SelectItem>
<SelectItem value="running">Running ({summary.running})</SelectItem>
<SelectItem value="attention">
Needs Attention ({summary.error})
</SelectItem>
<SelectItem value="listening">
Listening ({summary.listening})
</SelectItem>
<SelectItem value="scheduled">
Scheduled ({summary.scheduled})
</SelectItem>
<SelectItem value="idle">Idle / Stale ({summary.idle})</SelectItem>
<SelectItem value="healthy">Healthy</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import {
EyeIcon,
ArrowsClockwiseIcon,
MonitorPlayIcon,
PlayIcon,
ArrowCounterClockwiseIcon,
} from "@phosphor-icons/react";
import { useToast } from "@/components/molecules/Toast/use-toast";
import type { AgentStatus } from "../../types";
interface Props {
status: AgentStatus;
agentID: string;
className?: string;
}
/**
* Renders the single most relevant action for an agent based on its status.
*
* | Status | Action | Behaviour (TODO: wire to real endpoints) |
* |-----------|-----------------|------------------------------------------|
* | error | View error | Opens error detail / run log |
* | listening | Reconnect | Opens reconnection flow |
* | running | Watch live | Opens real-time execution view |
* | idle | Run now | Triggers immediate new run |
* | scheduled | Run now | Triggers immediate new run |
*/
export function ContextualActionButton({ status, agentID, className }: Props) {
const { toast } = useToast();
const config = ACTION_CONFIG[status];
if (!config) return null;
const Icon = config.icon;
function handleClick(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
// TODO: Replace with real API calls
toast({
title: config.label,
description: `${config.label} triggered for agent ${agentID.slice(0, 8)}`,
});
}
return (
<Button
variant="outline"
size="small"
onClick={handleClick}
leftIcon={<Icon size={14} />}
className={className}
>
{config.label}
</Button>
);
}
const ACTION_CONFIG: Record<
AgentStatus,
{ label: string; icon: typeof EyeIcon }
> = {
error: { label: "View error", icon: EyeIcon },
listening: { label: "Reconnect", icon: ArrowsClockwiseIcon },
running: { label: "Watch live", icon: MonitorPlayIcon },
idle: { label: "Run now", icon: PlayIcon },
scheduled: { label: "Run now", icon: ArrowCounterClockwiseIcon },
};

View File

@@ -12,10 +12,16 @@ import Avatar, {
AvatarImage,
} from "@/components/atoms/Avatar/Avatar";
import { Link } from "@/components/atoms/Link/Link";
import { Progress } from "@/components/atoms/Progress/Progress";
import { cn } from "@/lib/utils";
import { AgentCardMenu } from "./components/AgentCardMenu";
import { FavoriteButton } from "./components/FavoriteButton";
import { useLibraryAgentCard } from "./useLibraryAgentCard";
import { useFavoriteAnimation } from "../../context/FavoriteAnimationContext";
import { StatusBadge } from "../StatusBadge/StatusBadge";
import { ContextualActionButton } from "../ContextualActionButton/ContextualActionButton";
import { useAgentStatus } from "../../hooks/useAgentStatus";
import { formatTimeAgo } from "../../helpers";
interface Props {
agent: LibraryAgent;
@@ -25,6 +31,7 @@ interface Props {
export function LibraryAgentCard({ agent, draggable = true }: Props) {
const { id, name, graph_id, can_access_graph, image_url } = agent;
const { triggerFavoriteAnimation } = useFavoriteAnimation();
const statusInfo = useAgentStatus(id);
function handleDragStart(e: React.DragEvent<HTMLDivElement>) {
e.dataTransfer.setData("application/agent-id", id);
@@ -42,6 +49,9 @@ export function LibraryAgentCard({ agent, draggable = true }: Props) {
onFavoriteAdd: triggerFavoriteAnimation,
});
const hasError = statusInfo.status === "error";
const isRunning = statusInfo.status === "running";
return (
<div
draggable={draggable}
@@ -52,7 +62,12 @@ export function LibraryAgentCard({ agent, draggable = true }: Props) {
layoutId={`agent-card-${id}`}
data-testid="library-agent-card"
data-agent-id={id}
className="group relative inline-flex h-[10.625rem] w-full max-w-[25rem] flex-col items-start justify-start gap-2.5 rounded-medium border border-zinc-100 bg-white hover:shadow-md"
className={cn(
"group relative inline-flex h-auto min-h-[10.625rem] w-full max-w-[25rem] flex-col items-start justify-start gap-2.5 rounded-medium border bg-white hover:shadow-md",
hasError
? "border-l-2 border-b-zinc-100 border-l-red-400 border-r-zinc-100 border-t-zinc-100"
: "border-zinc-100",
)}
transition={{
type: "spring",
damping: 25,
@@ -79,6 +94,7 @@ export function LibraryAgentCard({ agent, draggable = true }: Props) {
>
{isFromMarketplace ? "FROM MARKETPLACE" : "Built by you"}
</Text>
<StatusBadge status={statusInfo.status} className="ml-auto" />
</div>
</NextLink>
<FavoriteButton
@@ -128,26 +144,65 @@ export function LibraryAgentCard({ agent, draggable = true }: Props) {
)}
</Link>
<div className="mt-auto flex w-full justify-start gap-6 border-t border-zinc-100 pb-1 pt-3">
<Link
href={`/library/agents/${id}`}
data-testid="library-agent-card-see-runs-link"
className="flex items-center gap-1 text-[13px]"
>
See runs <CaretCircleRightIcon size={20} />
</Link>
{/* Status details: progress bar, error message, stats */}
{isRunning && statusInfo.progress !== null && (
<div className="mt-1 flex items-center gap-2">
<Progress value={statusInfo.progress} className="h-1.5 flex-1" />
<Text variant="small" className="text-blue-600">
{statusInfo.progress}%
</Text>
</div>
)}
{can_access_graph && (
<Link
href={`/build?flowID=${graph_id}`}
data-testid="library-agent-card-open-in-builder-link"
className="flex items-center gap-1 text-[13px]"
isExternal
>
Open in builder <CaretCircleRightIcon size={20} />
</Link>
{hasError && statusInfo.lastError && (
<Text variant="small" className="mt-1 line-clamp-1 text-red-500">
{statusInfo.lastError}
</Text>
)}
<div className="mt-1 flex items-center gap-3">
<Text variant="small" className="text-zinc-400">
{statusInfo.totalRuns} runs
</Text>
<Text variant="small" className="text-zinc-400">
${statusInfo.monthlySpend}
</Text>
{statusInfo.lastRunAt && (
<Text variant="small" className="text-zinc-400">
{formatTimeAgo(statusInfo.lastRunAt)}
</Text>
)}
</div>
<div className="mt-auto flex w-full items-center justify-between gap-2 border-t border-zinc-100 pb-1 pt-3">
<div className="flex gap-6">
<Link
href={`/library/agents/${id}`}
data-testid="library-agent-card-see-runs-link"
className="flex items-center gap-1 text-[13px]"
>
See runs <CaretCircleRightIcon size={20} />
</Link>
{can_access_graph && (
<Link
href={`/build?flowID=${graph_id}`}
data-testid="library-agent-card-open-in-builder-link"
className="flex items-center gap-1 text-[13px]"
isExternal
>
Open in builder <CaretCircleRightIcon size={20} />
</Link>
)}
</div>
<div className="opacity-0 transition-opacity group-hover:opacity-100">
<ContextualActionButton
status={statusInfo.status}
agentID={id}
className="text-xs"
/>
</div>
</div>
</div>
</motion.div>
</div>

View File

@@ -16,8 +16,9 @@ import {
} from "framer-motion";
import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolderEditDialog";
import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog";
import { LibraryTab } from "../../types";
import type { LibraryTab, AgentStatusFilter, FleetSummary } from "../../types";
import { useLibraryAgentList } from "./useLibraryAgentList";
import { AgentBriefingPanel } from "../AgentBriefingPanel/AgentBriefingPanel";
// cancels the current spring and starts a new one from current state.
const containerVariants = {
@@ -70,6 +71,9 @@ interface Props {
tabs: LibraryTab[];
activeTab: string;
onTabChange: (tabId: string) => void;
statusFilter?: AgentStatusFilter;
onStatusFilterChange?: (filter: AgentStatusFilter) => void;
fleetSummary?: FleetSummary;
}
export function LibraryAgentList({
@@ -81,6 +85,9 @@ export function LibraryAgentList({
tabs,
activeTab,
onTabChange,
statusFilter = "all",
onStatusFilterChange,
fleetSummary,
}: Props) {
const shouldReduceMotion = useReducedMotion();
const activeContainerVariants = shouldReduceMotion
@@ -95,7 +102,8 @@ export function LibraryAgentList({
const {
isFavoritesTab,
agentLoading,
allAgentsCount,
displayedCount,
allAgentIDs,
favoritesCount,
agents,
hasNextPage,
@@ -116,18 +124,33 @@ export function LibraryAgentList({
selectedFolderId,
onFolderSelect,
activeTab,
statusFilter,
});
return (
<>
{!selectedFolderId && fleetSummary && (
<div className="mb-4">
<AgentBriefingPanel
summary={fleetSummary}
agentIDs={allAgentIDs}
onFilterChange={onStatusFilterChange}
activeFilter={statusFilter}
/>
</div>
)}
{!selectedFolderId && (
<LibrarySubSection
tabs={tabs}
activeTab={activeTab}
onTabChange={onTabChange}
allCount={allAgentsCount}
allCount={displayedCount}
favoritesCount={favoritesCount}
setLibrarySort={setLibrarySort}
statusFilter={statusFilter}
onStatusFilterChange={onStatusFilterChange}
fleetSummary={fleetSummary}
/>
)}

View File

@@ -22,6 +22,10 @@ import { useFavoriteAgents } from "../../hooks/useFavoriteAgents";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import type { AgentStatusFilter } from "../../types";
import { mockStatusForAgent } from "../../hooks/useAgentStatus";
const FILTER_EXHAUST_THRESHOLD = 3;
interface Props {
searchTerm: string;
@@ -29,6 +33,7 @@ interface Props {
selectedFolderId: string | null;
onFolderSelect: (folderId: string | null) => void;
activeTab: string;
statusFilter?: AgentStatusFilter;
}
export function useLibraryAgentList({
@@ -37,12 +42,15 @@ export function useLibraryAgentList({
selectedFolderId,
onFolderSelect,
activeTab,
statusFilter = "all",
}: Props) {
const isFavoritesTab = activeTab === "favorites";
const { toast } = useToast();
const stableQueryClient = getQueryClient();
const queryClient = useQueryClient();
const prevSortRef = useRef<LibraryAgentSort | null>(null);
const consecutiveEmptyPagesRef = useRef(0);
const prevFilteredLengthRef = useRef(0);
const [editingFolder, setEditingFolder] = useState<LibraryFolder | null>(
null,
@@ -199,6 +207,50 @@ export function useLibraryAgentList({
const showFolders = !isFavoritesTab;
// All loaded agent IDs (unfiltered) — used by AgentBriefingPanel so the
// sitrep always covers the full fleet, not just the currently filtered view.
const allAgentIDs = agents.map((a) => a.id);
// Client-side filter by status using mock data until the real API supports it.
const filteredAgents = filterAgentsByStatus(agents, statusFilter);
// Track consecutive pages that produced no new filtered items
useEffect(() => {
if (statusFilter === "all") {
consecutiveEmptyPagesRef.current = 0;
prevFilteredLengthRef.current = filteredAgents.length;
return;
}
const newFilteredCount = filteredAgents.length;
const previousCount = prevFilteredLengthRef.current;
if (newFilteredCount > previousCount) {
// New filtered items were added, reset counter
consecutiveEmptyPagesRef.current = 0;
} else if (!isFetchingNextPage && previousCount > 0) {
// No new items and not currently fetching means last fetch was empty
consecutiveEmptyPagesRef.current++;
}
prevFilteredLengthRef.current = newFilteredCount;
}, [filteredAgents.length, statusFilter, isFetchingNextPage]);
// Reset counter when statusFilter changes
useEffect(() => {
consecutiveEmptyPagesRef.current = 0;
prevFilteredLengthRef.current = 0;
}, [statusFilter]);
// Derive filteredExhausted: stop fetching when threshold reached
const filteredExhausted =
statusFilter !== "all" &&
consecutiveEmptyPagesRef.current >= FILTER_EXHAUST_THRESHOLD;
// When a filter is active, show the filtered count instead of the API total.
const displayedCount =
statusFilter === "all" ? allAgentsCount : filteredAgents.length;
function handleFolderDeleted() {
if (selectedFolderId === deletingFolder?.id) {
onFolderSelect(null);
@@ -210,9 +262,11 @@ export function useLibraryAgentList({
agentLoading,
agentCount,
allAgentsCount,
displayedCount,
allAgentIDs,
favoritesCount: favoriteAgentsData.agentCount,
agents,
hasNextPage: agentsHasNextPage,
agents: filteredAgents,
hasNextPage: agentsHasNextPage && !filteredExhausted,
isFetchingNextPage: agentsIsFetchingNextPage,
fetchNextPage: agentsFetchNextPage,
foldersData,
@@ -226,3 +280,16 @@ export function useLibraryAgentList({
handleFolderDeleted,
};
}
function filterAgentsByStatus<T extends { id: string }>(
agents: T[],
statusFilter: AgentStatusFilter,
): T[] {
if (statusFilter === "all") return agents;
return agents.filter((agent) => {
const info = mockStatusForAgent(agent.id);
if (statusFilter === "attention") return info.health === "attention";
if (statusFilter === "healthy") return info.health === "good";
return info.status === statusFilter;
});
}

View File

@@ -10,6 +10,8 @@ import {
} from "./FolderIcon";
import { useState } from "react";
import { PencilSimpleIcon, TrashIcon } from "@phosphor-icons/react";
import type { AgentStatus } from "../../types";
import { StatusBadge } from "../StatusBadge/StatusBadge";
interface Props {
id: string;
@@ -21,6 +23,8 @@ interface Props {
onDelete?: () => void;
onAgentDrop?: (agentId: string, folderId: string) => void;
onClick?: () => void;
/** Worst status among child agents (optional, for status aggregation). */
worstStatus?: AgentStatus;
}
export function LibraryFolder({
@@ -33,6 +37,7 @@ export function LibraryFolder({
onDelete,
onAgentDrop,
onClick,
worstStatus,
}: Props) {
const [isHovered, setIsHovered] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
@@ -86,13 +91,18 @@ export function LibraryFolder({
>
{name}
</Text>
<Text
variant="small"
className="text-zinc-500"
data-testid="library-folder-agent-count"
>
{agentCount} {agentCount === 1 ? "agent" : "agents"}
</Text>
<div className="flex items-center gap-2">
<Text
variant="small"
className="text-zinc-500"
data-testid="library-folder-agent-count"
>
{agentCount} {agentCount === 1 ? "agent" : "agents"}
</Text>
{worstStatus && worstStatus !== "idle" && (
<StatusBadge status={worstStatus} />
)}
</div>
</div>
{/* Right side - Custom folder icon */}

View File

@@ -6,9 +6,10 @@ import {
} from "@/components/molecules/TabsLine/TabsLine";
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
import { useFavoriteAnimation } from "../../context/FavoriteAnimationContext";
import { LibraryTab } from "../../types";
import type { LibraryTab, AgentStatusFilter, FleetSummary } from "../../types";
import LibraryFolderCreationDialog from "../LibraryFolderCreationDialog/LibraryFolderCreationDialog";
import { LibrarySortMenu } from "../LibrarySortMenu/LibrarySortMenu";
import { AgentFilterMenu } from "../AgentFilterMenu/AgentFilterMenu";
interface Props {
tabs: LibraryTab[];
@@ -17,6 +18,9 @@ interface Props {
allCount: number;
favoritesCount: number;
setLibrarySort: (value: LibraryAgentSort) => void;
statusFilter?: AgentStatusFilter;
onStatusFilterChange?: (filter: AgentStatusFilter) => void;
fleetSummary?: FleetSummary;
}
export function LibrarySubSection({
@@ -26,6 +30,9 @@ export function LibrarySubSection({
allCount,
favoritesCount,
setLibrarySort,
statusFilter = "all",
onStatusFilterChange,
fleetSummary,
}: Props) {
const { registerFavoritesTabRef } = useFavoriteAnimation();
const favoritesRef = useRef<HTMLButtonElement>(null);
@@ -70,6 +77,13 @@ export function LibrarySubSection({
</TabsLine>
<div className="hidden items-center gap-6 md:flex">
<LibraryFolderCreationDialog />
{fleetSummary && onStatusFilterChange && (
<AgentFilterMenu
value={statusFilter}
onChange={onStatusFilterChange}
summary={fleetSummary}
/>
)}
<LibrarySortMenu setLibrarySort={setLibrarySort} />
</div>
</div>

View File

@@ -0,0 +1,118 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import {
WarningCircleIcon,
PlayIcon,
ClockCountdownIcon,
CheckCircleIcon,
ChatCircleDotsIcon,
} from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import type { AgentStatus } from "../../types";
import { ContextualActionButton } from "../ContextualActionButton/ContextualActionButton";
export type SitrepPriority = "error" | "running" | "stale" | "success";
export interface SitrepItemData {
id: string;
agentID: string;
agentName: string;
priority: SitrepPriority;
message: string;
status: AgentStatus;
}
interface Props {
item: SitrepItemData;
onAskAutoPilot?: (prompt: string) => void;
}
const PRIORITY_CONFIG: Record<
SitrepPriority,
{ icon: typeof WarningCircleIcon; color: string; bg: string }
> = {
error: {
icon: WarningCircleIcon,
color: "text-red-500",
bg: "bg-red-50",
},
running: {
icon: PlayIcon,
color: "text-blue-600",
bg: "bg-blue-50",
},
stale: {
icon: ClockCountdownIcon,
color: "text-yellow-600",
bg: "bg-yellow-50",
},
success: {
icon: CheckCircleIcon,
color: "text-green-600",
bg: "bg-green-50",
},
};
export function SitrepItem({ item, onAskAutoPilot }: Props) {
const config = PRIORITY_CONFIG[item.priority];
const Icon = config.icon;
function handleAskAutoPilot() {
const prompt = buildAutoPilotPrompt(item);
onAskAutoPilot?.(prompt);
}
return (
<div
className={cn(
"group flex items-start gap-3 rounded-medium border border-transparent p-3 transition-colors hover:border-zinc-100 hover:bg-zinc-50/50",
)}
>
<div
className={cn(
"mt-0.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full",
config.bg,
)}
>
<Icon size={14} className={config.color} weight="fill" />
</div>
<div className="min-w-0 flex-1">
<Text variant="small-medium" className="text-zinc-900">
{item.agentName}
</Text>
<Text variant="small" className="mt-0.5 text-zinc-500">
{item.message}
</Text>
</div>
<div className="flex flex-shrink-0 items-center gap-1.5 opacity-0 transition-opacity group-hover:opacity-100">
<ContextualActionButton status={item.status} agentID={item.agentID} />
<Button
variant="ghost"
size="small"
onClick={handleAskAutoPilot}
leftIcon={<ChatCircleDotsIcon size={14} />}
className="text-xs"
>
Ask AutoPilot
</Button>
</div>
</div>
);
}
function buildAutoPilotPrompt(item: SitrepItemData): string {
switch (item.priority) {
case "error":
return `What happened with ${item.agentName}? It says "${item.message}" — can you check the logs and tell me what to fix?`;
case "running":
return `Give me a status update on the ${item.agentName} run — what has it found so far?`;
case "stale":
return `${item.agentName} hasn't run recently. Should I keep it or update and re-run it?`;
case "success":
return `How has ${item.agentName} been performing? Give me a quick summary of recent results.`;
}
}

View File

@@ -0,0 +1,44 @@
"use client";
import { Text } from "@/components/atoms/Text/Text";
import { useSitrepItems } from "./useSitrepItems";
import { SitrepItem } from "./SitrepItem";
import { useAutoPilotBridge } from "@/contexts/AutoPilotBridgeContext";
interface Props {
agentIDs: string[];
maxItems?: number;
}
export function SitrepList({ agentIDs, maxItems = 10 }: Props) {
const items = useSitrepItems(agentIDs, maxItems);
const { sendPrompt } = useAutoPilotBridge();
if (items.length === 0) {
return (
<div className="py-4 text-center">
<Text variant="small" className="text-zinc-400">
All agents are healthy nothing to report.
</Text>
</div>
);
}
return (
<div>
<div className="mb-2 flex items-center justify-between">
<Text variant="small-medium" className="text-zinc-700">
AI Summary
</Text>
<Text variant="small" className="text-zinc-400">
Updated just now
</Text>
</div>
<div className="space-y-1">
{items.map((item) => (
<SitrepItem key={item.id} item={item} onAskAutoPilot={sendPrompt} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import { useMemo } from "react";
import { mockStatusForAgent } from "../../hooks/useAgentStatus";
import type { SitrepItemData, SitrepPriority } from "./SitrepItem";
import type { AgentStatus } from "../../types";
/**
* Produce a prioritised list of sitrep items from agent IDs.
* Priority order: error → running → stale → success.
*
* TODO: Replace with `GET /agents/sitrep` once the backend endpoint exists.
*/
export function useSitrepItems(
agentIDs: string[],
maxItems: number,
): SitrepItemData[] {
const items = useMemo<SitrepItemData[]>(() => {
const raw: SitrepItemData[] = agentIDs.map((id) => {
const info = mockStatusForAgent(id);
return {
id,
agentID: id,
agentName: `Agent ${id.slice(0, 6)}`,
priority: toPriority(info.status, info.health === "stale"),
message: buildMessage(info.status, info.lastError, info.progress),
status: info.status,
};
});
const order: Record<SitrepPriority, number> = {
error: 0,
running: 1,
stale: 2,
success: 3,
};
raw.sort((a, b) => order[a.priority] - order[b.priority]);
return raw.slice(0, maxItems);
}, [agentIDs, maxItems]);
return items;
}
function toPriority(status: AgentStatus, isStale: boolean): SitrepPriority {
if (status === "error") return "error";
if (status === "running") return "running";
if (isStale || status === "idle") return "stale";
return "success";
}
function buildMessage(
status: AgentStatus,
lastError: string | null,
progress: number | null,
): string {
switch (status) {
case "error":
return lastError ?? "Unknown error occurred";
case "running":
return progress !== null
? `${progress}% complete`
: "Currently executing";
case "idle":
return "Hasn't run recently — still relevant?";
case "listening":
return "Waiting for trigger event";
case "scheduled":
return "Next run scheduled";
}
}

View File

@@ -0,0 +1,86 @@
"use client";
import { cn } from "@/lib/utils";
import type { AgentStatus } from "../../types";
const STATUS_CONFIG: Record<
AgentStatus,
{ label: string; bg: string; text: string; pulse: boolean }
> = {
running: {
label: "Running",
bg: "bg-blue-50",
text: "text-blue-600",
pulse: true,
},
error: {
label: "Error",
bg: "bg-red-50",
text: "text-red-500",
pulse: false,
},
listening: {
label: "Listening",
bg: "bg-purple-50",
text: "text-purple-500",
pulse: true,
},
scheduled: {
label: "Scheduled",
bg: "bg-yellow-50",
text: "text-yellow-600",
pulse: false,
},
idle: {
label: "Idle",
bg: "bg-zinc-100",
text: "text-zinc-500",
pulse: false,
},
};
interface Props {
status: AgentStatus;
className?: string;
}
export function StatusBadge({ status, className }: Props) {
const config = STATUS_CONFIG[status];
return (
<span
className={cn(
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium",
config.bg,
config.text,
className,
)}
>
<span
className={cn(
"inline-block h-1.5 w-1.5 rounded-full",
config.pulse && "animate-pulse",
statusDotColor(status),
)}
/>
{config.label}
</span>
);
}
function statusDotColor(status: AgentStatus): string {
switch (status) {
case "running":
return "bg-blue-500";
case "error":
return "bg-red-500";
case "listening":
return "bg-purple-500";
case "scheduled":
return "bg-yellow-500";
case "idle":
return "bg-zinc-400";
}
}
export { STATUS_CONFIG };

View File

@@ -0,0 +1,16 @@
/**
* Formats an ISO date string into a human-readable relative time string.
* e.g. "3m ago", "2h ago", "5d ago".
*/
export function formatTimeAgo(isoDate: string): string {
const parsed = new Date(isoDate).getTime();
if (Number.isNaN(parsed)) return "unknown";
const diff = Date.now() - parsed;
if (diff < 0) return "just now";
const minutes = Math.floor(diff / 60000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}

View File

@@ -0,0 +1,113 @@
"use client";
import { useMemo, useState } from "react";
import type {
AgentStatus,
AgentHealth,
AgentStatusInfo,
FleetSummary,
} from "../types";
/**
* Derive health from status and recency.
* TODO: Replace with real computation once backend provides the data.
*/
function deriveHealth(
status: AgentStatus,
lastRunAt: string | null,
): AgentHealth {
if (status === "error") return "attention";
if (status === "idle" && lastRunAt) {
const daysSince =
(Date.now() - new Date(lastRunAt).getTime()) / (1000 * 60 * 60 * 24);
if (daysSince > 14) return "stale";
}
return "good";
}
/**
* Generate deterministic mock status for an agent based on its ID.
* This allows the UI to render realistic data before the real API is built.
* TODO: Replace with real API call `GET /agents/:id/status`.
*/
function mockStatusForAgent(agentID: string): AgentStatusInfo {
const hash = simpleHash(agentID);
const statuses: AgentStatus[] = [
"running",
"error",
"listening",
"scheduled",
"idle",
];
const status = statuses[hash % statuses.length];
const progress = status === "running" ? (hash * 17) % 100 : null;
const totalRuns = (hash % 200) + 1;
const daysAgo = (hash % 30) + 1;
const lastRunAt = new Date(
Date.now() - daysAgo * 24 * 60 * 60 * 1000,
).toISOString();
const lastError =
status === "error" ? "API rate limit exceeded — paused" : null;
const monthlySpend = Number(((hash % 5000) / 100).toFixed(2));
return {
status,
health: deriveHealth(status, lastRunAt),
progress,
totalRuns,
lastRunAt,
lastError,
monthlySpend,
nextScheduledRun:
status === "scheduled"
? new Date(Date.now() + 3600_000).toISOString()
: null,
triggerType: status === "listening" ? "webhook" : null,
};
}
function simpleHash(str: string): number {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = (h * 31 + str.charCodeAt(i)) >>> 0;
}
return h;
}
/**
* Hook returning status info for a single agent.
* TODO: Wire to `GET /agents/:id/status` + WebSocket `/agents/live`.
*/
export function useAgentStatus(agentID: string): AgentStatusInfo {
// NOTE: useState initializer runs once on mount; a new agentID prop will not
// re-derive info. Replace with a real API call wired to the agentID param.
const [info] = useState(() => mockStatusForAgent(agentID));
return info;
}
/**
* Hook returning fleet-wide summary counts.
* TODO: Wire to `GET /agents/summary`.
*/
export function useFleetSummary(agentIDs: string[]): FleetSummary {
const summary = useMemo<FleetSummary>(() => {
const counts: FleetSummary = {
running: 0,
error: 0,
listening: 0,
scheduled: 0,
idle: 0,
monthlySpend: 0,
};
for (const id of agentIDs) {
const info = mockStatusForAgent(id);
counts[info.status] += 1;
counts.monthlySpend += info.monthlySpend;
}
counts.monthlySpend = Number(counts.monthlySpend.toFixed(2));
return counts;
}, [agentIDs]);
return summary;
}
export { mockStatusForAgent, deriveHealth };

View File

@@ -0,0 +1,25 @@
"use client";
import { useState } from "react";
import type { FleetSummary } from "../types";
/**
* Returns fleet-wide summary counts for the Agent Briefing Panel.
*
* TODO: Replace with a real `GET /agents/summary` API call once available.
* For now, returns deterministic mock data so the UI renders correctly.
*/
export function useLibraryFleetSummary(): FleetSummary {
// NOTE: useState initializer runs once on mount; the hardcoded mock values
// will not update if the component re-renders. Replace with a real API call
// once the backend endpoint is available.
const [summary] = useState<FleetSummary>(() => ({
running: 3,
error: 2,
listening: 4,
scheduled: 5,
idle: 8,
monthlySpend: 127.45,
}));
return summary;
}

View File

@@ -7,7 +7,8 @@ import { LibraryActionHeader } from "./components/LibraryActionHeader/LibraryAct
import { LibraryAgentList } from "./components/LibraryAgentList/LibraryAgentList";
import { useLibraryListPage } from "./components/useLibraryListPage";
import { FavoriteAnimationProvider } from "./context/FavoriteAnimationContext";
import { LibraryTab } from "./types";
import type { LibraryTab, AgentStatusFilter } from "./types";
import { useLibraryFleetSummary } from "./hooks/useLibraryFleetSummary";
const LIBRARY_TABS: LibraryTab[] = [
{ id: "all", title: "All", icon: ListIcon },
@@ -19,6 +20,8 @@ export default function LibraryPage() {
useLibraryListPage();
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(LIBRARY_TABS[0].id);
const [statusFilter, setStatusFilter] = useState<AgentStatusFilter>("all");
const fleetSummary = useLibraryFleetSummary();
useEffect(() => {
document.title = "Library AutoGPT Platform";
@@ -50,6 +53,9 @@ export default function LibraryPage() {
tabs={LIBRARY_TABS}
activeTab={activeTab}
onTabChange={handleTabChange}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
fleetSummary={fleetSummary}
/>
</main>
</FavoriteAnimationProvider>

View File

@@ -1,7 +1,52 @@
import { Icon } from "@phosphor-icons/react";
import type { Icon } from "@phosphor-icons/react";
export interface LibraryTab {
id: string;
title: string;
icon: Icon;
}
/** Agent execution status — drives StatusBadge visuals & filtering. */
export type AgentStatus =
| "running"
| "error"
| "listening"
| "scheduled"
| "idle";
/** Derived health bucket for quick triage. */
export type AgentHealth = "good" | "attention" | "stale";
/** Real-time metadata that powers the Intelligence Layer features. */
export interface AgentStatusInfo {
status: AgentStatus;
health: AgentHealth;
/** 0-100 progress for currently running agents. */
progress: number | null;
totalRuns: number;
lastRunAt: string | null;
lastError: string | null;
monthlySpend: number;
nextScheduledRun: string | null;
triggerType: string | null;
}
/** Fleet-wide aggregate counts used by the Briefing Panel stats grid. */
export interface FleetSummary {
running: number;
error: number;
listening: number;
scheduled: number;
idle: number;
monthlySpend: number;
}
/** Filter options for the agent filter dropdown. */
export type AgentStatusFilter =
| "all"
| "running"
| "attention"
| "listening"
| "scheduled"
| "idle"
| "healthy";

View File

@@ -0,0 +1,73 @@
"use client";
import { createContext, useContext, useCallback, useState } from "react";
import { useRouter } from "next/navigation";
const STORAGE_KEY = "autopilot_pending_prompt";
interface AutoPilotBridgeState {
/** Pending prompt to be injected into AutoPilot chat. */
pendingPrompt: string | null;
/** Queue a prompt that the Home/Copilot tab will pick up. */
sendPrompt: (prompt: string) => void;
/** Consume and clear the pending prompt (called by the chat page). */
consumePrompt: () => string | null;
}
const AutoPilotBridgeContext = createContext<AutoPilotBridgeState | null>(null);
interface Props {
children: React.ReactNode;
}
export function AutoPilotBridgeProvider({ children }: Props) {
const router = useRouter();
// Hydrate from sessionStorage in case we just navigated here
const [pendingPrompt, setPendingPrompt] = useState<string | null>(() => {
if (typeof window === "undefined") return null;
return sessionStorage.getItem(STORAGE_KEY);
});
const sendPrompt = useCallback(
(prompt: string) => {
// Persist to sessionStorage so it survives client-side navigation
sessionStorage.setItem(STORAGE_KEY, prompt);
setPendingPrompt(prompt);
// Use Next.js router for client-side navigation (preserves React tree)
router.push("/");
},
[router],
);
const consumePrompt = useCallback((): string | null => {
const prompt = pendingPrompt ?? sessionStorage.getItem(STORAGE_KEY);
if (prompt !== null) {
sessionStorage.removeItem(STORAGE_KEY);
setPendingPrompt(null);
}
return prompt;
}, [pendingPrompt]);
return (
<AutoPilotBridgeContext.Provider
value={{ pendingPrompt, sendPrompt, consumePrompt }}
>
{children}
</AutoPilotBridgeContext.Provider>
);
}
export function useAutoPilotBridge(): AutoPilotBridgeState {
const context = useContext(AutoPilotBridgeContext);
if (!context) {
// Return a no-op implementation when used outside the provider
// (e.g. in tests or isolated component renders).
return {
pendingPrompt: null,
sendPrompt: () => {},
consumePrompt: () => null,
};
}
return context;
}