From f8883d0495aa6fb3162077667827b187220c7a7e Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 19 Feb 2026 02:42:24 -0800 Subject: [PATCH] settings page --- apps/desktop/src/main.ts | 86 +++++++- apps/desktop/src/preload.ts | 12 ++ apps/server/src/codexAppServerManager.ts | 9 +- apps/web/src/appSettings.ts | 109 ++++++++++ apps/web/src/components/ChatView.tsx | 8 + apps/web/src/components/Sidebar.tsx | 46 ++-- apps/web/src/routeTree.gen.ts | 24 ++- apps/web/src/routes/_chat.settings.tsx | 259 +++++++++++++++++++++++ apps/web/src/routes/_chat.tsx | 21 +- apps/web/src/vite-env.d.ts | 1 + packages/contracts/src/provider.test.ts | 22 ++ packages/contracts/src/provider.ts | 2 + 12 files changed, 570 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/appSettings.ts create mode 100644 apps/web/src/routes/_chat.settings.tsx diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index fcc120a460ddd758b2fd1153cb3a2577d06de29a..c741deec8aeabf9aeb66827c034a37df8b9b1db0 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -5,7 +5,15 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; -import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from "electron"; +import { + app, + BrowserWindow, + dialog, + ipcMain, + Menu, + type MenuItemConstructorOptions, + shell, +} from "electron"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { fixPath } from "./fixPath"; @@ -16,6 +24,7 @@ const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; +const MENU_ACTION_CHANNEL = "desktop:menu-action"; const ROOT_DIR = path.resolve(__dirname, "../../.."); const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.mjs"); const WEB_ENTRY = path.join(ROOT_DIR, "apps/web/dist/index.html"); @@ -33,6 +42,80 @@ let restartAttempt = 0; let restartTimer: ReturnType | null = null; let isQuitting = false; +function dispatchMenuAction(action: string): void { + const existingWindow = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0]; + const targetWindow = existingWindow ?? createWindow(); + if (!existingWindow) { + mainWindow = targetWindow; + } + + const send = () => { + if (targetWindow.isDestroyed()) return; + targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); + if (!targetWindow.isVisible()) { + targetWindow.show(); + } + targetWindow.focus(); + }; + + if (targetWindow.webContents.isLoadingMainFrame()) { + targetWindow.webContents.once("did-finish-load", send); + return; + } + + send(); +} + +function configureApplicationMenu(): void { + const template: MenuItemConstructorOptions[] = []; + + if (process.platform === "darwin") { + template.push({ + label: app.name, + submenu: [ + { role: "about" }, + { + label: "Settings...", + accelerator: "CmdOrCtrl+,", + click: () => dispatchMenuAction("open-settings"), + }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }); + } + + template.push( + { + label: "File", + submenu: [ + ...(process.platform === "darwin" + ? [] + : [ + { + label: "Settings...", + accelerator: "CmdOrCtrl+,", + click: () => dispatchMenuAction("open-settings"), + }, + { type: "separator" as const }, + ]), + { role: process.platform === "darwin" ? "close" : "quit" }, + ], + }, + { role: "editMenu" }, + { role: "viewMenu" }, + { role: "windowMenu" }, + ); + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + function resolveResourcePath(fileName: string): string | null { const candidates = [ path.join(__dirname, "../resources", fileName), @@ -322,6 +405,7 @@ app.on("before-quit", () => { app.whenReady().then(() => { configureAppIdentity(); + configureApplicationMenu(); void bootstrap(); app.on("activate", () => { diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index fb90831fc0ded445d5ba9dcd85ef58c9b9c7a4c0..464832b33d307420860bfd9d90a6977d9b9d7247 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -4,6 +4,7 @@ const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; +const MENU_ACTION_CHANNEL = "desktop:menu-action"; const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null; contextBridge.exposeInMainWorld("desktopBridge", { @@ -13,4 +14,15 @@ contextBridge.exposeInMainWorld("desktopBridge", { showContextMenu: (items: { id: string; label: string }[]) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items) as Promise, openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url) as Promise, + onMenuAction: (listener: (action: string) => void) => { + const wrappedListener = (_event: Electron.IpcRendererEvent, action: unknown) => { + if (typeof action !== "string") return; + listener(action); + }; + + ipcRenderer.on(MENU_ACTION_CHANNEL, wrappedListener); + return () => { + ipcRenderer.removeListener(MENU_ACTION_CHANNEL, wrappedListener); + }; + }, }); diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts index 6122ec601f61e1415abcba35662c7a831c1ffebd..912f9b2db867aabd380a17fa2e83ab348dc97dd7 100644 --- a/apps/server/src/codexAppServerManager.ts +++ b/apps/server/src/codexAppServerManager.ts @@ -163,9 +163,14 @@ export class CodexAppServerManager extends EventEmitter; + +const DEFAULT_APP_SETTINGS: AppSettings = appSettingsSchema.parse({}); + +let listeners: Array<() => void> = []; +let cachedRawSettings: string | null | undefined; +let cachedSnapshot: AppSettings = DEFAULT_APP_SETTINGS; + +function emitChange(): void { + for (const listener of listeners) { + listener(); + } +} + +function parsePersistedSettings(value: string | null): AppSettings { + if (!value) { + return DEFAULT_APP_SETTINGS; + } + + try { + const parsed = JSON.parse(value) as unknown; + return appSettingsSchema.parse(parsed); + } catch { + return DEFAULT_APP_SETTINGS; + } +} + +export function getAppSettingsSnapshot(): AppSettings { + if (typeof window === "undefined") { + return DEFAULT_APP_SETTINGS; + } + + const raw = window.localStorage.getItem(APP_SETTINGS_STORAGE_KEY); + if (raw === cachedRawSettings) { + return cachedSnapshot; + } + + cachedRawSettings = raw; + cachedSnapshot = parsePersistedSettings(raw); + return cachedSnapshot; +} + +function persistSettings(next: AppSettings): void { + if (typeof window === "undefined") return; + + const raw = JSON.stringify(next); + try { + if (raw !== cachedRawSettings) { + window.localStorage.setItem(APP_SETTINGS_STORAGE_KEY, raw); + } + } catch { + // Best-effort persistence only. + } + + cachedRawSettings = raw; + cachedSnapshot = next; +} + +function subscribe(listener: () => void): () => void { + listeners.push(listener); + + const onStorage = (event: StorageEvent) => { + if (event.key === APP_SETTINGS_STORAGE_KEY) { + emitChange(); + } + }; + + window.addEventListener("storage", onStorage); + return () => { + listeners = listeners.filter((entry) => entry !== listener); + window.removeEventListener("storage", onStorage); + }; +} + +export function useAppSettings() { + const settings = useSyncExternalStore(subscribe, getAppSettingsSnapshot, () => DEFAULT_APP_SETTINGS); + + const updateSettings = useCallback((patch: Partial) => { + const next = appSettingsSchema.parse({ + ...getAppSettingsSnapshot(), + ...patch, + }); + persistSettings(next); + emitChange(); + }, []); + + const resetSettings = useCallback(() => { + persistSettings(DEFAULT_APP_SETTINGS); + emitChange(); + }, []); + + return { + settings, + updateSettings, + resetSettings, + defaults: DEFAULT_APP_SETTINGS, + } as const; +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 9b5288a4fdfd08a45d380c5cad659688a955159d..9039149da193d6310f881ddb5aa50e62d7b31622 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -35,6 +35,7 @@ import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; import { isElectron } from "../env"; +import { useAppSettings } from "../appSettings"; import { buildBootstrapInput } from "../historyBootstrap"; import { type ComposerTriggerKind, @@ -376,6 +377,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const { state, dispatch } = useStore(); const api = useNativeApi(); const { resolvedTheme } = useTheme(); + const { settings: appSettings } = useAppSettings(); const queryClient = useQueryClient(); const createWorktreeMutation = useMutation( gitCreateWorktreeMutationOptions({ api, queryClient }), @@ -642,6 +644,8 @@ export default function ChatView({ threadId }: ChatViewProps) { const runtimeApprovalPolicy = state.runtimeMode === "full-access" ? "never" : "on-request"; const runtimeSandboxMode = state.runtimeMode === "full-access" ? "danger-full-access" : "workspace-write"; + const codexBinaryPath = appSettings.codexBinaryPath.trim(); + const codexHomePath = appSettings.codexHomePath.trim(); const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; const composerTrigger = useMemo( () => detectComposerTrigger(prompt, composerCursor), @@ -1515,6 +1519,8 @@ export default function ChatView({ threadId }: ChatViewProps) { cwd: cwdOverride ?? activeThread.worktreePath ?? activeProject.cwd, model: selectedModel || undefined, resumeThreadId: priorCodexThreadId ?? undefined, + ...(codexBinaryPath.length > 0 ? { codexBinaryPath } : {}), + ...(codexHomePath.length > 0 ? { codexHomePath } : {}), approvalPolicy: runtimeApprovalPolicy, sandboxMode: runtimeSandboxMode, }); @@ -1555,6 +1561,8 @@ export default function ChatView({ threadId }: ChatViewProps) { activeProject, activeThread, api, + codexBinaryPath, + codexHomePath, dispatch, runtimeApprovalPolicy, runtimeSandboxMode, diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index e89d858109d1aaff19b0a871e9e22bbda0165f3c..9291843ea9d07e92cbfa44fc8782afe5a55e69dd 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,11 +1,11 @@ -import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; +import { TerminalIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { ResolvedKeybindingsConfig } from "@t3tools/contracts"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigate, useParams } from "@tanstack/react-router"; +import { useAppSettings } from "../appSettings"; import { isElectron } from "../env"; import { APP_STAGE_LABEL } from "../branding"; -import { useTheme } from "../hooks/useTheme"; import { DEFAULT_MODEL } from "../model-logic"; import { derivePendingApprovals } from "../session-logic"; import { useStore } from "../store"; @@ -18,7 +18,6 @@ import { toastManager } from "./ui/toast"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; import { createThread } from "../threadFactory"; -const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; function formatRelativeTime(iso: string): string { @@ -117,6 +116,7 @@ export default function Sidebar() { const { state, dispatch } = useStore(); const api = useNativeApi(); const navigate = useNavigate(); + const { settings: appSettings } = useAppSettings(); const params = useParams({ strict: false }); const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; const { data: keybindings = EMPTY_KEYBINDINGS } = useQuery({ @@ -127,7 +127,6 @@ export default function Sidebar() { const removeWorktreeMutation = useMutation( gitRemoveWorktreeMutationOptions({ api, queryClient }), ); - const { theme, setTheme } = useTheme(); const [addingProject, setAddingProject] = useState(false); const [newCwd, setNewCwd] = useState(""); const [isPickingFolder, setIsPickingFolder] = useState(false); @@ -272,6 +271,17 @@ export default function Sidebar() { const thread = state.threads.find((t) => t.id === threadId); if (!thread) return; + if (appSettings.confirmThreadDelete) { + const confirmed = await api.dialogs.confirm( + [ + `Delete thread \"${thread.title}\"?`, + "This permanently clears conversation history for this thread.", + ].join("\n"), + ); + if (!confirmed) { + return; + } + } const threadProject = state.projects.find((project) => project.id === thread.projectId); const orphanedWorktreePath = getOrphanedWorktreePathForThread(state.threads, threadId); const displayWorktreePath = orphanedWorktreePath @@ -349,7 +359,16 @@ export default function Sidebar() { }); } }, - [api, dispatch, navigate, removeWorktreeMutation, routeThreadId, state.projects, state.threads], + [ + api, + appSettings.confirmThreadDelete, + dispatch, + navigate, + removeWorktreeMutation, + routeThreadId, + state.projects, + state.threads, + ], ); const handleProjectContextMenu = useCallback( @@ -436,25 +455,10 @@ export default function Sidebar() { T3 Code - + {APP_STAGE_LABEL} - {/* New thread (global) */} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index a005a05e5ac23fb035c653563cd788382b8edc1f..880d5ef64ba441142945fdfe91d85a9e897d7dbe 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as ChatRouteImport } from './routes/_chat' import { Route as ChatIndexRouteImport } from './routes/_chat.index' +import { Route as ChatSettingsRouteImport } from './routes/_chat.settings' import { Route as ChatThreadIdRouteImport } from './routes/_chat.$threadId' const ChatRoute = ChatRouteImport.update({ @@ -22,6 +23,11 @@ const ChatIndexRoute = ChatIndexRouteImport.update({ path: '/', getParentRoute: () => ChatRoute, } as any) +const ChatSettingsRoute = ChatSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => ChatRoute, +} as any) const ChatThreadIdRoute = ChatThreadIdRouteImport.update({ id: '/$threadId', path: '/$threadId', @@ -31,23 +37,26 @@ const ChatThreadIdRoute = ChatThreadIdRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof ChatIndexRoute '/$threadId': typeof ChatThreadIdRoute + '/settings': typeof ChatSettingsRoute } export interface FileRoutesByTo { '/$threadId': typeof ChatThreadIdRoute + '/settings': typeof ChatSettingsRoute '/': typeof ChatIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/_chat': typeof ChatRouteWithChildren '/_chat/$threadId': typeof ChatThreadIdRoute + '/_chat/settings': typeof ChatSettingsRoute '/_chat/': typeof ChatIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/$threadId' + fullPaths: '/' | '/$threadId' | '/settings' fileRoutesByTo: FileRoutesByTo - to: '/$threadId' | '/' - id: '__root__' | '/_chat' | '/_chat/$threadId' | '/_chat/' + to: '/$threadId' | '/settings' | '/' + id: '__root__' | '/_chat' | '/_chat/$threadId' | '/_chat/settings' | '/_chat/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -70,6 +79,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ChatIndexRouteImport parentRoute: typeof ChatRoute } + '/_chat/settings': { + id: '/_chat/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof ChatSettingsRouteImport + parentRoute: typeof ChatRoute + } '/_chat/$threadId': { id: '/_chat/$threadId' path: '/$threadId' @@ -82,11 +98,13 @@ declare module '@tanstack/react-router' { interface ChatRouteChildren { ChatThreadIdRoute: typeof ChatThreadIdRoute + ChatSettingsRoute: typeof ChatSettingsRoute ChatIndexRoute: typeof ChatIndexRoute } const ChatRouteChildren: ChatRouteChildren = { ChatThreadIdRoute: ChatThreadIdRoute, + ChatSettingsRoute: ChatSettingsRoute, ChatIndexRoute: ChatIndexRoute, } diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..740707addd9e71f622f499025f2ce1e8e680736c --- /dev/null +++ b/apps/web/src/routes/_chat.settings.tsx @@ -0,0 +1,259 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { useAppSettings } from "../appSettings"; +import { isElectron } from "../env"; +import { useTheme } from "../hooks/useTheme"; +import { useStore } from "../store"; +import { Button } from "../components/ui/button"; +import { Input } from "../components/ui/input"; +import { Switch } from "../components/ui/switch"; + +const THEME_OPTIONS = [ + { + value: "system", + label: "System", + description: "Match your OS appearance setting.", + }, + { + value: "light", + label: "Light", + description: "Always use the light theme.", + }, + { + value: "dark", + label: "Dark", + description: "Always use the dark theme.", + }, +] as const; + +const RUNTIME_MODE_OPTIONS = [ + { + value: "full-access", + label: "Full access", + description: "Allow commands to run without confirmation prompts.", + }, + { + value: "approval-required", + label: "Supervised", + description: "Require approval prompts before command execution.", + }, +] as const; + +function SettingsRouteView() { + const { theme, setTheme, resolvedTheme } = useTheme(); + const { state, dispatch } = useStore(); + const { settings, defaults, updateSettings } = useAppSettings(); + + const codexBinaryPath = settings.codexBinaryPath; + const codexHomePath = settings.codexHomePath; + + return ( +
+ {isElectron && ( +
+ Settings +
+ )} + +
+
+
+

