Skip to content
3 changes: 3 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ export const de = {
"startup.copied": "Kopiert",
"startup.recommended": "Empfohlene Reparatur: {cmd}",
"startup.navRisk": "Der Startschutz erfordert Aufmerksamkeit",
"startup.codexRuntime.clampHidden": "Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat.",
"startup.codexRuntime.clampHiddenWithEfforts": "Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat (entfernt: {efforts}).",
"startup.codexRuntime.olderBinary": "OpenCodex verwendet eine ältere Codex-Binary ({version}). Eine neuere Installation ist verfügbar.",
"dash.subtitle": "Live-Status des lokalen opencodex-Proxys, seiner Anbieter und der in Codex gerouteten Modelle.",
"dash.status": "Status",
"dash.online": "Online",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export const en = {
"startup.copied": "Copied",
"startup.recommended": "Recommended repair: {cmd}",
"startup.navRisk": "Startup protection requires attention",
"startup.codexRuntime.clampHidden": "Some reasoning effort options were hidden because OpenCodex used Codex {version}.",
"startup.codexRuntime.clampHiddenWithEfforts": "Some reasoning effort options were hidden because OpenCodex used Codex {version} (removed: {efforts}).",
"startup.codexRuntime.olderBinary": "OpenCodex is using an older Codex binary ({version}). A newer installation is available.",

// dashboard
"dash.subtitle": "Live status of the local opencodex proxy, its providers, and the models routed into Codex.",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export const ja: Record<TKey, string> = {
"startup.copied": "コピー済み",
"startup.recommended": "推奨修復: {cmd}",
"startup.navRisk": "起動保護に対応が必要です",
"startup.codexRuntime.clampHidden": "OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました。",
"startup.codexRuntime.clampHiddenWithEfforts": "OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました(削除: {efforts})。",
"startup.codexRuntime.olderBinary": "OpenCodex は古い Codex バイナリ({version})を使用しています。より新しいインストールが利用可能です。",

// dashboard
"dash.subtitle": "ローカル opencodex プロキシ、そのプロバイダー、Codex にルーティングされるモデルのライブ状態です。",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export const ko: Record<TKey, string> = {
"startup.copied": "복사됨",
"startup.recommended": "권장 복구 명령: {cmd}",
"startup.navRisk": "시작 보호 상태에 조치가 필요합니다",
"startup.codexRuntime.clampHidden": "OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다.",
"startup.codexRuntime.clampHiddenWithEfforts": "OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다(제거됨: {efforts}).",
"startup.codexRuntime.olderBinary": "OpenCodex가 더 오래된 Codex 바이너리({version})를 사용 중입니다. 더 새 설치를 사용할 수 있습니다.",

// dashboard
"dash.subtitle": "로컬 opencodex 프록시와 프로바이더, 그리고 Codex로 라우팅되는 모델의 실시간 상태입니다.",
Expand Down
10 changes: 7 additions & 3 deletions gui/src/i18n/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { DICTS, I18nContext, LOCALES, detectInitial, interpolate, type TFn, type TKey, type Vars } from "./shared";
import { en } from "./en";
import { useI18n } from "./shared";
Expand All @@ -12,9 +12,13 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
try { localStorage.setItem("ocx-lang", locale); } catch { /* ignore */ }
}, [locale]);

const t: TFn = (key, vars) => interpolate(DICTS[locale][key] ?? en[key] ?? key, vars);
const t: TFn = useCallback(
(key, vars) => interpolate(DICTS[locale][key] ?? en[key] ?? key, vars),
[locale],
);
const value = useMemo(() => ({ locale, setLocale, t }), [locale, t]);

return <I18nContext.Provider value={{ locale, setLocale, t }}>{children}</I18nContext.Provider>;
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}

export function Trans({ k, cmd, vars }: { k: TKey; cmd: string; vars?: Vars }) {
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export const ru: Record<TKey, string> = {
"startup.copied": "Скопировано",
"startup.recommended": "Рекомендуемое исправление: {cmd}",
"startup.navRisk": "Защита запуска требует внимания",
"startup.codexRuntime.clampHidden": "Некоторые уровни reasoning effort скрыты, потому что OpenCodex использовал Codex {version}.",
"startup.codexRuntime.clampHiddenWithEfforts": "Некоторые уровни reasoning effort скрыты, потому что OpenCodex использовал Codex {version} (удалены: {efforts}).",
"startup.codexRuntime.olderBinary": "OpenCodex использует более старый бинарник Codex ({version}). Доступна более новая установка.",

// dashboard
"dash.subtitle": "Актуальное состояние локального прокси opencodex, его провайдеров и моделей, маршрутизируемых в Codex.",
Expand Down
3 changes: 3 additions & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export const zh: Record<TKey, string> = {
"startup.copied": "已复制",
"startup.recommended": "推荐修复:{cmd}",
"startup.navRisk": "启动保护需要处理",
"startup.codexRuntime.clampHidden": "部分推理强度选项已隐藏,因为 OpenCodex 使用了 Codex {version}。",
"startup.codexRuntime.clampHiddenWithEfforts": "部分推理强度选项已隐藏,因为 OpenCodex 使用了 Codex {version}(已移除:{efforts})。",
"startup.codexRuntime.olderBinary": "OpenCodex 正在使用较旧的 Codex 二进制文件({version})。检测到可用的较新安装。",

// dashboard
"dash.subtitle": "本地 opencodex 代理、其提供方以及路由到 Codex 的模型的实时状态。",
Expand Down
70 changes: 69 additions & 1 deletion gui/src/pages/Startup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export default function Startup({ apiBase }: { apiBase: string }) {
const [trayError, setTrayError] = useState(false);
const [installBusy, setInstallBusy] = useState<StartupInstallAction | null>(null);
const [installResult, setInstallResult] = useState<{ kind: "success" | "error"; action: StartupInstallAction; detail?: string } | null>(null);
const [codexRuntimeWarning, setCodexRuntimeWarning] = useState<string | null>(null);
const [codexRuntimeFix, setCodexRuntimeFix] = useState<string | null>(null);

const refresh = useCallback(async (signal?: AbortSignal) => {
setLoading(true);
Expand All @@ -99,6 +101,55 @@ export default function Startup({ apiBase }: { apiBase: string }) {
if (signal?.aborted) return;
setData(next);
setFailed(next.diagnosticStale);
try {
const settingsRes = await fetch(`${apiBase}/api/settings`, { signal });
if (settingsRes.ok) {
const settings = await settingsRes.json() as {
codexRuntime?: {
version?: string | null;
newerAvailable?: { path?: string; version?: string | null } | null;
catalogClamp?: { active?: boolean; removedEfforts?: string[]; runtimeVersion?: string | null };
};
};
if (!signal?.aborted) {
// newerAvailable comes from /api/settings full resolveCodexRuntime()
// (memoized alternative discovery), not discoverAlternatives:false.
const runtime = settings.codexRuntime;
const clampActive = Boolean(runtime?.catalogClamp?.active);
const newer = Boolean(runtime?.newerAvailable);
const version = (clampActive
? runtime?.catalogClamp?.runtimeVersion
: runtime?.version) ?? runtime?.version ?? "unknown";
const efforts = (runtime?.catalogClamp?.removedEfforts ?? []).join(", ");
if (clampActive) {
setCodexRuntimeWarning(
efforts
? t("startup.codexRuntime.clampHiddenWithEfforts", { version, efforts })
: t("startup.codexRuntime.clampHidden", { version }),
);
} else if (newer) {
setCodexRuntimeWarning(t("startup.codexRuntime.olderBinary", { version }));
} else {
setCodexRuntimeWarning(null);
}
setCodexRuntimeFix(
newer
? "ocx doctor --fix-codex-runtime && ocx sync"
: clampActive
? "ocx sync"
: null,
);
}
} else if (!signal?.aborted) {
setCodexRuntimeWarning(null);
setCodexRuntimeFix(null);
}
Comment thread
Wibias marked this conversation as resolved.
} catch {
if (!signal?.aborted) {
setCodexRuntimeWarning(null);
setCodexRuntimeFix(null);
}
}
if (next.platform === "win32") {
setTrayError(false);
try {
Expand Down Expand Up @@ -130,7 +181,7 @@ export default function Startup({ apiBase }: { apiBase: string }) {
} finally {
if (!signal?.aborted) setLoading(false);
}
}, [apiBase]);
}, [apiBase, t]);

useEffect(() => {
const controller = new AbortController();
Expand Down Expand Up @@ -233,6 +284,23 @@ export default function Startup({ apiBase }: { apiBase: string }) {
) : data ? (
<>
{failed && <div className="notice notice-warn" role="alert">{t("startup.staleData")}</div>}
{codexRuntimeWarning && (
<div className="notice notice-warn" role="status">
<p>{codexRuntimeWarning}</p>
{codexRuntimeFix && (
<p>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => void copyCommand(codexRuntimeFix)}
>
{copied === codexRuntimeFix ? t("startup.copied") : t("startup.copy")}
</button>
<code style={{ marginLeft: "0.5rem" }}>{codexRuntimeFix}</code>
Comment thread
Wibias marked this conversation as resolved.
</p>
)}
</div>
)}
<section className={`panel startup-hero ${statusClass}`} aria-live="polite">
<div className="startup-hero-icon"><StatusIcon /></div>
<div className="startup-hero-copy">
Expand Down
62 changes: 61 additions & 1 deletion src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
import { countPendingOpencodexHistory } from "../codex/history-provider";
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
import {
displayCodexRuntimePath,
loadLastEffortClamp,
persistCodexRuntime,
resolveAndPersistCodexRuntime,
resolveCodexRuntime,
} from "../codex/runtime";
export { resolveCodexHomeDir } from "../codex/home";

const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
Expand Down Expand Up @@ -434,7 +441,31 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
return lines;
}

export async function runDoctor(): Promise<void> {
export async function runDoctor(args: string[] = []): Promise<void> {
if (args.includes("--fix-codex-runtime")) {
const resolved = resolveCodexRuntime();
if (!resolved.newerAvailable) {
console.log("No newer Codex runtime found; keeping current selection.");
const current = resolveAndPersistCodexRuntime();
console.log(`Selected: ${displayCodexRuntimePath(current.runtime.command)} (${current.runtime.version ?? "unknown"})`);
return;
}
if (resolved.runtime.source === "environment") {
console.log("CODEX_CLI_PATH currently overrides configured runtimes.");
console.log(`Unset or update CODEX_CLI_PATH to use ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"}).`);
console.log("Then run ocx sync.");
return;
}
persistCodexRuntime({
command: resolved.newerAvailable.command,
version: resolved.newerAvailable.version,
source: "configured",
Comment thread
Wibias marked this conversation as resolved.
});
console.log(`Updated Codex runtime to ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"}).`);
console.log("Run ocx sync to refresh the catalog against this runtime.");
Comment thread
Wibias marked this conversation as resolved.
return;
}

console.log("opencodex doctor\n");

// Ordering note: the memory/runtime section renders after "Running proxy
Expand All @@ -455,6 +486,35 @@ export async function runDoctor(): Promise<void> {
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);

console.log("\nCodex runtime selection");
{
const resolved = resolveCodexRuntime();
const selected = resolved.runtime;
console.log(` ok Selected runtime: ${displayCodexRuntimePath(selected.command)} (${selected.version ?? "unknown"}, source=${selected.source})`);
const envFailures = resolved.failures.filter(item => item.source === "environment");
for (const failure of envFailures) {
console.log(` !! Invalid CODEX_CLI_PATH: ${failure.reason}`);
}
const shimFailures = resolved.failures.filter(item => item.source === "shim");
if (shimFailures.length > 0) {
console.log(` !! Stale shim target rejected (${shimFailures.length})`);
}
if (resolved.replacedConfigured) {
console.log(` !! Preferred runtime unavailable; fell back to ${displayCodexRuntimePath(selected.command)}`);
}
if (resolved.newerAvailable) {
console.log(` !! Multiple Codex installations found.`);
console.log(` ok Newer usable runtime found: ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"})`);
console.log(" Suggested: set CODEX_CLI_PATH to the desired binary and run ocx sync.");
console.log(" Optional: ocx doctor --fix-codex-runtime");
}
const lastClamp = loadLastEffortClamp();
if (lastClamp && lastClamp.removedEfforts.length > 0) {
console.log(` !! ${lastClamp.removedEfforts.join(" and ")} were removed during catalog sync.`);
console.log(" Suggested: set CODEX_CLI_PATH to a newer Codex binary and run ocx sync.");
}
}

const currentProxyEnv = collectProxyEnv();
const configuredProxy = collectConfiguredProxy();
const runningProxyEnv = collectRunningProxyEnv();
Expand Down
12 changes: 11 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,16 @@ async function handleStatus() {
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
console.log(` Service: ${status.json.service.summary}`);
console.log(` ${status.json.codexShim.summary}`);
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
console.log(` Codex source: ${status.json.codexRuntime.source}`);
console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`);
if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) {
console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`);
}
if (status.json.codexRuntime.warning) {
console.log(` ⚠️ ${status.json.codexRuntime.warning}`);
}
if (status.json.codexPlugins.applicable) {
const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
Expand Down Expand Up @@ -581,7 +591,7 @@ switch (command) {
break;
case "doctor": {
const { runDoctor } = await import("./doctor");
await runDoctor();
await runDoctor(args.slice(1));
break;
}
case "debug": {
Expand Down
Loading
Loading