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}
)}
);
}