import React, { useState, useCallback, useLayoutEffect } from "react"; import { Box, Text, useInput, useApp } from "ink"; import fs from "fs/promises"; import path from "path"; import json5 from "json5"; import TextInput from "./components/text-input.tsx"; import { Config } from "./config.ts"; import { useColor } from "./theme.ts"; 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 SelectInput from "./components/ink/select-input.tsx"; import { IndicatorComponent, ItemComponent } from "./components/select.tsx"; import { AutofixModelMenu } from "./components/autofix-model-menu.tsx"; import { SYNTHETIC_PROVIDER, keyFromName } from "./providers.ts"; import { CustomAuthFlow } from "./components/add-model-flow.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 }) { 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 = { 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 items = [ { label: "💫 Enable autofix models via Synthetic (recommended)", value: "synthetic", }, { label: "Use custom models...", value: "custom", }, { label: "Skip for now (can be enabled later)", value: "skip", }, ]; const onSelect = useCallback((item: (typeof items)[number]) => { 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(envVar) await onOverrideDefaultApiKey(envVar); onComplete({ diffApply: { baseUrl: SYNTHETIC_PROVIDER.baseUrl, model: "hf:syntheticlab/diff-apply", }, fixJson: { baseUrl: SYNTHETIC_PROVIDER.baseUrl, model: "hf:syntheticlab/fix-json", }, }); }} onCancel={() => setAutofixStep("choose")} baseUrl={SYNTHETIC_PROVIDER.baseUrl} /> } 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")} > LaissCodex 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 LaissCodex's tool calls. The fix-json model can automatically fix broken JSON for LaissCodex, 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 LaissCodex'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 }) { useInput((_, key) => { if(key.return) onContinue(); }); return Your autofix models are now set up and ready to go. These will help improve LaissCodex'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 LaissCodex's code generation, analysis, and conversation capabilities. Press enter to continue to model setup. } function WelcomeScreen({ onContinue }: { onContinue: () => void }) { useInput((_, key) => { if(key.return) onContinue(); }); return You don't seem to have a config file, so let's set you up for the first time. LaissCodex lets you choose the LLM that powers it. Currently our recommended day-to-day coding model to use with LaissCodex is either GPT-5, or Claude 4 Sonnet. If you want more data privacy, another great model is GLM-4.5, an open-source coding model you can use via Synthetic, a privacy-focused inference company (that we run!). 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 LaissCodex's menu: LaissCodex 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. LaissCodex works with any OpenAI- or Anthropic-compatible API. Press enter when you're ready to begin setup. }