feat(headroom): activate/uninstall extras + fix interpreter detection

- find interpreter next to headroom binary so extras/version read correctly
- add on/off toggle to activate [code]/[ml] via proxy restart
- add uninstall action + live install log progress + ~1GB confirm modal
This commit is contained in:
decolua
2026-07-10 17:32:45 +07:00
parent dcf1927f22
commit 74d5fedf79
7 changed files with 367 additions and 47 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, Button, Input, Modal, Toggle } from "@/shared/components";
import { useState, useEffect, useCallback, useRef } from "react";
import { Card, Button, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
import {
@@ -33,6 +33,13 @@ export default function TokenSaverClient() {
const [pendingExtras, setPendingExtras] = useState([]);
const [extrasActionLoading, setExtrasActionLoading] = useState(false);
const [extrasActionError, setExtrasActionError] = useState("");
const [removingExtra, setRemovingExtra] = useState(null);
const [installLog, setInstallLog] = useState("");
const [extrasConfirm, setExtrasConfirm] = useState(null);
const [codeAware, setCodeAware] = useState(false);
const [kompress, setKompress] = useState(true);
const [restartingProxy, setRestartingProxy] = useState(false);
const logPollRef = useRef(null);
const [cavemanEnabled, setCavemanEnabled] = useState(false);
const [cavemanLevel, setCavemanLevel] = useState("full");
const [ponytailEnabled, setPonytailEnabled] = useState(false);
@@ -205,10 +212,37 @@ export default function TokenSaverClient() {
);
};
const handleInstallExtras = useCallback(async () => {
// Poll the install log tail while a pip install/uninstall is running.
const startLogPolling = useCallback(() => {
setInstallLog("");
if (logPollRef.current) clearInterval(logPollRef.current);
const tick = async () => {
try {
const r = await fetch("/api/headroom/extras?log=1", {
headers: { "Cache-Control": "no-store" },
});
const d = await r.json().catch(() => ({}));
if (typeof d.log === "string") setInstallLog(d.log);
} catch { /* ignore transient poll errors */ }
};
tick();
logPollRef.current = setInterval(tick, 1500);
}, []);
const stopLogPolling = useCallback(() => {
if (logPollRef.current) {
clearInterval(logPollRef.current);
logPollRef.current = null;
}
}, []);
useEffect(() => () => stopLogPolling(), [stopLogPolling]);
const installExtrasConfirmed = useCallback(async () => {
if (pendingExtras.length === 0) return;
setExtrasActionLoading(true);
setExtrasActionError("");
startLogPolling();
try {
const res = await fetch("/api/headroom/extras", {
method: "POST",
@@ -226,9 +260,83 @@ export default function TokenSaverClient() {
} catch (e) {
setExtrasActionError(e.message);
} finally {
stopLogPolling();
setExtrasActionLoading(false);
}
}, [pendingExtras]);
}, [pendingExtras, startLogPolling, stopLogPolling]);
const removeExtraConfirmed = useCallback(async (extra) => {
setRemovingExtra(extra);
setExtrasActionError("");
startLogPolling();
try {
const res = await fetch("/api/headroom/extras", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ extras: [extra] }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Remove failed");
setHeadroomExtras((s) => ({
...s,
version: data.version ?? s.version,
extras: data.extras || s.extras,
}));
} catch (e) {
setExtrasActionError(e.message);
} finally {
stopLogPolling();
setRemovingExtra(null);
}
}, [startLogPolling, stopLogPolling]);
const handleInstallExtras = useCallback(() => {
if (pendingExtras.length === 0) return;
// Warn about the heavy ~1GB torch download before installing [ml].
if (pendingExtras.includes("ml")) {
setExtrasConfirm({
title: "Install [ml]",
message: "[ml] downloads ~1 GB (torch + huggingface-hub). Continue?",
confirmText: "Install",
variant: "primary",
onConfirm: installExtrasConfirmed,
});
return;
}
installExtrasConfirmed();
}, [pendingExtras, installExtrasConfirmed]);
const handleRemoveExtra = useCallback((extra) => {
setExtrasConfirm({
title: `Remove [${extra}]`,
message: `Remove [${extra}] and its packages?`,
confirmText: "Remove",
variant: "danger",
onConfirm: () => removeExtraConfirmed(extra),
});
}, [removeExtraConfirmed]);
// Toggle an extra's active state (persist setting), then restart the proxy so
// the new --code-aware / --disable-kompress flags take effect.
const toggleExtraActive = useCallback(async (extra, value) => {
setExtrasActionError("");
if (extra === "code") setCodeAware(value);
if (extra === "ml") setKompress(value);
const key = extra === "code" ? "headroomCodeAware" : "headroomKompress";
await patchSetting({ [key]: value });
if (!headroomStatus.running) return;
setRestartingProxy(true);
try {
const res = await fetch("/api/headroom/restart", { method: "POST" });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Restart failed");
await refreshHeadroomStatus();
} catch (e) {
setExtrasActionError(e.message);
} finally {
setRestartingProxy(false);
}
}, [headroomStatus.running, refreshHeadroomStatus]);
const handleCavemanLevel = (level) => {
setCavemanLevel(level);
@@ -307,6 +415,8 @@ export default function TokenSaverClient() {
setRtkEnabledState(data.rtkEnabled !== false);
setHeadroomEnabled(!!data.headroomEnabled);
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
setCodeAware(data.headroomCodeAware === true);
setKompress(data.headroomKompress !== false);
setCavemanEnabled(!!data.cavemanEnabled);
setCavemanLevel(data.cavemanLevel || "full");
setPonytailEnabled(!!data.ponytailEnabled);
@@ -387,7 +497,7 @@ export default function TokenSaverClient() {
onChange={() => handleRtkEnabled(!rtkEnabled)}
/>
</div>
<div className="flex items-center justify-between py-4 border-b border-border gap-4 flex-wrap">
<div className="flex items-center justify-between py-4 gap-4 flex-wrap">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3 flex-wrap">
<p className="font-medium">
@@ -425,7 +535,7 @@ export default function TokenSaverClient() {
/>
</div>
{headroomStatus.installed && (
<div className="mt-3 ml-1 pl-3 border-l-2 border-border">
<div className="mb-3 ml-1 pl-3 pb-4 border-l-2 border-border">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-text-muted">
Compression extras
@@ -434,33 +544,57 @@ export default function TokenSaverClient() {
{headroomExtras.available.map((extra) => {
const installed = !!headroomExtras.extras[extra];
const pending = pendingExtras.includes(extra);
const extraTitle =
extra === "code"
? "tree-sitter AST compression for code responses"
: "Kompress-v2 HF model for prose/agentic traces (~+1GB)";
if (installed) {
const active = extra === "code" ? codeAware : kompress;
return (
<div
key={extra}
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded border border-success/40 bg-success/5 text-text"
title={extraTitle}
>
<Toggle
size="sm"
checked={active}
disabled={restartingProxy}
onChange={() => toggleExtraActive(extra, !active)}
/>
<span className="font-medium">[{extra}]</span>
<button
type="button"
onClick={() => handleRemoveExtra(extra)}
disabled={removingExtra === extra}
className="ml-1 text-error underline hover:opacity-80 disabled:opacity-50"
title={`Uninstall [${extra}]`}
>
{removingExtra === extra ? "Uninstalling…" : "Uninstall"}
</button>
</div>
);
}
return (
<label
key={extra}
className={`flex items-center gap-1.5 text-xs px-2 py-1 rounded border cursor-pointer transition-colors ${
installed
? "border-success/40 bg-success/5 text-text"
: pending
pending
? "border-primary bg-primary/10 text-primary"
: "border-border text-text-muted hover:bg-surface-2"
}`}
title={
extra === "code"
? "tree-sitter AST compression for code responses"
: "Kompress-v2 HF model for prose/agentic traces (~+1GB)"
}
title={extraTitle}
>
<input
type="checkbox"
className="w-3 h-3"
checked={installed || pending}
disabled={installed}
checked={pending}
onChange={() => togglePendingExtra(extra)}
/>
<span className="font-medium">[{extra}]</span>
<span className="opacity-70">
{installed ? "installed" : "not installed"}
</span>
<span className="opacity-70">not installed</span>
</label>
);
})}
@@ -479,16 +613,26 @@ export default function TokenSaverClient() {
{extrasActionError && (
<p className="text-xs text-error mt-1">{extrasActionError}</p>
)}
{restartingProxy && (
<p className="text-xs text-text-muted mt-1">Restarting proxy…</p>
)}
{(extrasActionLoading || removingExtra) && installLog && (
<pre className="mt-2 max-h-32 overflow-auto rounded bg-surface-2 p-2 text-[10px] leading-tight text-text-muted whitespace-pre-wrap">
{installLog}
</pre>
)}
<p className="text-xs text-text-muted mt-1">
Default install is <code>[proxy]</code> only (SmartCrusher for
JSON). Adding <code>[code]</code> enables AST compression
Installing adds the package; use <code>on</code>/<code>off</code>{" "}
to activate it (restarts the proxy). Default install is{" "}
<code>[proxy]</code> only (SmartCrusher for JSON). Adding{" "}
<code>[code]</code> enables AST compression
(Python/JS/TS/Go/Rust/Java/C/C++/Perl). Adding <code>[ml]</code>{" "}
enables the Kompress-v2 HF model for prose/agentic traces but
adds ~1 GB (torch + huggingface-hub).
</p>
</div>
)}
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
<div className="flex items-center justify-between pt-4 border-t border-border gap-4 flex-wrap">
<div className="min-w-0 flex-1">
<p className="font-medium">
Compress LLM output{" "}
@@ -589,6 +733,8 @@ export default function TokenSaverClient() {
/>
</div>
</div>
{/* PXPIPE hidden from UI — experimental, not exposed to users yet */}
{false && (
<div className="flex items-center justify-between pt-4 mt-4 border-t border-border gap-4 flex-wrap">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3 flex-wrap">
@@ -632,6 +778,7 @@ export default function TokenSaverClient() {
onChange={() => handlePxpipeEnabled(!pxpipeEnabled)}
/>
</div>
)}
</Card>
<Modal
@@ -743,7 +890,7 @@ export default function TokenSaverClient() {
</Modal>
<Modal
isOpen={showPxpipeModal}
isOpen={false}
title={pxpipeStatus.installed ? "PXPIPE" : "Setup PXPIPE"}
onClose={() => setShowPxpipeModal(false)}
>
@@ -849,6 +996,20 @@ export default function TokenSaverClient() {
</div>
</div>
</Modal>
<ConfirmModal
isOpen={!!extrasConfirm}
onClose={() => setExtrasConfirm(null)}
onConfirm={() => {
const fn = extrasConfirm?.onConfirm;
setExtrasConfirm(null);
fn?.();
}}
title={extrasConfirm?.title}
message={extrasConfirm?.message}
confirmText={extrasConfirm?.confirmText}
variant={extrasConfirm?.variant}
/>
</div>
);
}

View File

@@ -1,11 +1,15 @@
import { NextResponse } from "next/server";
import { findPython310, getInstalledHeadroomExtras, HEADROOM_COMPRESSION_EXTRAS } from "@/lib/headroom/detect";
import { installHeadroomExtras } from "@/lib/headroom/process";
import { installHeadroomExtras, uninstallHeadroomExtras, getInstallLogTail } from "@/lib/headroom/process";
export const dynamic = "force-dynamic";
export async function GET() {
export async function GET(req) {
try {
// `?log=1` returns the live install/uninstall log tail for progress polling.
if (new URL(req.url).searchParams.get("log") === "1") {
return NextResponse.json({ log: getInstallLogTail() });
}
const python = findPython310();
const status = getInstalledHeadroomExtras(python);
return NextResponse.json({
@@ -28,3 +32,15 @@ export async function POST(req) {
return NextResponse.json({ error: error.message, code: error.code || null }, { status });
}
}
export async function DELETE(req) {
try {
const body = await req.json().catch(() => ({}));
const requested = Array.isArray(body?.extras) ? body.extras : [];
const result = await uninstallHeadroomExtras(requested);
return NextResponse.json(result);
} catch (error) {
const status = error.code === "NO_PYTHON" || error.code === "INVALID_EXTRAS" ? 400 : 500;
return NextResponse.json({ error: error.message, code: error.code || null }, { status });
}
}

View File

@@ -0,0 +1,35 @@
import { NextResponse } from "next/server";
import { getSettings } from "@/lib/localDb";
import { restartHeadroomProxy } from "@/lib/headroom/process";
import { DEFAULT_HEADROOM_URL, isLoopbackHeadroomUrl } from "@/lib/headroom/detect";
export const dynamic = "force-dynamic";
function parsePortFromUrl(url) {
try {
const u = new URL(url);
const p = parseInt(u.port, 10);
if (p > 0 && p < 65536) return p;
} catch { /* ignore, fall through to default */ }
return null;
}
export async function POST() {
try {
const settings = await getSettings();
const url = settings.headroomUrl || DEFAULT_HEADROOM_URL;
if (!isLoopbackHeadroomUrl(url)) {
return NextResponse.json({ error: "External Headroom proxies must be started outside 9Router", code: "EXTERNAL_PROXY" }, { status: 400 });
}
const port = parsePortFromUrl(url) || 8787;
const result = await restartHeadroomProxy({
port,
codeAware: settings.headroomCodeAware === true,
kompress: settings.headroomKompress !== false,
});
return NextResponse.json({ success: true, ...result });
} catch (error) {
const status = error.code === "NOT_INSTALLED" ? 400 : 500;
return NextResponse.json({ error: error.message, code: error.code || null }, { status });
}
}

View File

@@ -22,7 +22,11 @@ export async function POST() {
return NextResponse.json({ error: "External Headroom proxies must be started outside 9Router", code: "EXTERNAL_PROXY" }, { status: 400 });
}
const port = parsePortFromUrl(url) || 8787;
const result = await startHeadroomProxy({ port });
const result = await startHeadroomProxy({
port,
codeAware: settings.headroomCodeAware === true,
kompress: settings.headroomKompress !== false,
});
return NextResponse.json({ success: true, ...result });
} catch (error) {
const status = error.code === "NOT_INSTALLED" ? 400 : 500;