import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"; import { Link } from "@tanstack/react-router"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { toast } from "sonner"; import type { ArtifactId, ArtifactPreview as ArtifactPreviewValue } from "@executor-js/sdk/shared"; import { trackEvent } from "../api/analytics"; import { usePreloadArtifactRenderer } from "../api/artifact-renderer"; import { artifactsOptimisticAtom, removeArtifactOptimistic, renameArtifactOptimistic, } from "../api/atoms"; import { artifactWriteKeys } from "../api/reactivity-keys"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "../components/alert-dialog"; import { Button } from "../components/button"; import { ArtifactPreview } from "../components/artifact-preview"; import { ErrorState } from "../components/error-state"; import { PageContainer, PageHeader } from "../components/page"; import { isAsyncResultLoading } from "../lib/async-result"; import { useExecutorDocumentTitle } from "../lib/document-title"; import { formatRelativeTime } from "../lib/relative-time"; import { RenameArtifactDialog } from "./artifact-rename-dialog"; /** The wire row `artifacts.list` returns — no `code`, so the list stays cheap. * The preview is the exception: the gallery is what draws it. */ interface ArtifactSummary { readonly id: ArtifactId; readonly title: string; readonly description: string | null; readonly preview: ArtifactPreviewValue | null; readonly updatedAt: number; } const LoadingState = () => (

Loading artifacts…

); /** * Artifacts are never created here — a model makes them by calling `create-artifact` * over MCP — so the empty state teaches that path rather than offering a button * that cannot exist. */ const EmptyState = () => (

No artifacts yet. Ask an agent to render a UI and it appears here, ready to reopen.

); /** * One artifact, as a gallery card. * * The whole card is the link — the preview is the biggest, most obvious target, * and a title-only hit area would waste it. The row actions therefore cannot be * nested inside it (a button inside an anchor is invalid, and the click would * navigate through them), so the link is a full-bleed overlay and the actions * sit above it on the z-axis. That keeps one accessible name for the card and * one focus stop, with the actions as their own stops after it. */ function ArtifactCard(props: { readonly artifact: ArtifactSummary; readonly onRename: (title: string) => void; readonly onRemove: () => void; }) { const { artifact } = props; return (
{/* 16:10 preview, on its own plane, separated by the same hairline the rest of the console uses. */}

trackEvent("artifact_opened", { surface: "list" })} > {artifact.title}

{artifact.description ? (

{artifact.description}

) : null} {formatRelativeTime(artifact.updatedAt)}
{/* Above the link overlay, so these stay clickable. Revealed on hover or focus, and pinned open while their own dialog is. */}
Rename } /> Delete {artifact.title}? This removes the artifact for good. Agents will no longer find it by name. Cancel Delete Artifact
); } export function ArtifactsPage() { useExecutorDocumentTitle("Artifacts"); // Opening a card is the only thing this page is for, and the renderer chunk is // the slowest part of doing so. Fetching it while the user is still choosing // means the detail page never has to wait for it — the renderer's own // placeholder simply stops existing for anyone arriving from here. usePreloadArtifactRenderer(); const artifacts = useAtomValue(artifactsOptimisticAtom); const refreshArtifacts = useAtomRefresh(artifactsOptimisticAtom); const doRename = useAtomSet(renameArtifactOptimistic, { mode: "promiseExit" }); const doRemove = useAtomSet(removeArtifactOptimistic, { mode: "promiseExit" }); const handleRename = async (artifactId: ArtifactId, title: string) => { const exit = await doRename({ params: { artifactId }, payload: { title }, reactivityKeys: artifactWriteKeys, }); trackEvent("artifact_renamed", { success: Exit.isSuccess(exit) }); if (Exit.isFailure(exit)) toast.error("Couldn't rename the artifact. Try again."); }; const handleRemove = async (artifactId: ArtifactId) => { const exit = await doRemove({ params: { artifactId }, reactivityKeys: artifactWriteKeys, }); trackEvent("artifact_removed", { success: Exit.isSuccess(exit) }); if (Exit.isFailure(exit)) toast.error("Couldn't delete the artifact. Try again."); }; return ( // Wider than the settings column: three preview cards need the room, and // this page is a gallery rather than a form. {isAsyncResultLoading(artifacts) ? ( ) : ( AsyncResult.match(artifacts, { onInitial: () => , onFailure: () => ( ), onSuccess: ({ value }) => { // Most-recently-updated first: an artifact just generated by an // agent is the one the user is most likely coming here to open. const rows = [...value].sort((a, b) => b.updatedAt - a.updatedAt); return (

Saved artifacts {rows.length > 0 ? ( {rows.length} ) : null}

{rows.length === 0 ? ( ) : (
{rows.map((artifact) => ( void handleRename(artifact.id, title)} onRemove={() => void handleRemove(artifact.id)} /> ))}
)}
); }, }) )}
); } export type { ArtifactSummary };