import { useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, CircleAlert, FlaskConical, LoaderCircle, ScrollText } from "lucide-react"; import { asPiManagementApiError, getPiManagementLogs, getPiManagementStatus, runPiManagementTest, } from "../api/pi-management"; import { listModels } from "../api/models"; import { Button } from "../components/ui/button"; import { nextTabIndex } from "../components/ui/tab-navigation"; import { WorkAreaPanel } from "./WorkAreaPanel"; type Feedback = { tone: "success" | "error"; message: string } | undefined; type SmokeState = "passed" | "failed" | undefined; function errorMessage(error: unknown, fallback: string): string { return asPiManagementApiError(error)?.message ?? fallback; } function ReadinessRail({ ready, configured, credentials, smokeState }: { ready: boolean; configured: boolean; credentials: "present" | "missing"; smokeState: SmokeState; }) { return (
); } function RailItem({ label, value, state }: { label: string; value: string; state: "ready" | "attention" | "idle" }) { const iconClass = state === "ready" ? "text-[oklch(var(--success))]" : state === "attention" ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground"; return

{label}

{state === "ready" ? : state === "attention" ? : } {value}

; } type PiPlatform = "linux" | "macos" | "windows"; type PiPlatformDetails = { catalogPath: string; terminal: string; changeDirectoryCommand: string; credentialProtection: string; restartCommand: string; updateCommand: string; pullCommand: string; diagnosticCommands: string; rollbackCommand: string; recoverCommand: string; verificationCommands: string; }; const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDetails }> = [ { id: "linux", label: "Linux", details: { catalogPath: "deploy//thothii-installation.yaml", terminal: "a terminal", changeDirectoryCommand: "cd /absolute/path/to/ThothII", credentialProtection: "a protected host file with mode 0600", restartCommand: "tht pi restart --yes --drain", updateCommand: "tht pi update", pullCommand: "tht pi update --version --source pull --image @sha256: --yes --drain", diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs", rollbackCommand: "tht pi rollback --yes", recoverCommand: "tht pi maintenance recover --yes", verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test", }, }, { id: "macos", label: "macOS", details: { catalogPath: "deploy//thothii-installation.yaml", terminal: "Terminal", changeDirectoryCommand: "cd /absolute/path/to/ThothII", credentialProtection: "a protected host file with mode 0600", restartCommand: "tht pi restart --yes --drain", updateCommand: "tht pi update", pullCommand: "tht pi update --version --source pull --image @sha256: --yes --drain", diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs", rollbackCommand: "tht pi rollback --yes", recoverCommand: "tht pi maintenance recover --yes", verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test", }, }, { id: "windows", label: "Windows", details: { catalogPath: "deploy\\\\thothii-installation.yaml", terminal: "PowerShell", changeDirectoryCommand: "Set-Location C:\\absolute\\path\\to\\ThothII", credentialProtection: "a protected host file with a user-only ACL", restartCommand: "tht pi restart --yes --drain", updateCommand: "tht pi update", pullCommand: "tht pi update --version --source pull --image @sha256: --yes --drain", diagnosticCommands: "tht pi maintenance status\ntht pi status\ntht pi doctor\ntht pi logs", rollbackCommand: "tht pi rollback --yes", recoverCommand: "tht pi maintenance recover --yes", verificationCommands: "tht pi maintenance status\ntht pi doctor\ntht pi test", }, }, ]; function PiCodeBlock({ children, className = "" }: { children: string; className?: string }) { return
{children}
; } function PiInstructionSteps({ details }: { details: PiPlatformDetails }) { return
  1. Open the project root

    Using {details.terminal}, before editing files or running any lifecycle command, enter the exact checkout or worktree root. Run the platform command below, then run every command in this list from that directory: tht resolves the installation descriptor and configuration from the selected project. A worktree is an independent checkout, so this prevents changing a different copy by mistake.

    {details.changeDirectoryCommand}

    The relevant layout is:

    project-root/ ├── compose.yaml ├── deploy/ │ └── <installation-id>/ │ └── thothii-installation.yaml └── docker/

    The deploy/ directory contains the selected installation descriptor and profile files. You do not need to create or manage a bin/ directory: tht is the installed host CLI. If discovery finds multiple descriptors, pass the intended one explicitly with --installation <absolute-path>/thothii-installation.yaml. If tht is not on your PATH, install it using the installation guide.

  2. Edit the Installation Model Catalog

    Edit {details.catalogPath} when adding or correcting a provider or model. Its modelCatalog block is the only authored model source for sessions, metadata generation, and embedding. It declares endpoint references and authentication modes, but never secret values. Provider integrations remain declarative; do not add model-provider code under harness/.pi/extensions/ or edit files under generated/.

    providers
    maps stable provider keys to endpoint, authentication, and runtime adapters.
    models
    maps upstream model keys; the canonical identity is provider/model.
    session
    makes a model selectable for interactive work.
    metadataGeneration
    makes a model available to metadata generation.
    embedding
    declares the one installation embedding identity and its dimensions.
  3. Choose eligibility and defaults

    Use modelCatalog.defaults.interaction as the single installation-wide LLM default for Core and Administration. When Admin AI is configured, a selectable model needs both session and metadataGeneration blocks. The user's last model choice takes precedence and does not depend on workspace. Runtime files such as Pi models.json and settings.json are generated projections and must not be edited.

    defaults.interaction
    contains exactly one canonical provider/model identity for every workspace.

    Before making each model available, verify an actual Core tool-call/review cycle through Pi and a small test description-generation run through LiteLLM. Configuration validation and Pi smoke tests alone do not certify both paths. Use approved test data; generation may incur provider charges.

  4. Complete the provider setup

    During the initial installation, run tht setup and complete the prompt named “Pi credentials file location”; it can create the empty protected template. On an existing installation with a missing credential, correct the protected credential file selected during setup by following the installation guide. Keep {details.credentialProtection}, then restart and run tht pi doctor and tht pi test. Never paste credentials into the installation YAML, this page, a command, or a log: YAML may contain only approved environment-variable names or authentication modes.

  5. Reload Pi configuration

    After changing the Installation Model Catalog or a provider credential, reload the running core service. The command validates the YAML and regenerates every runtime projection before recreation. This is a configuration reload, not a Pi version update; it keeps the current image.

    {details.restartCommand}
  6. Update the Pi version

    Use tht pi update for the routine build: it resolves the latest stable Pi release from the registry, builds it locally, drains active sessions, and recreates only core. Add --version <VERSION> when an explicitly reviewed version is required. The digest-pinned --source pull form is for an already-built, reviewed registry image; its declared version must match the digest-pinned image. Configuration changes use Reload above, not update.

    {details.updateCommand}

    Advanced: pull an immutable, digest-pinned image.

    {details.pullCommand}
  7. Diagnose and recover a failed operation

    Start with these diagnostics, in order:

    {details.diagnosticCommands}
    Configuration
    Invalid YAML, an unknown model default, or an incompatible use/adapter combination: fix the reported modelCatalog field, then reload. Do not repair generated JSON directly.
    Credentials
    Missing or unreadable credential: correct the protected credential file selected during setup and its permissions, without printing the file.
    Provider connectivity
    Wrong baseUrl, network, or provider error: correct the catalog endpoint or network, then retry.
    Docker and disk
    Unhealthy Docker or insufficient disk: restore Docker health or free space before retrying.

    If maintenance is inactive, no recovery is needed: correct the cause and retry the original command.

    If maintenance is active after an update, rollback the previous image:

    {details.rollbackCommand}

    If maintenance is active after a restart, fix the cause first, then recover the captured lifecycle state:

    {details.recoverCommand}

    After the selected remedy, verify the installation:

    {details.verificationCommands}
; } function PiPlatformInstructions({ hostPlatform }: { hostPlatform?: PiPlatform }) { const [chosenPlatform, setActivePlatform] = useState(); const activePlatform = chosenPlatform ?? hostPlatform ?? "linux"; const tabRefs = useRef>({ linux: null, macos: null, windows: null }); function activateAndFocus(platform: PiPlatform) { setActivePlatform(platform); tabRefs.current[platform]?.focus(); } function handleTabKeyDown(event: React.KeyboardEvent, platform: PiPlatform) { const index = piPlatforms.findIndex((item) => item.id === platform); const next = nextTabIndex(event.key, index, piPlatforms.length); if (next === undefined) return; event.preventDefault(); activateAndFocus(piPlatforms[next].id); } return (

Using the host terminal:

{piPlatforms.map((platform) => ( ))}
{piPlatforms.map((platform) => ( ))}
); } export function PiManagement({ open, onClose, page = false }: { open: boolean; onClose: () => void; page?: boolean }) { const [section, setSection] = useState<"runtime" | "instructions">("runtime"); const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(); const [smokeState, setSmokeState] = useState(); const [logsRequested, setLogsRequested] = useState(false); const statusQuery = useQuery({ queryKey: ["pi-management", "status"], queryFn: getPiManagementStatus, enabled: open }); const modelsQuery = useQuery({ queryKey: ["models"], queryFn: listModels, enabled: open }); const logsQuery = useQuery({ queryKey: ["pi-management", "logs"], queryFn: getPiManagementLogs, enabled: open && logsRequested }); const catalogReady = Boolean( statusQuery.data?.config.provider && statusQuery.data.config.model && statusQuery.data.config.reasoning, ); useEffect(() => { if (!open) { setFeedback(undefined); setSmokeState(undefined); setLogsRequested(false); queryClient.removeQueries({ queryKey: ["pi-management"] }); } }, [open, queryClient]); const smokeMutation = useMutation({ mutationFn: runPiManagementTest, onSuccess: (result) => { setSmokeState(result.ready ? "passed" : "failed"); setFeedback({ tone: result.ready ? "success" : "error", message: result.ready ? "Catalog default test passed." : `Catalog default test failed.${result.message ? ` ${result.message}` : ""}`, }); }, onError: (error) => { setSmokeState("failed"); setFeedback({ tone: "error", message: errorMessage(error, "Could not test the catalog default.") }); }, }); function testCatalogDefault() { if (!catalogReady) { setFeedback({ tone: "error", message: "Fix the Installation Model Catalog before running the test." }); return; } smokeMutation.mutate(); } const forbidden = asPiManagementApiError(statusQuery.error)?.code === "pi_management_forbidden"; const loading = statusQuery.isLoading; const unavailable = statusQuery.isError; return (
{page && }
); }