()
| 164 | } |
| 165 | |
| 166 | export function ArtifactsPage() { |
| 167 | useExecutorDocumentTitle("Artifacts"); |
| 168 | // Opening a card is the only thing this page is for, and the renderer chunk is |
| 169 | // the slowest part of doing so. Fetching it while the user is still choosing |
| 170 | // means the detail page never has to wait for it — the renderer's own |
| 171 | // placeholder simply stops existing for anyone arriving from here. |
| 172 | usePreloadArtifactRenderer(); |
| 173 | const artifacts = useAtomValue(artifactsOptimisticAtom); |
| 174 | const refreshArtifacts = useAtomRefresh(artifactsOptimisticAtom); |
| 175 | const doRename = useAtomSet(renameArtifactOptimistic, { mode: "promiseExit" }); |
| 176 | const doRemove = useAtomSet(removeArtifactOptimistic, { mode: "promiseExit" }); |
| 177 | |
| 178 | const handleRename = async (artifactId: ArtifactId, title: string) => { |
| 179 | const exit = await doRename({ |
| 180 | params: { artifactId }, |
| 181 | payload: { title }, |
| 182 | reactivityKeys: artifactWriteKeys, |
| 183 | }); |
| 184 | trackEvent("artifact_renamed", { success: Exit.isSuccess(exit) }); |
| 185 | if (Exit.isFailure(exit)) toast.error("Couldn't rename the artifact. Try again."); |
| 186 | }; |
| 187 | |
| 188 | const handleRemove = async (artifactId: ArtifactId) => { |
| 189 | const exit = await doRemove({ |
| 190 | params: { artifactId }, |
| 191 | reactivityKeys: artifactWriteKeys, |
| 192 | }); |
| 193 | trackEvent("artifact_removed", { success: Exit.isSuccess(exit) }); |
| 194 | if (Exit.isFailure(exit)) toast.error("Couldn't delete the artifact. Try again."); |
| 195 | }; |
| 196 | |
| 197 | return ( |
| 198 | // Wider than the settings column: three preview cards need the room, and |
| 199 | // this page is a gallery rather than a form. |
| 200 | <PageContainer className="max-w-6xl"> |
| 201 | <PageHeader |
| 202 | title="Artifacts" |
| 203 | description="Interactive components your agents generated. Ask an agent to render a UI and it is saved here — reopen it any time, or have the agent bring it back by name." |
| 204 | /> |
| 205 | |
| 206 | {isAsyncResultLoading(artifacts) ? ( |
| 207 | <LoadingState /> |
| 208 | ) : ( |
| 209 | AsyncResult.match(artifacts, { |
| 210 | onInitial: () => <LoadingState />, |
| 211 | onFailure: () => ( |
| 212 | <ErrorState message="Failed to load artifacts" onRetry={refreshArtifacts} /> |
| 213 | ), |
| 214 | onSuccess: ({ value }) => { |
| 215 | // Most-recently-updated first: an artifact just generated by an |
| 216 | // agent is the one the user is most likely coming here to open. |
| 217 | const rows = [...value].sort((a, b) => b.updatedAt - a.updatedAt); |
| 218 | return ( |
| 219 | <section> |
| 220 | <h2 className="mb-3 flex items-center font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground"> |
| 221 | Saved artifacts |
| 222 | {rows.length > 0 ? ( |
| 223 | <span className="ml-2 font-normal tabular-nums">{rows.length}</span> |
nothing calls this directly
no test coverage detected