import React, { useState, useEffect } from 'react'; import { Box, Text, useInput } from 'ink'; import { c } from './colors.js'; import { readEnvFile, writeEnvFile, describeEnvPath } from '../utils/env-file.js'; type Field = 'baseUrl' | 'apiKey' | 'modelId'; const FIELDS: { key: Field; label: string; placeholder: string; help: string; secret?: boolean }[] = [ { key: 'baseUrl', label: 'Base URL', placeholder: 'https://www.kob-ai.dev', help: 'Endpoint root — /v1, /v2 suffixes will be stripped automatically.', }, { key: 'apiKey', label: 'API Key', placeholder: 'kob_xxx:your_token (or just kob_xxx)', help: 'Format: kob_xxx:your_token, or just kob_xxx.', secret: true, }, { key: 'modelId', label: 'Default Model', placeholder: 'deepseek/deepseek-v4-flash', help: 'Optional. Used as the fallback model when none is specified.', }, ]; interface Props { onDone: (saved: boolean) => void; } export function ConfigForm({ onDone }: Props) { const envPath = describeEnvPath(); const initial = readEnvFile(); const [values, setValues] = useState>({ baseUrl: initial.KOB_API_BASE_URL || 'https://www.kob-ai.dev', apiKey: initial.KOB_API_KEY || '', modelId: initial.KOB_MODEL_ID || '', }); const [idx, setIdx] = useState(0); const [draft, setDraft] = useState(''); const [editing, setEditing] = useState(true); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); useEffect(() => { // When field changes, seed draft with the current value const f = FIELDS[idx]!; setDraft(values[f.key]); }, [idx]); useInput((input, key) => { // After saving we're just waiting for the user to acknowledge — // any key (Enter / Esc / printable) closes the overlay so focus // returns to the chat. Without this, the form would lock the // keyboard: InputBox is also disabled while configOpen is true. if (saved) { if (input || key.return || key.escape || key.backspace || key.tab) { onDone(true); } return; } if (!editing) return; if (key.escape) { onDone(false); return; } if (key.return) { // Commit draft, advance or save const f = FIELDS[idx]!; const next = { ...values, [f.key]: draft }; setValues(next); if (idx < FIELDS.length - 1) { setIdx(idx + 1); } else { // Save and exit try { const updates: Record = {}; const comments: Record = {}; if (next.baseUrl && next.baseUrl !== initial.KOB_API_BASE_URL) { updates.KOB_API_BASE_URL = next.baseUrl; comments.KOB_API_BASE_URL = 'Base URL for KOB AI API'; } if (next.apiKey && next.apiKey !== initial.KOB_API_KEY) { updates.KOB_API_KEY = next.apiKey; comments.KOB_API_KEY = 'Your API Key (format: kob_xxx:your_token or just kob_xxx)'; } if (next.modelId && next.modelId !== initial.KOB_MODEL_ID) { updates.KOB_MODEL_ID = next.modelId; comments.KOB_MODEL_ID = 'Default AI Model ID (optional)'; } if (Object.keys(updates).length > 0) { writeEnvFile(updates, comments); } setSaved(true); setEditing(false); } catch (e: any) { setError(e?.message || String(e)); } } return; } if (key.backspace || key.delete) { setDraft((d) => d.slice(0, -1)); return; } if (key.tab) { // Optional: skip optional empty fields const f = FIELDS[idx]!; const next = { ...values, [f.key]: draft }; setValues(next); setIdx((idx + 1) % FIELDS.length); return; } if (input && !key.ctrl && !key.meta) { setDraft((d) => d + input); } }); if (saved) { return ( ✓ Configuration saved File: {envPath} ↻ Restart KOB CLI to apply the new settings (env vars are loaded at startup). Press any key to return to the chat… ); } return ( ◆ /config edit {FIELDS[idx]!.label} ({idx + 1}/{FIELDS.length}) ↵ next/save · esc cancel {FIELDS.map((f, i) => { const isActive = i === idx; const showDraft = isActive ? draft : values[f.key]; const display = f.secret && showDraft ? '•'.repeat(Math.max(0, showDraft.length)) : (showDraft || f.placeholder); const displayColor = isActive ? (showDraft ? c.text : c.textDim) : (values[f.key] ? c.text : c.textDim); return ( {isActive ? '▶ ' : ' '} {f.label.padEnd(14)} : {display} {isActive && ▌} ); })} ↳ {FIELDS[idx]!.help} File: {envPath} {error && ( ✗ {error} )} ); }