import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Clock, FlaskConical, Play, Search } from "lucide-react"; import type { IssueGraphLivenessAutoRecoveryPreview, PatchInstanceExperimentalSettings, } from "@paperclipai/shared"; import { instanceSettingsApi } from "@/api/instanceSettings"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; function issueHref(identifier: string | null, issueId: string) { if (!identifier) return `/issues/${issueId}`; const prefix = identifier.split("-")[0] || "PAP"; return `/${prefix}/issues/${identifier}`; } function formatRecoveryState(state: string) { return state.replace(/_/g, " "); } function RecoveryPreviewDialog({ preview, open, onOpenChange, onEnableOnly, onEnableAndRun, isPending, }: { preview: IssueGraphLivenessAutoRecoveryPreview | null; open: boolean; onOpenChange: (open: boolean) => void; onEnableOnly: () => void; onEnableAndRun: () => void; isPending: boolean; }) { const count = preview?.recoverableFindings ?? 0; return ( Confirm auto-recovery {preview ? `${count} recovery ${count === 1 ? "task" : "tasks"} match the last ${preview.lookbackHours} hours.` : "Checking recovery candidates before enabling."}
{preview && preview.items.length === 0 ? (
No recovery tasks would be created right now. Auto-recovery can still run for future liveness incidents in this window.
) : null} {preview?.items.map((item) => (
{item.identifier ?? item.issueId} {formatRecoveryState(item.state)}

{item.title}

{item.reason}

Recovery target:{" "} {item.recoveryIdentifier ?? item.recoveryIssueId}
))}
{preview && preview.skippedOutsideLookback > 0 ? (

{preview.skippedOutsideLookback} current{" "} {preview.skippedOutsideLookback === 1 ? "finding is" : "findings are"} outside the configured lookback and will not be touched.

) : null}
); } export function InstanceExperimentalSettings() { const { setBreadcrumbs } = useBreadcrumbs(); const queryClient = useQueryClient(); const [actionError, setActionError] = useState(null); const [lookbackHoursDraft, setLookbackHoursDraft] = useState("24"); const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [pendingPreview, setPendingPreview] = useState(null); useEffect(() => { setBreadcrumbs([ { label: "Instance Settings" }, { label: "Experimental" }, ]); }, [setBreadcrumbs]); const experimentalQuery = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), }); const toggleMutation = useMutation({ mutationFn: async (patch: PatchInstanceExperimentalSettings) => instanceSettingsApi.updateExperimental(patch), onSuccess: async () => { setActionError(null); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.instance.experimentalSettings }), queryClient.invalidateQueries({ queryKey: queryKeys.health }), ]); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to update experimental settings."); }, }); const previewMutation = useMutation({ mutationFn: async (lookbackHours: number) => instanceSettingsApi.previewIssueGraphLivenessAutoRecovery({ lookbackHours }), onSuccess: (preview) => { setActionError(null); setPendingPreview(preview); setPreviewDialogOpen(true); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to preview recovery tasks."); }, }); const runRecoveryMutation = useMutation({ mutationFn: async (lookbackHours: number) => instanceSettingsApi.runIssueGraphLivenessAutoRecovery({ lookbackHours }), onSuccess: async () => { setActionError(null); setPreviewDialogOpen(false); await Promise.all([ queryClient.invalidateQueries({ queryKey: queryKeys.instance.experimentalSettings }), queryClient.invalidateQueries({ queryKey: queryKeys.health }), ]); }, onError: (error) => { setActionError(error instanceof Error ? error.message : "Failed to create recovery tasks."); }, }); useEffect(() => { const next = experimentalQuery.data?.issueGraphLivenessAutoRecoveryLookbackHours; if (typeof next === "number") { setLookbackHoursDraft(String(next)); } }, [experimentalQuery.data?.issueGraphLivenessAutoRecoveryLookbackHours]); if (experimentalQuery.isLoading) { return
Loading experimental settings...
; } if (experimentalQuery.error) { return (
{experimentalQuery.error instanceof Error ? experimentalQuery.error.message : "Failed to load experimental settings."}
); } const enableEnvironments = experimentalQuery.data?.enableEnvironments === true; const enableIsolatedWorkspaces = experimentalQuery.data?.enableIsolatedWorkspaces === true; const enableNewestFirstIssueThread = experimentalQuery.data?.enableNewestFirstIssueThread === true; const autoRestartDevServerWhenIdle = experimentalQuery.data?.autoRestartDevServerWhenIdle === true; const enableIssueGraphLivenessAutoRecovery = experimentalQuery.data?.enableIssueGraphLivenessAutoRecovery === true; const lookbackHours = experimentalQuery.data?.issueGraphLivenessAutoRecoveryLookbackHours ?? 24; const parsedLookbackHours = Number.parseInt(lookbackHoursDraft, 10); const lookbackHoursIsValid = Number.isInteger(parsedLookbackHours) && parsedLookbackHours >= 1 && parsedLookbackHours <= 720; const recoveryActionPending = toggleMutation.isPending || previewMutation.isPending || runRecoveryMutation.isPending; function previewForEnable() { if (!lookbackHoursIsValid) { setActionError("Lookback hours must be a whole number from 1 to 720."); return; } previewMutation.mutate(parsedLookbackHours); } function enableOnly() { if (!lookbackHoursIsValid) return; toggleMutation.mutate({ enableIssueGraphLivenessAutoRecovery: true, issueGraphLivenessAutoRecoveryLookbackHours: parsedLookbackHours, }, { onSuccess: () => setPreviewDialogOpen(false), }); } function enableAndRun() { if (!lookbackHoursIsValid) return; toggleMutation.mutate({ enableIssueGraphLivenessAutoRecovery: true, issueGraphLivenessAutoRecoveryLookbackHours: parsedLookbackHours, }, { onSuccess: () => runRecoveryMutation.mutate(parsedLookbackHours), }); } return (

Experimental

Opt into features that are still being evaluated before they become default behavior.

{actionError && (
{actionError}
)}

Enable Environments

Show environment management in company settings and allow project and agent environment assignment controls.

toggleMutation.mutate({ enableEnvironments: !enableEnvironments })} disabled={toggleMutation.isPending} aria-label="Toggle environments experimental setting" />

