import React, { useCallback } from "react"; import { create } from "zustand"; import { useInput, useApp, Text } from "ink"; import { useShallow } from "zustand/react/shallow"; import { useAppStore } from "./state.ts"; import { useConfig, useSetConfig, Config } from "./config.ts"; import { MenuPanel } from "./components/menu-panel.tsx"; import { ModelSetup } from "./components/auto-detect-models.tsx"; import { AutofixModelMenu } from "./components/autofix-model-menu.tsx"; import { ConfirmDialog } from "./components/confirm-dialog.tsx"; import { SetApiKey } from "./components/set-api-key.tsx"; import { readKeyForModel } from "./config.ts"; import { keyFromName, SYNTHETIC_PROVIDER } from "./providers.ts"; type MenuMode = "main-menu" | "settings-menu" | "model-select" | "add-model" | "diff-apply-toggle" | "fix-json-toggle" | "set-default-model" | "quit-confirm" | "remove-model" ; type MenuState = { menuMode: MenuMode, setMenuMode: (mode: MenuMode) => void, }; const useMenuState = create((set, _) => ({ menuMode: "main-menu", setMenuMode: menuMode => { set({ menuMode }); }, })); export function Menu() { const { menuMode } = useMenuState(useShallow(state => ({ menuMode: state.menuMode, }))); if(menuMode === "main-menu") return if(menuMode === "settings-menu") return if(menuMode === "model-select") return if(menuMode === "set-default-model") return if(menuMode === "quit-confirm") return if(menuMode === "remove-model") return if(menuMode === "diff-apply-toggle") return if(menuMode === "fix-json-toggle") return const _: "add-model" = menuMode; return } function AutofixToggle({ configKey, modelNickname, disableNotification, enableNotification, defaultModel, children }: { disableNotification: string, enableNotification: string, defaultModel: string, modelNickname: string, configKey: "diffApply" | "fixJson", children: React.ReactNode, }) { const config = useConfig(); const setConfig = useSetConfig(); const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const { toggleMenu, notify } = useAppStore(useShallow(state => ({ toggleMenu: state.toggleMenu, notify: state.notify, }))); useInput((_, key) => { if(key.escape) setMenuMode("main-menu"); }); if(config[configKey]) { return { const newconf = { ...config }; delete newconf[configKey]; await setConfig(newconf); setMenuMode("main-menu"); toggleMenu(); notify(disableNotification); }} onConfirm={() => { setMenuMode("main-menu"); }} /> } return { await setConfig({ ...config, defaultApiKeyOverrides: { ...(config.defaultApiKeyOverrides || {}), [keyFromName(SYNTHETIC_PROVIDER.name)]: apiEnvVar, }, }); }} onComplete={async (setting) => { await setConfig({ ...config, [configKey]: setting, }); setMenuMode("main-menu"); toggleMenu(); notify(enableNotification); }} onCancel={() => { setMenuMode("main-menu"); }} > { children } } function DiffApplyToggle() { return 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. } function FixJsonToggle() { return 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. } function SwitchModelMenu() { const { setModelOverride, toggleMenu } = useAppStore(useShallow(state => ({ setModelOverride: state.setModelOverride, toggleMenu: state.toggleMenu, }))); const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const config = useConfig(); const [ pendingModel, setPendingModel ] = React.useState(null); useInput((_, key) => { if(key.escape && pendingModel == null) setMenuMode("main-menu"); }); if(pendingModel) { return { setModelOverride(pendingModel.nickname); setPendingModel(null); setMenuMode("main-menu"); toggleMenu(); }} onCancel={() => { setPendingModel(null); }} /> } const items = [ ...config.models.map(model => { return { label: model.nickname, value: `model-${model.nickname}`, }; }), { label: "Back to main menu", value: "back", }, ]; const onSelect = useCallback(async (item: (typeof items)[number]) => { if(item.value === "back") { setMenuMode("main-menu"); return; } const target = item.value.replace("model-", ""); const model = config.models.find(m => m.nickname === target)!; if(!model.apiEnvVar) { const key = await readKeyForModel(model, config); if(key == null) { setPendingModel(model); return; } } setModelOverride(target); setMenuMode("main-menu"); toggleMenu(); }, [ config ]); return } const SETTINGS_ITEMS = [ { label: "Change the default model", value: "set-default-model" as const, }, { label: "Remove a model", value: "remove-model" as const, }, { label: "Disable fast diff application", value: "disable-diff-apply" as const, }, { label: "Disable auto-fixing JSON tool calls", value: "disable-fix-json" as const, }, ]; function filterSettings(config: Config) { let items = SETTINGS_ITEMS.concat([]); items = items.filter(item => { if(config.diffApply == null && item.value === "disable-diff-apply") return false; if(config.fixJson == null && item.value === "disable-fix-json") return false; return true; }); if(config.models.length > 1) return items; return items.filter(item => { if(item.value === "remove-model") return false; if(item.value === "set-default-model") return false; return true; }); } function MainMenu() { const { toggleMenu } = useAppStore( useShallow(state => ({ toggleMenu: state.toggleMenu, })) ); const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const config = useConfig(); useInput((_, key) => { if(key.escape) toggleMenu(); }); let items = [ { label: "💫 Enable fast diff application", value: "diff-apply-toggle" as const, }, { label: "🪄 Enable auto-fixing JSON tool calls", value: "fix-json-toggle" as const, }, { label: "⤭ Switch model", value: "model-select" as const, }, { label: "+ Add a new model", value: "add-model" as const, }, { label: "* Settings", value: "settings-menu" as const, }, { label: "⟵ Return to LaissCodex", value: "return" as const, }, { label: "× Quit", value: "quit" as const, }, ]; items = items.filter(item => { if(config.diffApply != null && item.value === "diff-apply-toggle") return false; if(config.fixJson != null && item.value === "fix-json-toggle") return false; return true; }); const settingsItems = filterSettings(config); if(settingsItems.length === 0) { items = items.filter(item => item.value !== "settings-menu"); } const onSelect = useCallback((item: (typeof items)[number]) => { if(item.value === "return") toggleMenu(); else if(item.value === "quit") setMenuMode("quit-confirm"); else setMenuMode(item.value); }, []); return } function SettingsMenu() { const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const config = useConfig(); useInput((_, key) => { if(key.escape) setMenuMode("main-menu"); }); const settingsItems = filterSettings(config); let items = [ ...settingsItems, { label: "Back", value: "back" as const, }, ]; const onSelect = useCallback((item: (typeof items)[number]) => { if(item.value === "disable-diff-apply") setMenuMode("diff-apply-toggle"); else if(item.value === "disable-fix-json") setMenuMode("fix-json-toggle"); else if(item.value === "back") setMenuMode("main-menu"); else setMenuMode(item.value); }, []); return } function QuitConfirm() { const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const app = useApp(); useInput((_, key) => { if(key.escape) setMenuMode("main-menu"); }); const items = [ { label: "Never mind, take me back", value: "no" as const, }, { label: "Yes, quit", value: "yes" as const, } ]; const onSelect = useCallback((item: (typeof items)[number]) => { if(item.value === "no") setMenuMode("main-menu"); else app.exit(); }, []); return } function SetDefaultModelMenu() { const { setModelOverride, toggleMenu } = useAppStore(useShallow(state => ({ setModelOverride: state.setModelOverride, toggleMenu: state.toggleMenu, }))); const config = useConfig(); const setConfig = useSetConfig(); const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); useInput((_, key) => { if(key.escape) setMenuMode("main-menu"); }); const items = [ ...config.models.map(model => { return { label: model.nickname, value: `model-${model.nickname}`, }; }), { label: "Back to main menu", value: "back", }, ]; const onSelect = useCallback(async (item: (typeof items)[number]) => { if(item.value === "back") { setMenuMode("main-menu"); return; } const target = item.value.replace("model-", "") const model = config.models.find(m => m.nickname === target)!; const rest = config.models.filter(m => m.nickname !== target); await setConfig({ ...config, models: [ model, ...rest, ], }); setModelOverride(target); setMenuMode("main-menu"); toggleMenu(); }, [ config ]); return } function RemoveModelMenu() { const { setModelOverride, toggleMenu } = useAppStore(useShallow(state => ({ setModelOverride: state.setModelOverride, toggleMenu: state.toggleMenu, }))); const config = useConfig(); const setConfig = useSetConfig(); const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); useInput((_, key) => { if(key.escape) setMenuMode("main-menu"); }); const items = [ ...config.models.map(model => { return { label: model.nickname, value: `model-${model.nickname}`, }; }), { label: "Back to main menu", value: "back", }, ]; const onSelect = useCallback(async (item: (typeof items)[number]) => { if(item.value === "back") { setMenuMode("main-menu"); return; } const target = item.value.replace("model-", "") const rest = config.models.filter(m => m.nickname !== target); await setConfig({ ...config, models: [ ...rest, ], }); const current = rest[0]; setModelOverride(current.nickname); setMenuMode("main-menu"); toggleMenu(); }, [ config ]); return } function AddModelMenuFlow() { const { setMenuMode } = useMenuState(useShallow(state => ({ setMenuMode: state.setMenuMode, }))); const setConfig = useSetConfig(); const config = useConfig(); const onComplete = useCallback(async (models: Config["models"]) => { await setConfig({ ...config, models: [ ...config.models, ...models, ], }); setMenuMode("model-select"); }, [ config, setConfig ]); const onCancel = useCallback(() => { setMenuMode("main-menu"); }, [ setMenuMode ]); const onOverrideDefaultApiKey = useCallback(async (overrides: Record) => { await setConfig({ ...config, defaultApiKeyOverrides: { ...(config.defaultApiKeyOverrides || {}), ...overrides, }, }); }, [ config, setConfig ]); return }