import React, { useState, useCallback, useLayoutEffect } from "react";
import fs from "fs/promises";
import path from "path";
import json5 from "json5";
import TextInput from "./components/text-input.tsx";
import { Config, Auth, CURRENT_CONFIG_VERSION } from "./config.ts";
import { useColor } from "./theme.ts";
import { KbShortcutPanel } from "./components/kb-select/kb-shortcut-panel.tsx";
import { Item, ShortcutArray } from "./components/kb-select/kb-shortcut-select.tsx";
import { ModelSetup } from "./components/auto-detect-models.tsx";
import { MenuHeader } from "./components/menu-panel.tsx";
import { CenteredBox } from "./components/centered-box.tsx";
import { THEME_COLOR } from "./theme.ts";
import { AutofixModelMenu } from "./components/autofix-model-menu.tsx";
import { SYNTHETIC_PROVIDER, keyFromName } from "./providers.ts";
import { CustomAuthFlow } from "./components/add-model-flow.tsx";
import { recommendedModel } from "./providers.ts";
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";
type SetupStep =
| {
step: "welcome";
}
| {
step: "autofix-setup";
}
| {
step: "autofix-complete";
autofixConfig: {
diffApply: Config["diffApply"];
fixJson: Config["fixJson"];
};
}
| {
step: "name";
models: Config["models"];
autofixConfig?: {
diffApply: Config["diffApply"];
fixJson: Config["fixJson"];
};
}
| {
step: "add-model";
autofixConfig?: {
diffApply: Config["diffApply"];
fixJson: Config["fixJson"];
};
}
| {
step: "done";
};
export function FirstTimeSetup({ configPath }: { configPath: string }) {
return (
);
}
function FirstTimeSetupContent({ configPath }: { configPath: string }) {
const [step, setStep] = useState({
step: "welcome",
});
const [yourName, setYourName] = useState("");
const [nameError, setNameError] = useState(null);
const [defaultApiKeyOverrides, setDefaultApiKeyOverrides] = useState>({});
const themeColor = useColor();
const app = useApp();
const addOverride = useCallback(
async (override: Record) => {
setDefaultApiKeyOverrides({
...defaultApiKeyOverrides,
...override,
});
},
[defaultApiKeyOverrides],
);
useLayoutEffect(() => {
if (step.step === "done") app.exit();
}, [step, app]);
const handleWelcomeContinue = useCallback(() => {
setStep({
step: "autofix-setup",
});
}, []);
const autofixComplete = useCallback(
(autofixConfig: { diffApply: Config["diffApply"]; fixJson: Config["fixJson"] }) => {
setStep({
step: "autofix-complete",
autofixConfig,
});
},
[],
);
const autofixSkip = useCallback(() => {
setStep({
step: "add-model",
});
}, []);
const autofixCompleteContinue = useCallback(() => {
if (step.step === "autofix-complete") {
setStep({
step: "add-model",
autofixConfig: step.autofixConfig,
});
}
}, [step]);
const addModelComplete = useCallback(
(models: Config["models"]) => {
if (step.step === "add-model" && step.autofixConfig) {
setStep({
step: "name",
models,
autofixConfig: step.autofixConfig,
});
} else {
setStep({
step: "name",
models,
});
}
},
[step],
);
const addModelCancel = useCallback(() => {
if (step.step === "add-model" && step.autofixConfig) {
setStep({
step: "autofix-complete",
autofixConfig: step.autofixConfig,
});
} else {
setStep({
step: "welcome",
});
}
}, [step]);
if (step.step === "welcome") return ;
if (step.step === "autofix-setup") {
return (
{
addOverride({
[keyFromName(SYNTHETIC_PROVIDER.name)]: envVar,
});
}}
/>
);
}
if (step.step === "autofix-complete") {
return ;
}
if (step.step === "add-model") {
return (
);
}
if (step.step === "done") return null;
// Assert from typesystem level that we're handled all cases
const _: "name" = step.step;
return (
And finally... What's your name?
Your name:
{
setYourName(value);
setNameError(null);
}}
onSubmit={async () => {
const trimmedName = yourName.trim();
if (!trimmedName) {
setNameError("Name can't be empty");
return;
}
setNameError(null);
const config: Config = {
configVersion: CURRENT_CONFIG_VERSION,
yourName: trimmedName,
models: step.models,
};
if (defaultApiKeyOverrides) {
config.defaultApiKeyOverrides = defaultApiKeyOverrides;
}
if (step.autofixConfig) {
config.diffApply = step.autofixConfig.diffApply;
config.fixJson = step.autofixConfig.fixJson;
}
const dir = path.dirname(configPath);
await fs.mkdir(dir, {
recursive: true,
});
if (configPath.endsWith("json5")) {
await fs.writeFile(configPath, json5.stringify(config, null, 2));
} else {
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
}
setStep({
step: "done",
});
}}
/>
{nameError && (
{nameError}
)}
);
}
type AutofixStates = "choose" | "synthetic-setup" | "diff-apply-custom" | "fix-json-custom";
function AutofixSetup({
onComplete,
onSkip,
onOverrideDefaultApiKey,
}: {
onComplete: (config: { diffApply: Config["diffApply"]; fixJson: Config["fixJson"] }) => void;
onSkip: () => void;
onOverrideDefaultApiKey: (envVar: string) => Promise;
}) {
const [autofixStep, setAutofixStep] = useState("choose");
const [diffApplyConfig, setDiffApplyConfig] = useState();
const shortcutItems = [
{
type: "key" as const,
mapping: {
e: {
label: "💫 Enable autofix models via Synthetic (recommended)",
value: "synthetic",
},
c: {
label: "Use custom models...",
value: "custom",
},
s: {
label: "Skip for now (can be enabled later)",
value: "skip",
},
} as const,
},
] satisfies ShortcutArray<"synthetic" | "custom" | "skip">;
const onSelect = useCallback(
(item: Item<"synthetic" | "custom" | "skip">) => {
if (item.value === "synthetic") {
const defaultEnvVar = SYNTHETIC_PROVIDER.envVar;
if (process.env[defaultEnvVar]) {
onComplete({
diffApply: {
baseUrl: SYNTHETIC_PROVIDER.baseUrl,
model: "hf:syntheticlab/diff-apply",
},
fixJson: {
baseUrl: SYNTHETIC_PROVIDER.baseUrl,
model: "hf:syntheticlab/fix-json",
},
});
} else {
setAutofixStep("synthetic-setup");
}
} else if (item.value === "custom") {
setAutofixStep("diff-apply-custom");
} else {
onSkip();
}
},
[onComplete, onSkip],
);
if (autofixStep === "synthetic-setup") {
return (
{
if (auth && auth.type === "env") await onOverrideDefaultApiKey(auth.name);
const authField =
auth?.type === "command"
? {
auth,
}
: {};
onComplete({
diffApply: {
baseUrl: SYNTHETIC_PROVIDER.baseUrl,
model: "hf:syntheticlab/diff-apply",
...authField,
},
fixJson: {
baseUrl: SYNTHETIC_PROVIDER.baseUrl,
model: "hf:syntheticlab/fix-json",
...authField,
},
});
}}
onCancel={() => setAutofixStep("choose")}
/>
);
}
if (autofixStep === "diff-apply-custom") {
return (
{
setDiffApplyConfig(config);
setAutofixStep("fix-json-custom");
}}
onCancel={() => setAutofixStep("choose")}
>
Even good coding models sometimes make minor mistakes generating code diffs, which can
cause slow retries and can confuse them, since models often aren't trained as well to
handle edit failures as they are successes. Diff-apply is a fast, small model that fixes
minor code diff edit inaccuracies. It speeds up iteration and can significantly improve
model performance.
);
}
if (autofixStep === "fix-json-custom") {
return (
{
onComplete({
diffApply: diffApplyConfig!,
fixJson: config,
});
}}
onCancel={() => setAutofixStep("diff-apply-custom")}
>
Octo uses tools to work with your underlying codebase. Some model providers don't support
strict constraints on how tool calls are generated, and models can make mistakes
generating JSON, the format used for all of Octo's tool calls.
The fix-json model can automatically fix broken JSON for Octo, helping models avoid
failures more quickly and cheaply than retrying the main model. It also may help reduce
the main model's confusion.
);
}
return (
Before we set up your main coding model, we can optionally enable two small helper models
that can significantly improve Octo's performance. These are small, fast models trained to
auto-fix broken tool calls and diff edits from your main coding model, since even fairly
good coding models can sometimes make mistakes.
Auto-fixing mistakes can help reduce model confusion, since models are often
less-well-trained on error recovery than they are at their happy paths.
);
}
function AutofixCompleteScreen({ onContinue }: { onContinue: () => void }) {
useKeyboard(event => {
if (event.key === "Enter") onContinue();
});
return (
Your autofix models are now set up and ready to go. These will help improve Octo's
performance by automatically fixing minor mistakes in code diffs and JSON tool calls.
Now let's set up your main coding model. This is the LLM that will power Octo's code
generation, analysis, and conversation capabilities.
Press enter to continue to model setup.
);
}
function WelcomeScreen({ onContinue }: { onContinue: () => void }) {
useKeyboard(event => {
if (event.key === "Enter") onContinue();
});
return (
You don't seem to have a config file, so let's set you up for the first time.
Octo lets you choose the LLM that powers it. Currently our recommended day-to-day coding
model to use with Octo is {recommendedModel("synthetic").nickname}, an open-source coding
model you can use via Synthetic, a privacy-focused inference company (that we run!). You
can also add closed-source models from OpenAI and Anthropic, like{" "}
{recommendedModel("openai").nickname} and {recommendedModel("anthropic").nickname}.
Be forewarned about using OpenRouter for open-source models: OpenRouter doesn't test model
implementations, and quality can vary drastically. Many are broken. We'd strongly
recommend using Synthetic instead.
You can add multiple models via Octo's menu: Octo lets you switch models mid-conversation
as needed to handle different problems. It's often helpful to add a couple of strong
models; if one gets stuck, another may often be able to solve your problem. Octo works
with any OpenAI- or Anthropic-compatible API.
Press enter when you're ready to begin setup.
);
}