import React, { useState } from "react"; import { CustomAuthFlow } from "./components/add-model-flow.tsx"; import { Config, writeConfig, readConfig, mergeEnvVar, mergeAutofixEnvVar, AuthError, readAuthForModel, } from "./config.ts"; import { HeightlessCenteredBox } from "./components/centered-box.tsx"; import { Span, useApp } from "paintcannon-react"; import { useKeyboard } from "./hooks/use-keyboard.ts"; import { TerminalFlex } from "./components/terminal-flex.tsx"; import { AppShell } from "./components/app-shell.tsx"; function PreflightShell({ children }: { children: React.ReactNode }) { return ( {children} ); } function ModelDetails({ name, model, baseUrl, }: { name: string; model?: string; baseUrl?: string; }) { const displayName = model && model !== name ? `${name} (${model})` : name; return ( Model: {displayName} {baseUrl && Base URL: {baseUrl}} ); } function AuthErrorPanel({ error, isRetrying }: { error: AuthError; isRetrying: boolean }) { const title = (() => { if (error.type === "missing") return "Auth is still missing"; if (error.type === "invalid") return "Your auth configuration is invalid"; return "Your auth command failed"; })(); return ( {title} {error.message} {error.type === "command_failed" && error.stderr && ( stderr: {error.stderr} )} {error.type === "command_failed" ? "[R]etry | " : ""}[ESC] to go back {isRetrying ? " (retrying...)" : ""} ); } function matchCodex( model: Config["models"][number], arms: { codex: ( model: Extract< Config["models"][number], { type: "codex"; } >, ) => T; others: ( model: Exclude< Config["models"][number], { type: "codex"; } >, ) => T; }, ): T { if (model.type === "codex") return arms.codex(model); return arms.others(model); } function resolveModelFromConfig( config: Config, model: Config["models"][number], ): Config["models"][number] { const exact = config.models.find(candidate => { return matchCodex(model, { codex: model => { return ( candidate.type === "codex" && candidate.nickname === model.nickname && candidate.model === model.model ); }, others: model => { if (candidate.type === "codex") return false; return candidate.nickname === model.nickname && candidate.baseUrl === model.baseUrl; }, }); }); if (exact) return exact; return model; } function resolveAutofixModelFromConfig( config: Config, model: Exclude, key: K, ): Exclude { const candidate = config[key]; if (candidate && candidate.baseUrl === model.baseUrl) return candidate as Exclude; return (candidate ?? model) as Exclude; } export function PreflightModelAuth({ model, config, configPath, error, }: { model: Config["models"][number]; config: Config; configPath: string; error?: string; }) { const app = useApp(); const [exitMessage, setExitMessage] = useState(null); const [authError, setAuthError] = useState(null); const [isRetrying, setIsRetrying] = useState(false); const [currentModel, setCurrentModel] = useState(model); useKeyboard(async event => { if (event.key === "Escape" && authError) { setAuthError(null); setIsRetrying(false); } else if (event.key === "r" && authError && authError.type === "command_failed") { setIsRetrying(true); const valid = await validateAuth(); if (valid) { app.exit(); } } else if (!(event.key === "Escape")) setExitMessage(null); }); const validateAuth = async () => { const reloadedConfig = await readConfig(configPath); const resolvedModel = resolveModelFromConfig(reloadedConfig, currentModel); setCurrentModel(resolvedModel); const result = await readAuthForModel(resolvedModel, reloadedConfig); if (result.ok === false) { setAuthError(result.error); setIsRetrying(false); return false; } return true; }; return ( {error && ( {error} )} {authError && } {!authError && ( { setExitMessage("Press CTRL-C to exit"); }} onComplete={async auth => { let index = config.models.indexOf(model); let updatedModel = model; if (index >= 0 && auth) { await matchCodex(model, { codex: async model => { if (auth.type === "codex") { const updatedModels = [...config.models]; updatedModel = { ...model, auth, }; updatedModels[index] = updatedModel; await writeConfig( { ...config, models: updatedModels, }, configPath, ); } }, others: async model => { if (auth.type === "env") { await writeConfig(mergeEnvVar(config, model, auth.name), configPath); } else if (auth.type === "command") { const updatedModels = [...config.models]; updatedModel = { ...model, auth, }; updatedModels[index] = updatedModel; await writeConfig( { ...config, models: updatedModels, }, configPath, ); } }, }); } setCurrentModel(updatedModel); // Reload config to ensure we validate against the updated state const reloadedConfig = await readConfig(configPath); const resolvedModel = resolveModelFromConfig(reloadedConfig, updatedModel); setCurrentModel(resolvedModel); const result = await readAuthForModel(resolvedModel, reloadedConfig); if (result.ok) { app.exit(); } else { setAuthError(result.error); } }} /> )} {!authError && exitMessage && ( {exitMessage} )} ); } export function PreflightAutofixAuth({ autofixKey, model, config, configPath, }: { autofixKey: K; model: Exclude; config: Config; configPath: string; }) { const app = useApp(); const [exitMessage, setExitMessage] = useState(null); const [authError, setAuthError] = useState(null); const [isRetrying, setIsRetrying] = useState(false); const [currentModel, setCurrentModel] = useState(model); useKeyboard(async event => { if (event.key === "Escape" && authError) { setAuthError(null); setIsRetrying(false); } else if (event.key === "r" && authError && authError.type === "command_failed") { setIsRetrying(true); const valid = await validateAuth(); if (valid) { app.exit(); } } else if (!(event.key === "Escape")) setExitMessage(null); }); const validateAuth = async () => { const reloadedConfig = await readConfig(configPath); const resolvedModel = resolveAutofixModelFromConfig(reloadedConfig, currentModel, autofixKey); setCurrentModel(resolvedModel); const result = await readAuthForModel(resolvedModel, reloadedConfig); if (result.ok === false) { setAuthError(result.error); setIsRetrying(false); return false; } return true; }; const modelName = (() => { if (autofixKey === "diffApply") return "diff-apply"; const _: "fixJson" = autofixKey; return "fix-json"; })(); return ( {authError && } {!authError && ( <> {`It looks like we need to set up auth for the ${modelName} model`} { setExitMessage("Press CTRL-C to exit"); }} onComplete={async auth => { let updatedModel = model; if (auth) { if (auth.type === "env") { await writeConfig( mergeAutofixEnvVar(config, autofixKey, model, auth.name), configPath, ); } else if (auth.type === "command") { const merged = { ...config, }; updatedModel = { ...model, auth, }; merged[autofixKey] = updatedModel; await writeConfig(merged, configPath); } } setCurrentModel(updatedModel); // Reload config to ensure we validate against the updated state const reloadedConfig = await readConfig(configPath); const resolvedModel = resolveAutofixModelFromConfig( reloadedConfig, updatedModel, autofixKey, ); setCurrentModel(resolvedModel); const result = await readAuthForModel(resolvedModel, reloadedConfig); if (result.ok) { app.exit(); } else { setAuthError(result.error); } }} /> )} {!authError && exitMessage && ( {exitMessage} )} ); }