Settings

+

+ Configure app-level preferences for this device. +

+
+ +
+
+

Appearance

+

+ Choose how T3 Code handles light and dark mode. +

+
+ +
+ {THEME_OPTIONS.map((option) => { + const selected = theme === option.value; + return ( + + ); + })} +
+ +

+ Active theme: {resolvedTheme} +

+
+ +
+
+

Codex App Server

+

+ These overrides apply to new sessions and let you use a non-default Codex install. +

+
+ +
+ + + + +
+

+ Binary source: {codexBinaryPath || "PATH"} +

+ +
+
+
+ +
+
+

Runtime Mode

+

+ Select the default execution policy for this client. +

+
+ +
+ {RUNTIME_MODE_OPTIONS.map((option) => { + const selected = state.runtimeMode === option.value; + return ( + + ); + })} +
+
+ +
+
+

Safety

+

+ Additional guardrails for destructive local actions. +

+
+ +
+
+

Confirm thread deletion

+

+ Ask for confirmation before deleting a thread and its chat history. +

+
+ + updateSettings({ + confirmThreadDelete: Boolean(checked), + }) + } + aria-label="Confirm thread deletion" + /> +
+ + {settings.confirmThreadDelete !== defaults.confirmThreadDelete ? ( +
+ +
+ ) : null} +
+
+
+
+ ); +} + +export const Route = createFileRoute("/_chat/settings")({ + component: SettingsRouteView, +}); diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 702e873b07fdf761c80121b9de3378e86257da4e..ed931773ee937844e77f9ac0ba289fbc7263b9c2 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -1,5 +1,5 @@ -import { Outlet, createFileRoute } from "@tanstack/react-router"; -import { Activity, Suspense, lazy, type ReactNode } from "react"; +import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router"; +import { Activity, Suspense, lazy, type ReactNode, useEffect } from "react"; import Sidebar from "../components/Sidebar"; import { useMediaQuery } from "../hooks/useMediaQuery"; @@ -48,6 +48,23 @@ const DiffPanelWrapper = (props: { children: ReactNode; sheet: boolean }) => { function ChatRouteLayout() { const { state } = useStore(); const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY); + const navigate = useNavigate(); + + useEffect(() => { + const onMenuAction = window.desktopBridge?.onMenuAction; + if (typeof onMenuAction !== "function") { + return; + } + + const unsubscribe = onMenuAction((action) => { + if (action !== "open-settings") return; + void navigate({ to: "/settings" }); + }); + + return () => { + unsubscribe?.(); + }; + }, [navigate]); const diffLoadingFallback = !state.diffOpen || shouldUseDiffSheet ? ( diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 3438d5d7b236ef604eb8111f1a531ddaefd8ea47..93076c718b369f48f230c54b31ed5d9ff033f994 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -8,6 +8,7 @@ interface DesktopBridge { confirm: (message: string) => Promise; showContextMenu: (items: readonly { id: string; label: string }[]) => Promise; openExternal: (url: string) => Promise; + onMenuAction: (listener: (action: string) => void) => () => void; } declare global { diff --git a/packages/contracts/src/provider.test.ts b/packages/contracts/src/provider.test.ts index df9c8e9b3d83f41dab157301e068cd6be519f516..15910a432b3bc51f29082749f2b51500f3a0f852 100644 --- a/packages/contracts/src/provider.test.ts +++ b/packages/contracts/src/provider.test.ts @@ -37,6 +37,28 @@ describe("providerSessionStartInputSchema", () => { }), ).toThrow(); }); + + it("accepts optional codex binary and home path overrides", () => { + const parsed = providerSessionStartInputSchema.parse({ + codexBinaryPath: "/opt/codex/bin/codex", + codexHomePath: "/Users/theo/.codex", + }); + expect(parsed.codexBinaryPath).toBe("/opt/codex/bin/codex"); + expect(parsed.codexHomePath).toBe("/Users/theo/.codex"); + }); + + it("rejects blank codex overrides", () => { + expect(() => + providerSessionStartInputSchema.parse({ + codexBinaryPath: " ", + }), + ).toThrow(); + expect(() => + providerSessionStartInputSchema.parse({ + codexHomePath: " ", + }), + ).toThrow(); + }); }); describe("providerSendTurnInputSchema", () => { diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 65877a6a11fda30af49afad943263dcbe321bc23..06a7582a8b2de5d8ef3afde0e780b0130ccd56a1 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -50,6 +50,8 @@ export const providerSessionStartInputSchema = z.object({ cwd: z.string().min(1).optional(), model: z.string().trim().min(1).optional(), resumeThreadId: z.string().trim().min(1).optional(), + codexBinaryPath: z.string().trim().min(1).optional(), + codexHomePath: z.string().trim().min(1).optional(), approvalPolicy: providerApprovalPolicySchema.default("never"), sandboxMode: providerSandboxModeSchema.default("workspace-write"), }); -- 2.54.0