Enable Isolated Workspaces

Show execution workspace controls in project configuration and allow isolated workspace behavior for new and existing issue runs.

toggleMutation.mutate({ enableIsolatedWorkspaces: !enableIsolatedWorkspaces })} disabled={toggleMutation.isPending} aria-label="Toggle isolated workspaces experimental setting" />

Enable Newest-First Issue Thread

Show issue comments and messages with the newest activity first, move the jump control to the bottom of the page, and surface plain comment timestamps in the header area.

toggleMutation.mutate({ enableNewestFirstIssueThread: !enableNewestFirstIssueThread })} disabled={toggleMutation.isPending} aria-label="Toggle newest-first issue thread experimental setting" />

Auto-Restart Dev Server When Idle

In `pnpm dev:once`, wait for all queued and running local agent runs to finish, then restart the server automatically when backend changes or migrations make the current boot stale.

toggleMutation.mutate({ autoRestartDevServerWhenIdle: !autoRestartDevServerWhenIdle })} disabled={toggleMutation.isPending} aria-label="Toggle guarded dev-server auto-restart" />

Auto-Create Issue Recovery Tasks

Let the heartbeat scheduler create recovery issues for issue dependency chains found inside the configured lookback window.

{ if (enableIssueGraphLivenessAutoRecovery) { toggleMutation.mutate({ enableIssueGraphLivenessAutoRecovery: false }); return; } previewForEnable(); }} disabled={recoveryActionPending} aria-label="Toggle issue graph liveness auto-recovery" />

Current window: last {lookbackHours} {lookbackHours === 1 ? "hour" : "hours"}.

); }