diff --git a/.gitignore b/.gitignore index a70d77c..9b48d56 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ coverage .claude/worktrees/ .claude/ +CLAUDE.md +pnpm-workspace.yaml *.tgz diff --git a/src/chat/ui/InputBox.tsx b/src/chat/ui/InputBox.tsx index aeb266b..4e3a055 100644 --- a/src/chat/ui/InputBox.tsx +++ b/src/chat/ui/InputBox.tsx @@ -88,13 +88,6 @@ export function InputBox({ disabled, onSubmit, placeholder, onSuggestionsChange, if (rest) setValue(rest); return; } - if (key.return) { - const out = value; - setValue(''); - setCursor(0); - onSubmit(out); - return; - } if (key.tab && matches.length > 0) { const pick = matches[selected]; if (pick) { @@ -103,6 +96,22 @@ export function InputBox({ disabled, onSubmit, placeholder, onSuggestionsChange, } return; } + if (key.return) { + if (matches.length > 0) { + const pick = matches[selected]; + if (pick) { + setValue(''); + setCursor(0); + onSubmit(pick.name); + return; + } + } + const out = value; + setValue(''); + setCursor(0); + onSubmit(out); + return; + } if (key.upArrow && matches.length > 0) { setSelected((s) => (s - 1 + matches.length) % matches.length); return; diff --git a/src/chat/ui/driver.ts b/src/chat/ui/driver.ts index 4467f8c..aa2ffd1 100644 --- a/src/chat/ui/driver.ts +++ b/src/chat/ui/driver.ts @@ -7,6 +7,7 @@ import { stepCountIs, streamText } from 'ai'; import type { Provider } from '../../config.js'; +import { getActiveCredentials, loadConfig } from '../../config.js'; import { buildModel } from '../../model.js'; import type { KrawlerClient } from '../../krawler.js'; import { buildChatTools } from '../tools.js'; @@ -71,14 +72,24 @@ export async function runTurn( const shellTools = buildShellTools(hooks); const tools = { ...baseTools, ...settingsTools, ...memoryTools, ...shellTools }; + // Re-read config on every turn so that setModel/setProvider changes + // made mid-session (via settings tools) take effect immediately without + // requiring a REPL restart. + const liveConfig = loadConfig(); + const liveCreds = getActiveCredentials(liveConfig); + const liveProvider = liveConfig.provider; + const liveModel = liveConfig.model; + const liveApiKey = liveCreds.apiKey; + const liveOllamaBaseUrl = liveCreds.baseUrl ?? deps.ollamaBaseUrl; + let fullText = ''; try { const result = streamText({ model: buildModel({ - provider: deps.provider, - model: deps.modelName, - apiKey: deps.apiKey, - ollamaBaseUrl: deps.ollamaBaseUrl, + provider: liveProvider, + model: liveModel, + apiKey: liveApiKey, + ollamaBaseUrl: liveOllamaBaseUrl, }), system: deps.system, messages, diff --git a/src/personal.ts b/src/personal.ts index 1a3c284..76a294c 100644 --- a/src/personal.ts +++ b/src/personal.ts @@ -17,7 +17,7 @@ import { join } from 'node:path'; import { z } from 'zod'; -import { PROVIDERS } from './config.js'; +import { PROVIDERS, normalizeModelForProvider } from './config.js'; const PERSONAL_PATH = join(homedir(), '.config', 'krawler-agent', 'personal.json'); const PERSONAL_DIR = join(homedir(), '.config', 'krawler-agent', 'personal'); @@ -79,12 +79,17 @@ export function loadPersonalConfig(): PersonalConfig { try { const raw = JSON.parse(readFileSync(PERSONAL_PATH, 'utf8')); const parsed = personalSchema.safeParse(raw); - if (parsed.success) return parsed.data; - // Permissive fallback: merge unknown extras with defaults so old - // fields survive a schema rename. The user hand-editing the file - // shouldn't lose their custom name because we renamed "model" to - // "modelName" at some point. - return personalSchema.parse({ ...(raw ?? {}) }); + const data = parsed.success + ? parsed.data + : personalSchema.parse({ ...(raw ?? {}) }); + // Repair cross-provider orphan slugs (e.g. personal.json seeded with + // provider=ollama and model=claude-opus-4-7 before normalization ran). + const repaired = normalizeModelForProvider(data.provider, data.model); + if (repaired !== data.model) { + data.model = repaired; + try { savePersonalConfig(data); } catch { /* non-fatal */ } + } + return data; } catch { return personalSchema.parse({}); }