import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, CircleAlert, ClipboardCheck, FlaskConical, LoaderCircle, ScrollText } from "lucide-react"; import { asPiManagementApiError, getPiManagementLogs, getPiManagementOptions, getPiManagementStatus, runPiManagementTest, savePiManagementConfig, type PiInstallationConfig, type PiManagementOptions, } from "../api/pi-management"; import { Button } from "../components/ui/button"; import { WorkAreaPanel } from "./WorkAreaPanel"; const fieldClass = "h-9 w-full rounded-md border border-input bg-background px-2.5 text-sm shadow-xs outline-none focus-visible:ring-3 focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-60"; type Feedback = { tone: "success" | "error"; message: string } | undefined; type SmokeState = "passed" | "failed" | undefined; function suggestedConfig(status: { config: Partial }, options: PiManagementOptions): PiInstallationConfig | undefined { const provider = status.config.provider && options.providers.includes(status.config.provider) ? status.config.provider : options.providers[0]; const model = status.config.model && options.models.some((item) => item.provider === provider && item.id === status.config.model) ? status.config.model : options.models.find((item) => item.provider === provider)?.id; const reasoning = status.config.reasoning && options.reasoning.includes(status.config.reasoning) ? status.config.reasoning : options.reasoning[0]; return provider && model && reasoning ? { provider, model, reasoning } : undefined; } function isSupportedConfig( config: Partial | undefined, options: PiManagementOptions | undefined, ): config is PiInstallationConfig { return Boolean( config?.provider && config.model && config.reasoning && options?.providers.includes(config.provider) && options.models.some((model) => model.provider === config.provider && model.id === config.model) && options.reasoning.includes(config.reasoning), ); } function sameConfig(a: Partial | undefined, b: PiInstallationConfig | undefined): boolean { return Boolean( a?.provider === b?.provider && a?.model === b?.model && a?.reasoning === b?.reasoning, ); } 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}

