mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
* chore(lint): batch 1 of #12146 — resolve the react-hooks compiler violations in dashboard/cli-code Real refactors, no suppressions — the 42 frozen react-hooks/* entries for the 12 dashboard/cli-code files (plus Antigravity's exhaustive-deps one) are removed from config/quality/eslint-suppressions.json and the files now lint clean under the React Compiler rules. Techniques, per pattern: - set-state-in-effect ("default API key" effects — Antigravity, Claude, Cline, Codex, Droid, GrokBuild, Kilo, OpenClaw): the setState-in-effect that copied apiKeys[0].id into the selection state is deleted; an `effective*` value is derived during render (`selected || apiKeys[0]?.id`) and used by the select and the submit handlers. Behavior identical, one less render pass. - immutability ("accessed before declared") + set-state-in-effect on the expand-time loaders (all tool cards): the fetchers (checkXStatus, fetchModelAliases, fetchBackups, fetchProfiles, loadSavedMappings) are hoisted above the effect as useCallback with correct deps, listed in the effect deps, and invoked through an async continuation (`void (async () => { await Promise.all([...]) })()`) so no setState runs synchronously in the effect body. - set-state-in-effect ("init form from fetched status" effects — Claude, Cline, Codex, Droid, OpenClaw): the status-parsing effects are deleted and their logic now runs inside checkXStatus right after the fetch resolves (setState after await), keeping the same one-time ref guards. Codex's config parser became syncFormFromStatus(), called on both success and error paths. - HermesAgentToolCard: Date.now() in render (purity) is snapshotted once via a lazy useState initializer; the batchStatus seeding effect is replaced by a derived `displayRoles` (useMemo over batchStatus with currentRoles taking precedence); the collapse-reset effect moved into the header toggle handler. - ClaudeClassifierCompatToggle / CliProfileAutoSyncToggles / Cliproxyapi / GrokBuild: mount/expand loads wrapped in the same async continuation. - DroidToolCard's isOmniRouteEntry helper hoisted to module scope (pure). Validation: eslint with suppressions --max-warnings 0 on the 12 files (clean), scripts/check/check-dashboard-typecheck.mjs (OK, within frozen baseline), vitest UI suites for the touched cards (15 files / 57 tests green, plus the 3 quarantined #8618 files run explicitly: 27 tests green), and the node-native cli-code tests (61 tests green). Refs #12146 * chore(lint): batch 1 follow-up — hoist the settings-init helpers so the cognitive gate stays flat The first pass folded the one-time form init into the status fetchers, which pushed sonarjs/cognitive-complexity to 1 in Claude/Cline/OpenClaw tool cards (caught by the new-code gate on the PR). The init logic now lives in module-level helpers (initXFormFromSettings + defaultKeyId); complexityNewCode=-1, cognitiveComplexityNewCode=0.
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
type CompatMode = "off" | "auto" | "always";
|
|
|
|
const MODES: CompatMode[] = ["off", "auto", "always"];
|
|
|
|
const MODE_STYLES: Record<CompatMode, string> = {
|
|
off: "bg-black/5 dark:bg-white/5 text-text-muted border-border",
|
|
auto: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border-yellow-500/40",
|
|
always: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/40",
|
|
};
|
|
|
|
function isCompatMode(value: unknown): value is CompatMode {
|
|
return value === "off" || value === "auto" || value === "always";
|
|
}
|
|
|
|
/**
|
|
* Opt-in toggle (default "off") for Claude Code's auto-permission classifier compat mode.
|
|
*
|
|
* Claude Code's `--permission-mode auto` sends an internal `/v1/messages` security-classifier
|
|
* request that requires the response to START with `<block>no</block>` (ALLOW) / `<block>yes</block>`
|
|
* (BLOCK). When a combo/fallback route sends that call to a cheap model returning empty content,
|
|
* Claude Code fails closed on every gated action. "auto" detects the classifier request and
|
|
* short-circuits with a synthetic ALLOW response without calling upstream; "always" applies it to
|
|
* every Claude-format request. Cycles off → auto → always via the existing /api/settings PATCH.
|
|
*/
|
|
export default function ClaudeClassifierCompatToggle() {
|
|
const t = useTranslations("cliTools");
|
|
const [mode, setMode] = useState<CompatMode>("off");
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/settings");
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
setMode(isCompatMode(data?.claudeClassifierCompat) ? data.claudeClassifierCompat : "off");
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : t("classifierCompatLoadFailed"));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
// Load in an async continuation so every setState happens after an await
|
|
// (react-hooks/set-state-in-effect: no synchronous setState in effect bodies).
|
|
void (async () => {
|
|
await load();
|
|
})();
|
|
}, [load]);
|
|
|
|
const cycle = useCallback(async () => {
|
|
const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length];
|
|
const previous = mode;
|
|
setMode(next); // optimistic
|
|
setSaving(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch("/api/settings", {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ claudeClassifierCompat: next }),
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
} catch (err) {
|
|
setMode(previous); // revert on failure
|
|
setError(err instanceof Error ? err.message : t("failedSave"));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [mode, t]);
|
|
|
|
return (
|
|
<div className="rounded-lg border border-border bg-surface/40 p-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<h4 className="text-xs font-semibold text-text-main">{t("classifierCompatTitle")}</h4>
|
|
<p className="text-xs text-text-muted">
|
|
{t.rich("classifierCompatDescription", {
|
|
code: (chunks) => <code>{chunks}</code>,
|
|
})}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={cycle}
|
|
disabled={loading || saving}
|
|
title={t("classifierCompatCycle")}
|
|
className={`shrink-0 rounded border px-3 py-1.5 text-xs font-medium uppercase tracking-wide transition-colors disabled:opacity-50 ${MODE_STYLES[mode]}`}
|
|
>
|
|
{t(`classifierCompatMode.${mode}`)}
|
|
</button>
|
|
</div>
|
|
{error ? <p className="mt-2 text-xs text-red-500">{error}</p> : null}
|
|
</div>
|
|
);
|
|
}
|