/** * SetupTab/ThresholdsPanel.tsx — per-model compaction thresholds (S52 / v0.16.1). * * Lists every known model (from model_snapshots) with two sliders each: * - Safety Margin (0-20%): reserved after summary + maxOutput. Lower = * more usable tail but risks the "too long even after compaction" error. * - Fire Point (10-90%): % of window where compaction fires. Lower = compact * earlier (more headroom); higher = more context before compaction. * * Warnings fire on <5% safety (overflow risk) and >85% fire point (aggressive * — may hit the cap before compaction triggers). Changes save immediately via * PUT /api/model-thresholds; a "Reset to default" button DELETEs the override. * * PREVENT-PI-004: all data comes from localhost dashboard API endpoints. */ import type React from "react"; import { useState, useEffect, useCallback } from "react"; import { fetchModelThresholds, putModelThreshold, deleteModelThreshold, } from "../../api/client"; import type { ModelThresholdsResponse } from "@contracts"; interface ModelRow { readonly modelId: string; readonly provider: string; readonly modelName: string | null; readonly contextWindow: number; readonly maxTokens: number; readonly hasOverride: boolean; readonly threshold: { readonly safetyMarginPct: number; readonly firePointPct: number; readonly isOverride: boolean; }; } function fmtWindow(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`; if (n >= 1000) return `${Math.round(n / 1000)}K`; return String(n); } /** Live preview of the usable tail budget for a model + slider values. */ function usableTail( ctxWindow: number, maxTokens: number, safetyPct: number, ): string { if (ctxWindow <= 0) return "—"; const maxOut = maxTokens > 0 ? maxTokens : Math.ceil(ctxWindow * 0.1); const reserve = maxOut + Math.ceil(ctxWindow * (safetyPct / 100)); const usable = ctxWindow - reserve; if (usable <= 0) return "0 (at risk)"; return `${fmtWindow(usable)} tokens`; } function ThresholdSlider({ label, value, min, max, step = 1, onChange, warning, }: { readonly label: string; readonly value: number; readonly min: number; readonly max: number; readonly step?: number; readonly onChange: (v: number) => void; readonly warning?: string; }): React.ReactElement { return (
{label} {value}%
onChange(Number(e.target.value))} className="threshold-slider w-full" /> {warning &&

⚠ {warning}

}
); } function ModelThresholdRow({ model, defaults, safetyRange, fireRange, onSave, onReset, saving, }: { readonly model: ModelRow; readonly defaults: { safetyMarginPct: number; firePointPct: number }; readonly safetyRange: readonly [number, number]; readonly fireRange: readonly [number, number]; readonly onSave: ( modelId: string, safetyMarginPct: number, firePointPct: number, ) => Promise; readonly onReset: (modelId: string) => Promise; readonly saving: boolean; }): React.ReactElement { const [safety, setSafety] = useState(model.threshold.safetyMarginPct); const [fire, setFire] = useState(model.threshold.firePointPct); const [dirty, setDirty] = useState(false); const [err, setErr] = useState(null); // Re-sync when the server data changes (e.g. after reset). useEffect(() => { setSafety(model.threshold.safetyMarginPct); setFire(model.threshold.firePointPct); setDirty(false); }, [model.threshold.safetyMarginPct, model.threshold.firePointPct]); const safetyWarning = safety < 5 ? `Low safety margin — risks "conversation too long" errors when a single turn injects a large tool output between gate fires.` : undefined; const fireWarning = fire > 85 ? `High fire point — compaction may trigger close to the window cap; a single large turn can overshoot before the gate fires.` : undefined; const save = useCallback(async () => { setErr(null); try { await onSave(model.modelId, safety, fire); setDirty(false); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } }, [model.modelId, safety, fire, onSave]); return (
{model.modelName ?? model.modelId} {model.threshold.isOverride && ( Override )}
{model.provider} · {fmtWindow(model.contextWindow)} window ·{" "} {fmtWindow(model.maxTokens)} output
Usable tail:{" "} {usableTail(model.contextWindow, model.maxTokens, safety)}
Compaction fires at:{" "} {fmtWindow(Math.ceil(model.contextWindow * (fire / 100)))}
{ setSafety(v); setDirty(true); }} warning={safetyWarning} /> { setFire(v); setDirty(true); }} warning={fireWarning} />
{model.threshold.isOverride && ( )} {err && {err}}
); } export default function ThresholdsPanel(): React.ReactElement { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const load = useCallback(async () => { try { setError(null); setData(await fetchModelThresholds()); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const onSave = useCallback( async (modelId: string, safetyMarginPct: number, firePointPct: number) => { setSaving(true); try { await putModelThreshold({ modelId, safetyMarginPct, firePointPct }); await load(); } finally { setSaving(false); } }, [load], ); const onReset = useCallback( async (modelId: string) => { setSaving(true); try { await deleteModelThreshold(modelId); await load(); } finally { setSaving(false); } }, [load], ); if (loading) return
Loading thresholds…
; if (error) return
Error: {error}
; if (!data) return
No data.
; return (

Per-Model Compaction Thresholds

Tune when compaction fires + how much safety margin to reserve, per model. Different providers' models range from 8K to 1M+ context — one global % is wrong. Defaults:{" "} {data.defaults.safetyMarginPct}% safety {" "} /{" "} {data.defaults.firePointPct}% fire. Values outside the safe band show warnings.

{data.models.length === 0 ? (
No models captured yet. Start a pi session in any repo to populate model snapshots, then return here to tune per-model thresholds.
) : (
{data.models.map((m) => ( ))}
)}
); }