; } function Field({ label, children }: { label: string; children: React.ReactNode }) { return ; } type PiPlatform = "linux" | "macos" | "windows"; type PiPlatformDetails = { modelsPath: string; settingsPath: 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: { modelsPath: "deploy/pi/models.json", settingsPath: "deploy/pi/settings.json", 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: { modelsPath: "deploy/pi/models.json", settingsPath: "deploy/pi/settings.json", 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: { modelsPath: "deploy\\pi\\models.json", settingsPath: "deploy\\pi\\settings.json", 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/ │ └── pi/ │ ├── models.json │ └── settings.json └── 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 provider catalog

    Edit {details.modelsPath} only when adding or correcting a provider/model definition. The provider catalog is an address book/map of the services Pi can call: each entry supplies the API endpoint and format, and lists the model identifiers offered there. It does not enable a model and never contains credentials. Provider integrations must remain declarative; do not add model-provider code under harness/.pi/extensions/.

    baseUrl
    is the provider API endpoint.
    api
    selects the provider API format.
    models
    lists that provider's available models.
    id
    is the model identifier.
    name
    is the model name shown to operators.
  3. Enable the model

    Enable a model after the provider setup is ready. For a custom provider, first define the provider and model ID in {details.modelsPath}; built-in Pi models may already be known without a local catalog entry. Then edit {details.settingsPath} in the project root, add the exact provider/model identifier to enabledModels, and reload Pi. Do not edit it merely to choose the default provider/model: use this page or tht pi configure.

    enabledModels
    uses provider/model identifiers to choose the models available for new Pi work.
  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 either JSON file, the page, a command, or a log.

  5. Reload Pi configuration

    After changing models.json, settings.json, or the provider credential, reload the running core service so it reads the new files. 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 JSON: fix the reported file and validate it. A provider/model ID mismatch or a model missing from enabledModels: correct the IDs or policy, then reload.
    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 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() { const [activePlatform, setActivePlatform] = useState(); const [focusedPlatform, setFocusedPlatform] = useState("linux"); const tabRefs = useRef>({ linux: null, macos: null, windows: null }); function activateAndFocus(platform: PiPlatform) { setFocusedPlatform(platform); setActivePlatform(platform); tabRefs.current[platform]?.focus(); } function handleTabKeyDown(event: React.KeyboardEvent, platform: PiPlatform) { const index = piPlatforms.findIndex((item) => item.id === platform); const nextPlatform = event.key === "ArrowRight" ? piPlatforms[(index + 1) % piPlatforms.length] : event.key === "ArrowLeft" ? piPlatforms[(index - 1 + piPlatforms.length) % piPlatforms.length] : event.key === "Home" ? piPlatforms[0] : event.key === "End" ? piPlatforms[piPlatforms.length - 1] : undefined; if (!nextPlatform) return; event.preventDefault(); activateAndFocus(nextPlatform.id); } return (

Using the host terminal:

{piPlatforms.map((platform) => ( ))}
{piPlatforms.map((platform) => ( ))}
); } export function PiManagement({ open, onClose }: { open: boolean; onClose: () => void }) { const queryClient = useQueryClient(); const [draft, setDraft] = useState(); 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 optionsQuery = useQuery({ queryKey: ["pi-management", "options"], queryFn: getPiManagementOptions, enabled: open }); const logsQuery = useQuery({ queryKey: ["pi-management", "logs"], queryFn: getPiManagementLogs, enabled: open && logsRequested }); const models = useMemo( () => optionsQuery.data?.models.filter((model) => model.provider === draft?.provider) ?? [], [draft?.provider, optionsQuery.data?.models], ); const initialDraft = useMemo( () => statusQuery.data && optionsQuery.data ? suggestedConfig(statusQuery.data, optionsQuery.data) : undefined, [optionsQuery.data, statusQuery.data], ); const persistedReady = isSupportedConfig(statusQuery.data?.config, optionsQuery.data); const validDraft = isSupportedConfig(draft, optionsQuery.data); const dirty = Boolean(draft && !sameConfig(statusQuery.data?.config, draft)); useEffect(() => { if (!open) { setDraft(undefined); setFeedback(undefined); setSmokeState(undefined); setLogsRequested(false); queryClient.removeQueries({ queryKey: ["pi-management"] }); } }, [open, queryClient]); useEffect(() => { if (draft || !initialDraft) return; setDraft(initialDraft); }, [draft, initialDraft]); const saveMutation = useMutation({ mutationFn: savePiManagementConfig, onSuccess: async (saved) => { const config = { provider: saved.provider, model: saved.model, reasoning: saved.reasoning }; setDraft(config); setSmokeState(undefined); await queryClient.invalidateQueries({ queryKey: ["pi-management", "status"] }); setFeedback({ tone: "success", message: "Defaults saved." }); }, onError: (error) => setFeedback({ tone: "error", message: errorMessage(error, "Could not save Pi defaults.") }), }); const smokeMutation = useMutation({ mutationFn: runPiManagementTest, onSuccess: (result) => { setSmokeState(result.ready ? "passed" : "failed"); setFeedback({ tone: result.ready ? "success" : "error", message: result.ready ? "Saved configuration test passed." : `Saved configuration test failed.${result.message ? ` ${result.message}` : ""}`, }); }, onError: (error) => { setSmokeState("failed"); setFeedback({ tone: "error", message: errorMessage(error, "Could not test the saved Pi defaults.") }); }, }); function saveDefaults() { if (!draft || !validDraft) { setFeedback({ tone: "error", message: "Choose a supported provider, model, and reasoning level." }); return; } saveMutation.mutate(draft); } function updateDraft(update: (current: PiInstallationConfig) => PiInstallationConfig) { setDraft((current) => current ? update(current) : current); setSmokeState(undefined); setFeedback(undefined); } function testSavedDefaults() { if (!persistedReady || dirty) { setFeedback({ tone: "error", message: "Save supported defaults before running the test." }); return; } smokeMutation.mutate(); } const forbidden = asPiManagementApiError(statusQuery.error)?.code === "pi_management_forbidden"; const loading = statusQuery.isLoading || optionsQuery.isLoading || Boolean(!draft && initialDraft); const unavailable = statusQuery.isError || optionsQuery.isError; return (
{forbidden ? (

Pi management is not permitted

Ask an installation administrator to manage Pi defaults and diagnostics.

) : unavailable ? (

{errorMessage(statusQuery.error ?? optionsQuery.error, "Pi management is unavailable")}

) : loading ?

Loading Pi management…

: statusQuery.data && optionsQuery.data && (

Bundled runtime

Pi {statusQuery.data.version ?? "version unavailable"}

{statusQuery.data.ready ? "Ready" : "Needs attention"}

{statusQuery.data.message &&

{statusQuery.data.message}

} {feedback &&

{feedback.tone === "error" ? : } {feedback.message}

}

Installation defaults

Select the provider, model, and reasoning used for new Pi work. Credentials stay in protected host files.

{draft ? ( <>
{dirty &&

{persistedReady ? "Changes are not saved yet." : "Suggested choices are not saved yet."}

}
{dirty &&

Save these changes before testing. The test always uses saved defaults.

} {!dirty && !persistedReady &&

Save supported defaults before testing. The test always uses saved defaults.

} ) : (

No provider or model options are available.

Check the host-managed Pi model configuration, then retry this panel.

)}

Sanitized diagnostics

Shows at most 200 recent lines with declared secret values removed.

{logsQuery.isError &&

Could not load sanitized Pi diagnostics.

} {logsQuery.data &&
{logsQuery.data.lines.join("\n") || "No diagnostic lines are available."}
}
)}
); }