From b8485a54905fae95e823149da8b1512bffb8f6f3 Mon Sep 17 00:00:00 2001 From: Varun Deep Saini Date: Fri, 20 Mar 2026 09:43:26 +0530 Subject: [PATCH] Fix Codex provider banner for custom overrides Signed-off-by: Varun Deep Saini --- apps/web/src/appSettings.test.ts | 24 +++++++++ apps/web/src/appSettings.ts | 29 ++++++++++- .../web/src/components/ChatView.logic.test.ts | 50 ++++++++++++++++++- apps/web/src/components/ChatView.logic.ts | 14 +++++- apps/web/src/components/ChatView.tsx | 43 ++++++++++------ 5 files changed, 140 insertions(+), 20 deletions(-) diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index 605b281df3..53666f25e0 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -5,6 +5,7 @@ import { AppSettingsSchema, DEFAULT_TIMESTAMP_FORMAT, getAppModelOptions, + getCodexProviderOverrides, normalizeCustomModelSlugs, resolveAppModelSelection, } from "./appSettings"; @@ -113,3 +114,26 @@ describe("AppSettingsSchema", () => { }); }); }); + +describe("getCodexProviderOverrides", () => { + it("returns undefined when both overrides are blank", () => { + expect( + getCodexProviderOverrides({ + codexBinaryPath: " ", + codexHomePath: "", + }), + ).toBeUndefined(); + }); + + it("returns trimmed override values", () => { + expect( + getCodexProviderOverrides({ + codexBinaryPath: " /usr/local/bin/codex ", + codexHomePath: " /Users/test/.codex ", + }), + ).toEqual({ + binaryPath: "/usr/local/bin/codex", + homePath: "/Users/test/.codex", + }); + }); +}); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index e4f4d8b1ca..e98dae91b9 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -1,6 +1,10 @@ import { useCallback } from "react"; import { Option, Schema } from "effect"; -import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts"; +import { + TrimmedNonEmptyString, + type ProviderKind, + type ProviderStartOptions, +} from "@t3tools/contracts"; import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { useLocalStorage } from "./hooks/useLocalStorage"; import { EnvMode } from "./components/BranchToolbar.logic"; @@ -87,6 +91,29 @@ function normalizeAppSettings(settings: AppSettings): AppSettings { customClaudeModels: normalizeCustomModelSlugs(settings.customClaudeModels, "claudeAgent"), }; } + +function trimToUndefined(value: string | null | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed || undefined; +} + +export function getCodexProviderOverrides(settings: { + codexBinaryPath: AppSettings["codexBinaryPath"]; + codexHomePath: AppSettings["codexHomePath"]; +}): NonNullable | undefined { + const binaryPath = trimToUndefined(settings.codexBinaryPath); + const homePath = trimToUndefined(settings.codexHomePath); + + if (!binaryPath && !homePath) { + return undefined; + } + + return { + ...(binaryPath ? { binaryPath } : {}), + ...(homePath ? { homePath } : {}), + }; +} + export function getAppModelOptions( provider: ProviderKind, customModels: readonly string[], diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index bf72ec0b84..6f6f9e4573 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -1,7 +1,23 @@ -import { ThreadId } from "@t3tools/contracts"; +import { ThreadId, type ServerProviderStatus } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; -import { buildExpiredTerminalContextToastCopy, deriveComposerSendState } from "./ChatView.logic"; +import { + buildExpiredTerminalContextToastCopy, + deriveComposerSendState, + resolveProviderHealthBannerStatus, +} from "./ChatView.logic"; + +function makeProviderStatus(overrides: Partial = {}): ServerProviderStatus { + return { + provider: "codex", + status: "error", + available: false, + authStatus: "unknown", + checkedAt: "2026-03-20T00:00:00.000Z", + message: "Codex CLI (`codex`) is not installed or not on PATH.", + ...overrides, + }; +} describe("deriveComposerSendState", () => { it("treats expired terminal pills as non-sendable content", () => { @@ -67,3 +83,33 @@ describe("buildExpiredTerminalContextToastCopy", () => { }); }); }); + +describe("resolveProviderHealthBannerStatus", () => { + it("keeps the server status when no local Codex overrides are configured", () => { + const status = makeProviderStatus(); + + expect(resolveProviderHealthBannerStatus(status, false)).toEqual(status); + }); + + it("hides Codex status when a custom binary path is configured", () => { + expect(resolveProviderHealthBannerStatus(makeProviderStatus(), true)).toBeNull(); + }); + + it("hides Codex status when a custom CODEX_HOME is configured", () => { + expect( + resolveProviderHealthBannerStatus( + makeProviderStatus({ status: "warning", available: true }), + true, + ), + ).toBeNull(); + }); + + it("keeps non-Codex provider status visible even with Codex overrides", () => { + const status = makeProviderStatus({ + provider: "claudeAgent", + message: "Claude Agent CLI (`claude`) is not installed or not on PATH.", + }); + + expect(resolveProviderHealthBannerStatus(status, true)).toEqual(status); + }); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 80567927b3..b7f527754f 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1,4 +1,9 @@ -import { ProjectId, type ProviderKind, type ThreadId } from "@t3tools/contracts"; +import { + ProjectId, + type ProviderKind, + type ServerProviderStatus, + type ThreadId, +} from "@t3tools/contracts"; import { type ChatMessage, type Thread } from "../types"; import { randomUUID } from "~/lib/utils"; import { getAppModelOptions } from "../appSettings"; @@ -131,6 +136,13 @@ export function getCustomModelOptionsByProvider(settings: { }; } +export function resolveProviderHealthBannerStatus( + status: ServerProviderStatus | null, + hasCodexOverrides: boolean, +): ServerProviderStatus | null { + return status?.provider === "codex" && hasCodexOverrides ? null : status; +} + export function deriveComposerSendState(options: { prompt: string; imageCount: number; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index eaead424fb..855eb1233b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -126,7 +126,11 @@ import { import { SidebarTrigger } from "./ui/sidebar"; import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; -import { resolveAppModelSelection, useAppSettings } from "../appSettings"; +import { + getCodexProviderOverrides, + resolveAppModelSelection, + useAppSettings, +} from "../appSettings"; import { isTerminalFocused } from "../lib/terminalFocus"; import { type ComposerImageAttachment, @@ -173,6 +177,7 @@ import { LastInvokedScriptByProjectSchema, PullRequestDialogState, readFileAsDataUrl, + resolveProviderHealthBannerStatus, revokeBlobPreviewUrl, revokeUserMessagePreviewUrls, SendPhase, @@ -662,17 +667,18 @@ export default function ChatView({ threadId }: ChatViewProps) { } return undefined; }, [draftModelOptions, selectedModel, selectedProvider]); - const providerOptionsForDispatch = useMemo(() => { - if (!settings.codexBinaryPath && !settings.codexHomePath) { - return undefined; - } - return { - codex: { - ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}), - ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}), - }, - }; - }, [settings.codexBinaryPath, settings.codexHomePath]); + const codexProviderOverrides = useMemo( + () => + getCodexProviderOverrides({ + codexBinaryPath: settings.codexBinaryPath, + codexHomePath: settings.codexHomePath, + }), + [settings.codexBinaryPath, settings.codexHomePath], + ); + const providerOptionsForDispatch = useMemo( + () => (codexProviderOverrides ? { codex: codexProviderOverrides } : undefined), + [codexProviderOverrides], + ); const selectedModelForPicker = selectedModel; const modelOptionsByProvider = useMemo( () => getCustomModelOptionsByProvider(settings), @@ -1150,9 +1156,14 @@ export default function ChatView({ threadId }: ChatViewProps) { const availableEditors = serverConfigQuery.data?.availableEditors ?? EMPTY_AVAILABLE_EDITORS; const providerStatuses = serverConfigQuery.data?.providers ?? EMPTY_PROVIDER_STATUSES; const activeProvider = activeThread?.session?.provider ?? "codex"; - const activeProviderStatus = useMemo( - () => providerStatuses.find((status) => status.provider === activeProvider) ?? null, - [activeProvider, providerStatuses], + const hasCodexProviderOverrides = codexProviderOverrides !== undefined; + const activeProviderBannerStatus = useMemo( + () => + resolveProviderHealthBannerStatus( + providerStatuses.find((status) => status.provider === activeProvider) ?? null, + hasCodexProviderOverrides, + ), + [activeProvider, hasCodexProviderOverrides, providerStatuses], ); const activeProjectCwd = activeProject?.cwd ?? null; const activeThreadWorktreePath = activeThread?.worktreePath ?? null; @@ -3528,7 +3539,7 @@ export default function ChatView({ threadId }: ChatViewProps) { {/* Error banner */} - + setThreadError(activeThread.id, null)}