osai-agent 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,1095 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput, useWindowSize } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { InputShell } from './InputShell.js';
5
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+
7
+ const MAIN_ACTIONS = [
8
+ { name: 'Select Provider', desc: 'Choose from the provider catalog', action: 'catalog' },
9
+ { name: 'Switch Provider', desc: 'Quick-switch between configured providers', action: 'switch_provider' },
10
+ { name: 'Current Provider', desc: 'Show active provider and model', action: 'show' },
11
+ { name: 'Switch Model', desc: 'Change model of a configured provider', action: 'switch_model' },
12
+ { name: 'Update API Key', desc: 'Change API key of a configured provider', action: 'update_key' },
13
+ { name: 'Reset to Default', desc: 'Switch back to OS AI Agent (auto)', action: 'reset' },
14
+ ];
15
+
16
+ const TITLE = ' Provider Management';
17
+
18
+ const LOCAL_PROVIDERS = [
19
+ { id: 'openai', name: 'OpenAI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
20
+ { id: 'anthropic', name: 'Anthropic', sdk: 'anthropic', needsKey: true, needsBaseUrl: false, freeTier: false },
21
+ { id: 'gemini', name: 'Google Gemini', sdk: 'gemini', needsKey: true, needsBaseUrl: false, freeTier: true },
22
+ { id: 'groq', name: 'Groq', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
23
+ { id: 'mistral', name: 'Mistral AI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
24
+ { id: 'deepseek', name: 'DeepSeek', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
25
+ { id: 'xai', name: 'xAI (Grok)', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
26
+ { id: 'cohere', name: 'Cohere', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
27
+ { id: 'perplexity', name: 'Perplexity', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
28
+ { id: 'together', name: 'Together AI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
29
+ { id: 'fireworks', name: 'Fireworks AI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
30
+ { id: 'cerebras', name: 'Cerebras', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
31
+ { id: 'openrouter', name: 'OpenRouter', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
32
+ { id: 'huggingface', name: 'Hugging Face', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
33
+ { id: 'github', name: 'GitHub Models', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
34
+ { id: 'siliconflow', name: 'SiliconFlow', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
35
+ { id: 'hyperbolic', name: 'Hyperbolic', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
36
+ { id: 'novita', name: 'Novita AI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
37
+ { id: 'deepinfra', name: 'DeepInfra', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
38
+ { id: 'codestral', name: 'Codestral', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
39
+ { id: 'qwen', name: 'Alibaba Qwen', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
40
+ { id: 'moonshot', name: 'Moonshot (Kimi)', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
41
+ { id: 'zhipu', name: 'Zhipu AI (GLM)', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
42
+ { id: 'yi', name: '01.AI (Yi)', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
43
+ { id: 'baidu', name: 'Baidu ERNIE', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
44
+ { id: 'nvidia', name: 'NVIDIA NIM', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
45
+ { id: 'nebius', name: 'Nebius AI', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: false },
46
+ { id: 'custom', name: 'Custom Provider', sdk: 'openai', needsKey: true, needsBaseUrl: true, freeTier: false },
47
+ { id: 'azure', name: 'Azure OpenAI', sdk: 'openai', needsKey: true, needsBaseUrl: true, freeTier: false },
48
+ { id: 'cloudflare', name: 'Cloudflare AI', sdk: 'openai', needsKey: true, needsBaseUrl: true, freeTier: true },
49
+ { id: 'bedrock', name: 'AWS Bedrock', sdk: 'openai', needsKey: true, needsBaseUrl: true, freeTier: false },
50
+ { id: 'vertex', name: 'Vertex AI', sdk: 'openai', needsKey: true, needsBaseUrl: true, freeTier: false },
51
+ { id: 'ollama', name: 'Ollama (Local)', sdk: 'openai', needsKey: false, needsBaseUrl: false, freeTier: true },
52
+ { id: 'ollama-cloud',name: 'Ollama Cloud', sdk: 'openai', needsKey: true, needsBaseUrl: false, freeTier: true },
53
+ { id: 'lmstudio', name: 'LM Studio', sdk: 'openai', needsKey: false, needsBaseUrl: false, freeTier: true },
54
+ { id: 'vllm', name: 'vLLM (Self-host)',sdk: 'openai', needsKey: false, needsBaseUrl: true, freeTier: true },
55
+ ];
56
+
57
+ export function ProviderMenu({ visible, onSelect, onCancel, serverUrl, token, currentProvider, isLocal = false }) {
58
+ const [phase, setPhase] = useState('menu');
59
+ const [menuCursor, setMenuCursor] = useState(0);
60
+ const [menuQuery, setMenuQuery] = useState('');
61
+
62
+ const [catalog, setCatalog] = useState([]);
63
+ const [catalogLoading, setCatalogLoading] = useState(false);
64
+ const [catalogError, setCatalogError] = useState(null);
65
+ const [catalogCursor, setCatalogCursor] = useState(0);
66
+ const [catalogQuery, setCatalogQuery] = useState('');
67
+
68
+ const [selectedProvider, setSelectedProvider] = useState(null);
69
+ const [modelsData, setModelsData] = useState(null);
70
+ const [modelsLoading, setModelsLoading] = useState(false);
71
+ const [modelsCursor, setModelsCursor] = useState(0);
72
+ const [modelsQuery, setModelsQuery] = useState('');
73
+
74
+ const [apiKeyInput, setApiKeyInput] = useState('');
75
+ const [selectedModel, setSelectedModel] = useState(null);
76
+ const [settingUp, setSettingUp] = useState(false);
77
+ const [settingError, setSettingError] = useState(null);
78
+
79
+ // Configured providers list (for Switch Model / Update API Key / Switch Provider)
80
+ const [configuredProviders, setConfiguredProviders] = useState([]);
81
+ const [configuredLoading, setConfiguredLoading] = useState(false);
82
+ const [configuredError, setConfiguredError] = useState(null);
83
+ const [configuredCursor, setConfiguredCursor] = useState(0);
84
+ const [configuredQuery, setConfiguredQuery] = useState('');
85
+ const [actionType, setActionType] = useState(null);
86
+
87
+ // Local mode: base URL input (for providers like vllm, custom, azure, etc.)
88
+ const [localBaseUrlInput, setLocalBaseUrlInput] = useState('');
89
+
90
+ // Filtered lists
91
+ const filteredMenu = useMemo(() => {
92
+ const actions = isLocal
93
+ ? MAIN_ACTIONS.filter(a => a.action !== 'reset')
94
+ : MAIN_ACTIONS;
95
+ if (!menuQuery) return actions;
96
+ const q = menuQuery.toLowerCase();
97
+ return actions.filter(o => o.name.toLowerCase().includes(q) || o.desc.toLowerCase().includes(q));
98
+ }, [menuQuery, isLocal]);
99
+
100
+ const filteredCatalog = useMemo(() => {
101
+ if (!catalogQuery) return catalog;
102
+ const q = catalogQuery.toLowerCase();
103
+ return catalog.filter(p => p.name.toLowerCase().includes(q) || p.id.toLowerCase().includes(q) || (p.sdk_type || '').toLowerCase().includes(q));
104
+ }, [catalog, catalogQuery]);
105
+
106
+ const filteredModels = useMemo(() => {
107
+ if (!modelsData?.models) return [];
108
+ const models = modelsData.models.map(m => typeof m === 'string' ? m : (m.id || m.name || m));
109
+ if (!modelsQuery) return models;
110
+ const q = modelsQuery.toLowerCase();
111
+ return models.filter(m => m.toLowerCase().includes(q));
112
+ }, [modelsData, modelsQuery]);
113
+
114
+ const allModels = useMemo(
115
+ () => ['__default__ (auto)', ...filteredModels],
116
+ [filteredModels]
117
+ );
118
+
119
+ const resolveModelAtCursor = (cursor) => {
120
+ const choice = allModels[cursor];
121
+ if (!choice || choice === '__default__ (auto)') return '__default__';
122
+ return choice;
123
+ };
124
+
125
+ const filteredConfigured = useMemo(() => {
126
+ if (!configuredQuery) return configuredProviders;
127
+ const q = configuredQuery.toLowerCase();
128
+ return configuredProviders.filter(p => p.name.toLowerCase().includes(q) || p.type.toLowerCase().includes(q));
129
+ }, [configuredProviders, configuredQuery]);
130
+
131
+ // Clamp cursors
132
+ useEffect(() => {
133
+ if (menuCursor >= filteredMenu.length) setMenuCursor(Math.max(0, filteredMenu.length - 1));
134
+ }, [filteredMenu.length]);
135
+ useEffect(() => {
136
+ if (catalogCursor >= filteredCatalog.length) setCatalogCursor(Math.max(0, filteredCatalog.length - 1));
137
+ }, [filteredCatalog.length]);
138
+ useEffect(() => {
139
+ if (modelsCursor >= allModels.length) setModelsCursor(Math.max(0, allModels.length - 1));
140
+ }, [allModels.length]);
141
+ useEffect(() => {
142
+ if (configuredCursor >= filteredConfigured.length) setConfiguredCursor(Math.max(0, filteredConfigured.length - 1));
143
+ }, [filteredConfigured.length]);
144
+
145
+ // Fetch catalog
146
+ const fetchCatalog = () => {
147
+ setCatalogLoading(true);
148
+ setCatalogError(null);
149
+ (async () => {
150
+ try {
151
+ if (isLocal) {
152
+ const data = LOCAL_PROVIDERS.map(p => ({
153
+ id: p.id,
154
+ name: p.name,
155
+ sdk_type: p.sdk,
156
+ free_tier: p.freeTier,
157
+ models_count: null,
158
+ active: false,
159
+ base_url: null,
160
+ needs_key: p.needsKey,
161
+ needs_base_url: p.needsBaseUrl,
162
+ }));
163
+ setCatalog(data);
164
+ setPhase('catalog');
165
+ } else {
166
+ const res = await fetch(`${serverUrl}/api/provider/catalog`, {
167
+ headers: { Authorization: `Bearer ${token}` }
168
+ });
169
+ if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
170
+ const data = await res.json();
171
+ setCatalog(data);
172
+ setPhase('catalog');
173
+ }
174
+ } catch (err) {
175
+ setCatalogError(err.message);
176
+ setPhase('catalog');
177
+ } finally {
178
+ setCatalogLoading(false);
179
+ }
180
+ })();
181
+ };
182
+
183
+ // Fetch configured providers
184
+ const fetchConfiguredProviders = (action) => {
185
+ setActionType(action);
186
+ setConfiguredLoading(true);
187
+ setConfiguredError(null);
188
+ setConfiguredProviders([]);
189
+ (async () => {
190
+ try {
191
+ if (isLocal) {
192
+ const { getAllLocalProviders } = await import('../../commands/provider.js');
193
+ const providers = getAllLocalProviders();
194
+ const mapped = providers.map(p => ({
195
+ name: p.type,
196
+ type: p.type,
197
+ model: p.model,
198
+ baseUrl: p.baseUrl,
199
+ apiKey: p.apiKey,
200
+ key_masked: p.apiKey ? p.apiKey.slice(0, 3) + '...' + p.apiKey.slice(-4) : 'N/A',
201
+ active: p.active,
202
+ }));
203
+ const keylessProviders = ['ollama', 'lmstudio', 'vllm'];
204
+ const filtered = action === 'update_key'
205
+ ? mapped.filter(p => !keylessProviders.includes(p.type) && p.key_masked !== 'N/A')
206
+ : mapped;
207
+ setConfiguredProviders(filtered);
208
+ setPhase('configured');
209
+ } else {
210
+ const res = await fetch(`${serverUrl}/api/provider/keys`, {
211
+ headers: { Authorization: `Bearer ${token}` }
212
+ });
213
+ if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
214
+ const data = await res.json();
215
+ const keylessProviders = ['osai', 'ollama', 'lmstudio', 'vllm'];
216
+ const filtered = action === 'update_key'
217
+ ? data.filter(p => !keylessProviders.includes(p.type) && p.key_masked)
218
+ : data;
219
+ setConfiguredProviders(filtered);
220
+ setPhase('configured');
221
+ }
222
+ } catch (err) {
223
+ setConfiguredError(err.message);
224
+ setPhase('configured');
225
+ } finally {
226
+ setConfiguredLoading(false);
227
+ }
228
+ })();
229
+ };
230
+
231
+ // Fetch models for a provider (server mode)
232
+ const fetchModels = (provider, nextPhase) => {
233
+ setSelectedProvider(provider);
234
+ setModelsLoading(true);
235
+ setModelsData(null);
236
+ setModelsCursor(0);
237
+ setModelsQuery('');
238
+ setSelectedModel(null);
239
+ (async () => {
240
+ try {
241
+ const providerId = provider.type || provider.id;
242
+ const res = await fetch(`${serverUrl}/api/provider/models/${providerId}`, {
243
+ headers: { Authorization: `Bearer ${token}` },
244
+ });
245
+ if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
246
+ const data = await res.json();
247
+ setModelsData(data);
248
+ setPhase(nextPhase || 'models');
249
+ } catch (err) {
250
+ setModelsData({ provider: provider.type || provider.id, models: [], source: 'error', warning: err.message });
251
+ setPhase(nextPhase || 'models');
252
+ } finally {
253
+ setModelsLoading(false);
254
+ }
255
+ })();
256
+ };
257
+
258
+ // Fetch models for a provider (local mode)
259
+ const fetchModelsLocal = async (provider, apiKey, baseUrl) => {
260
+ setSelectedProvider(provider);
261
+ setModelsLoading(true);
262
+ setModelsData(null);
263
+ setModelsCursor(0);
264
+ setModelsQuery('');
265
+ setPhase('models');
266
+ try {
267
+ const { fetchLocalModels } = await import('../../llm/direct.js');
268
+ const providerId = provider.type || provider.id;
269
+ const models = await fetchLocalModels({ type: providerId, apiKey, baseUrl });
270
+ setModelsData({ provider: provider.name || providerId, models, source: 'live' });
271
+ } catch (err) {
272
+ setModelsData({ provider: provider.name || provider.type || 'provider', models: [], source: 'error', warning: err.message });
273
+ } finally {
274
+ setModelsLoading(false);
275
+ }
276
+ };
277
+
278
+ // Set provider on server (full setup, server mode)
279
+ const setProvider = async (provider, model, apiKey) => {
280
+ setSettingUp(true);
281
+ setSettingError(null);
282
+ try {
283
+ const body = { type: provider.id };
284
+ if (model && model !== '__default__' && model !== '__default__ (auto)') body.model = model;
285
+ if (apiKey) body.api_key = apiKey;
286
+ const res = await fetch(`${serverUrl}/api/provider`, {
287
+ method: 'PUT',
288
+ headers: {
289
+ Authorization: `Bearer ${token}`,
290
+ 'Content-Type': 'application/json'
291
+ },
292
+ body: JSON.stringify(body)
293
+ });
294
+ const data = await res.json();
295
+ if (!res.ok) {
296
+ setSettingError(data.error || 'Failed to set provider');
297
+ setSettingUp(false);
298
+ return;
299
+ }
300
+ const modelDisplay = data.model ? ` | Model: ${data.model}` : '';
301
+ const keyDisplay = data.key ? ` | Key: ${data.key}` : '';
302
+ onSelect({
303
+ action: 'provider_set',
304
+ content: `**Provider set to ${data.type}**${modelDisplay}${keyDisplay}`,
305
+ providerType: data.type,
306
+ providerModel: data.model
307
+ });
308
+ } catch (err) {
309
+ setSettingError(err.message);
310
+ } finally {
311
+ setSettingUp(false);
312
+ }
313
+ };
314
+
315
+ // Patch model only (Switch Model flow, server mode)
316
+ const patchModel = async (provider, model) => {
317
+ setSettingUp(true);
318
+ setSettingError(null);
319
+ try {
320
+ const res = await fetch(`${serverUrl}/api/provider/model`, {
321
+ method: 'PATCH',
322
+ headers: {
323
+ Authorization: `Bearer ${token}`,
324
+ 'Content-Type': 'application/json'
325
+ },
326
+ body: JSON.stringify({ type: provider.type || provider.id, model })
327
+ });
328
+ const data = await res.json();
329
+ if (!res.ok) {
330
+ setSettingError(data.error || 'Failed to update model');
331
+ setSettingUp(false);
332
+ return;
333
+ }
334
+ onSelect({
335
+ action: 'model_updated',
336
+ content: `**Model changed to ${data.model} for ${data.type}**`,
337
+ providerType: data.type,
338
+ providerModel: data.model
339
+ });
340
+ } catch (err) {
341
+ setSettingError(err.message);
342
+ } finally {
343
+ setSettingUp(false);
344
+ }
345
+ };
346
+
347
+ // Patch API key only (Update API Key flow, server mode)
348
+ const patchKey = async (type, newKey) => {
349
+ setSettingUp(true);
350
+ setSettingError(null);
351
+ try {
352
+ const res = await fetch(`${serverUrl}/api/provider/key`, {
353
+ method: 'PATCH',
354
+ headers: {
355
+ Authorization: `Bearer ${token}`,
356
+ 'Content-Type': 'application/json'
357
+ },
358
+ body: JSON.stringify({ type, api_key: newKey })
359
+ });
360
+ const data = await res.json();
361
+ if (!res.ok) {
362
+ setSettingError(data.error || 'Failed to update API key');
363
+ setSettingUp(false);
364
+ return;
365
+ }
366
+ onSelect({
367
+ action: 'key_updated',
368
+ content: `**API key updated for ${data.type}** | Key: ${data.key}`,
369
+ providerType: data.type
370
+ });
371
+ } catch (err) {
372
+ setSettingError(err.message);
373
+ } finally {
374
+ setSettingUp(false);
375
+ }
376
+ };
377
+
378
+ // Handle model selection (Enter on a model)
379
+ const handleModelSelect = async (model) => {
380
+ const modelVal = model === '__default__' || model === '__default__ (auto)' ? null : model;
381
+ if (isLocal) {
382
+ const { saveLocalProvider } = await import('../../commands/provider.js');
383
+ try {
384
+ saveLocalProvider(
385
+ selectedProvider.id || selectedProvider.type,
386
+ apiKeyInput.trim() || null,
387
+ modelVal,
388
+ localBaseUrlInput.trim() || null
389
+ );
390
+ } catch {}
391
+ onSelect({ action: 'set_local', providerType: selectedProvider.id || selectedProvider.type, model: modelVal });
392
+ resetAll();
393
+ } else {
394
+ setSelectedModel(model);
395
+ const keylessProviders = ['osai', 'ollama', 'lmstudio', 'vllm'];
396
+ const needsKey = !keylessProviders.includes(selectedProvider.id) && selectedProvider.requires_key !== false;
397
+ if (needsKey) {
398
+ setApiKeyInput('');
399
+ setPhase('api_key');
400
+ } else {
401
+ setProvider(selectedProvider, modelVal, null);
402
+ }
403
+ }
404
+ };
405
+
406
+ // Handle model selection in switch_model flow
407
+ const handleSwitchModelSelect = (model) => {
408
+ const modelVal = model === '__default__' || model === '__default__ (auto)' ? null : model;
409
+ if (isLocal) {
410
+ (async () => {
411
+ const { saveLocalProvider } = await import('../../commands/provider.js');
412
+ try {
413
+ saveLocalProvider(selectedProvider.type, selectedProvider.apiKey || null, modelVal, selectedProvider.baseUrl || null);
414
+ } catch {}
415
+ onSelect({ action: 'set_local', providerType: selectedProvider.type, model: modelVal });
416
+ resetAll();
417
+ })();
418
+ } else {
419
+ patchModel(selectedProvider, modelVal);
420
+ }
421
+ };
422
+
423
+ // Quick switch to a configured provider (server mode)
424
+ const switchProvider = async (provider) => {
425
+ setSettingUp(true);
426
+ setSettingError(null);
427
+ try {
428
+ const res = await fetch(`${serverUrl}/api/provider/switch`, {
429
+ method: 'POST',
430
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
431
+ body: JSON.stringify({ type: provider.type || provider.id })
432
+ });
433
+ const data = await res.json();
434
+ if (!res.ok) {
435
+ setSettingError(data.error || 'Failed to switch provider');
436
+ setSettingUp(false);
437
+ return;
438
+ }
439
+ const modelDisplay = data.model ? ` | Model: ${data.model}` : '';
440
+ const keyDisplay = data.key ? ` | Key: ${data.key}` : '';
441
+ onSelect({
442
+ action: 'provider_set',
443
+ content: `**Switched to ${data.type}**${modelDisplay}${keyDisplay}`,
444
+ providerType: data.type,
445
+ providerModel: data.model
446
+ });
447
+ } catch (err) {
448
+ setSettingError(err.message);
449
+ setSettingUp(false);
450
+ }
451
+ };
452
+
453
+ // Handle configured provider select
454
+ const handleConfiguredSelect = (provider) => {
455
+ if (actionType === 'switch_provider') {
456
+ if (isLocal) {
457
+ (async () => {
458
+ const { saveLocalProvider } = await import('../../commands/provider.js');
459
+ try {
460
+ saveLocalProvider(provider.type, null, null, null);
461
+ } catch {}
462
+ onSelect({ action: 'set_local', providerType: provider.type, model: provider.model || null });
463
+ resetAll();
464
+ })();
465
+ } else {
466
+ switchProvider(provider);
467
+ }
468
+ } else if (actionType === 'switch_model') {
469
+ if (isLocal) {
470
+ fetchModelsLocal(provider, provider.apiKey || null, provider.baseUrl || null);
471
+ } else {
472
+ fetchModels(provider, 'switch_models');
473
+ }
474
+ } else if (actionType === 'update_key') {
475
+ setSelectedProvider(provider);
476
+ setApiKeyInput('');
477
+ setSettingError(null);
478
+ setPhase('update_key_input');
479
+ }
480
+ };
481
+
482
+ // Handle update key submission
483
+ const handleUpdateKeySubmit = () => {
484
+ const key = apiKeyInput.trim();
485
+ if (!key) return;
486
+ if (isLocal) {
487
+ (async () => {
488
+ const { saveLocalProvider } = await import('../../commands/provider.js');
489
+ try {
490
+ saveLocalProvider(selectedProvider.type, key, null, null);
491
+ } catch {}
492
+ onSelect({ action: 'set_local', providerType: selectedProvider.type, model: selectedProvider.model || null });
493
+ resetAll();
494
+ })();
495
+ } else {
496
+ patchKey(selectedProvider.type, key);
497
+ }
498
+ };
499
+
500
+ // Handle API key submission (models phase -> key input)
501
+ const handleApiKeySubmit = () => {
502
+ const key = apiKeyInput.trim();
503
+ if (!key) { setSettingError('API key is required'); return; }
504
+ if (isLocal) {
505
+ const baseUrl = localBaseUrlInput.trim() || null;
506
+ fetchModelsLocal(selectedProvider, key, baseUrl);
507
+ } else {
508
+ setProvider(selectedProvider, selectedModel, key);
509
+ }
510
+ };
511
+
512
+ // Handle local API key submission from catalog (enter key, then fetch models)
513
+ const handleLocalApiKeySubmit = () => {
514
+ const key = apiKeyInput.trim();
515
+ if (!key) return;
516
+ const baseUrl = localBaseUrlInput.trim() || null;
517
+ fetchModelsLocal(selectedProvider, key, baseUrl);
518
+ };
519
+
520
+ const resetAll = () => {
521
+ setPhase('menu');
522
+ setMenuCursor(0);
523
+ setMenuQuery('');
524
+ setCatalogCursor(0);
525
+ setCatalogQuery('');
526
+ setModelsCursor(0);
527
+ setModelsQuery('');
528
+ setSelectedProvider(null);
529
+ setModelsData(null);
530
+ setApiKeyInput('');
531
+ setSelectedModel(null);
532
+ setSettingUp(false);
533
+ setSettingError(null);
534
+ setConfiguredProviders([]);
535
+ setConfiguredLoading(false);
536
+ setConfiguredError(null);
537
+ setConfiguredCursor(0);
538
+ setConfiguredQuery('');
539
+ setActionType(null);
540
+ setLocalBaseUrlInput('');
541
+ };
542
+
543
+ // ── KEYBOARD HANDLING ──
544
+ useInput((input, key) => {
545
+ if (!visible) return;
546
+
547
+ // === MENU PHASE ===
548
+ if (phase === 'menu') {
549
+ if (key.escape) { resetAll(); onCancel(); return; }
550
+ if (key.upArrow) { setMenuCursor(c => (c > 0 ? c - 1 : filteredMenu.length - 1)); return; }
551
+ if (key.downArrow) { setMenuCursor(c => (c < filteredMenu.length - 1 ? c + 1 : 0)); return; }
552
+ if (key.return) {
553
+ if (filteredMenu.length === 0) { resetAll(); onCancel(); return; }
554
+ const action = filteredMenu[menuCursor].action;
555
+ if (action === 'catalog') { fetchCatalog(); return; }
556
+ if (action === 'show') {
557
+ onSelect({ action: isLocal ? 'show_local' : 'show' });
558
+ resetAll();
559
+ return;
560
+ }
561
+ if (action === 'switch_provider') { fetchConfiguredProviders('switch_provider'); return; }
562
+ if (action === 'switch_model') { fetchConfiguredProviders('switch_model'); return; }
563
+ if (action === 'update_key') { fetchConfiguredProviders('update_key'); return; }
564
+ if (action === 'reset') {
565
+ onSelect({ action: isLocal ? 'reset_local' : 'reset' });
566
+ resetAll();
567
+ return;
568
+ }
569
+ return;
570
+ }
571
+ if (key.backspace || key.delete) { setMenuQuery(q => q.slice(0, -1)); setMenuCursor(0); return; }
572
+ if (isOnlySgrMouseInput(input)) return;
573
+ if (input && !key.ctrl && !key.meta) { setMenuQuery(q => q + input); setMenuCursor(0); }
574
+ return;
575
+ }
576
+
577
+ // === CATALOG PHASE ===
578
+ if (phase === 'catalog') {
579
+ if (key.escape) { setPhase('menu'); setCatalogQuery(''); setCatalogCursor(0); return; }
580
+ if (key.upArrow) { setCatalogCursor(c => (c > 0 ? c - 1 : filteredCatalog.length - 1)); return; }
581
+ if (key.downArrow) { setCatalogCursor(c => (c < filteredCatalog.length - 1 ? c + 1 : 0)); return; }
582
+ if (key.return) {
583
+ if (filteredCatalog.length > 0) {
584
+ const prov = filteredCatalog[catalogCursor];
585
+ if (isLocal) {
586
+ if (prov.needs_base_url) {
587
+ setSelectedProvider(prov);
588
+ setLocalBaseUrlInput('');
589
+ setPhase('base_url');
590
+ } else if (prov.needs_key) {
591
+ setSelectedProvider(prov);
592
+ setApiKeyInput('');
593
+ setSettingError(null);
594
+ setPhase('api_key');
595
+ } else {
596
+ fetchModelsLocal(prov, null, null);
597
+ }
598
+ } else {
599
+ if (prov.id === 'osai') {
600
+ setProvider(prov, '__default__', null);
601
+ } else {
602
+ fetchModels(prov, 'models');
603
+ }
604
+ }
605
+ }
606
+ return;
607
+ }
608
+ if (key.backspace || key.delete) { setCatalogQuery(q => q.slice(0, -1)); setCatalogCursor(0); return; }
609
+ if (isOnlySgrMouseInput(input)) return;
610
+ if (input && !key.ctrl && !key.meta) { setCatalogQuery(q => q + input); setCatalogCursor(0); }
611
+ return;
612
+ }
613
+
614
+ // === BASE URL PHASE (local mode only) ===
615
+ if (phase === 'base_url') {
616
+ if (key.escape) { setPhase('catalog'); setLocalBaseUrlInput(''); setApiKeyInput(''); return; }
617
+ if (key.return) {
618
+ const prov = selectedProvider;
619
+ if (prov?.needs_key) {
620
+ setPhase('api_key');
621
+ setApiKeyInput('');
622
+ setSettingError(null);
623
+ } else {
624
+ fetchModelsLocal(prov, null, localBaseUrlInput.trim() || null);
625
+ }
626
+ return;
627
+ }
628
+ if (key.backspace || key.delete) { setLocalBaseUrlInput(t => t.slice(0, -1)); return; }
629
+ if (isOnlySgrMouseInput(input)) return;
630
+ if (input && !key.ctrl && !key.meta) { setLocalBaseUrlInput(t => t + input); }
631
+ return;
632
+ }
633
+
634
+ // === CONFIGURED PHASE ===
635
+ if (phase === 'configured') {
636
+ if (key.escape) { resetAll(); return; }
637
+ if (key.upArrow) { setConfiguredCursor(c => (c > 0 ? c - 1 : filteredConfigured.length - 1)); return; }
638
+ if (key.downArrow) { setConfiguredCursor(c => (c < filteredConfigured.length - 1 ? c + 1 : 0)); return; }
639
+ if (key.return) {
640
+ if (filteredConfigured.length > 0) {
641
+ handleConfiguredSelect(filteredConfigured[configuredCursor]);
642
+ }
643
+ return;
644
+ }
645
+ if (key.backspace || key.delete) { setConfiguredQuery(q => q.slice(0, -1)); setConfiguredCursor(0); return; }
646
+ if (isOnlySgrMouseInput(input)) return;
647
+ if (input && !key.ctrl && !key.meta) { setConfiguredQuery(q => q + input); setConfiguredCursor(0); }
648
+ return;
649
+ }
650
+
651
+ // === MODELS PHASE (full setup) ===
652
+ if (phase === 'models') {
653
+ if (modelsLoading) {
654
+ if (key.escape) { setPhase('catalog'); setModelsQuery(''); setModelsCursor(0); setSelectedProvider(null); setModelsData(null); return; }
655
+ return;
656
+ }
657
+ if (key.escape) { setPhase('catalog'); setModelsQuery(''); setModelsCursor(0); setSelectedProvider(null); return; }
658
+ if (key.upArrow) { setModelsCursor(c => (c > 0 ? c - 1 : allModels.length - 1)); return; }
659
+ if (key.downArrow) { setModelsCursor(c => (c < allModels.length - 1 ? c + 1 : 0)); return; }
660
+ if (key.return) {
661
+ if (allModels.length > 0) {
662
+ handleModelSelect(resolveModelAtCursor(modelsCursor));
663
+ }
664
+ return;
665
+ }
666
+ if (key.backspace || key.delete) { setModelsQuery(q => q.slice(0, -1)); setModelsCursor(0); return; }
667
+ if (isOnlySgrMouseInput(input)) return;
668
+ if (input && !key.ctrl && !key.meta) { setModelsQuery(q => q + input); setModelsCursor(0); }
669
+ return;
670
+ }
671
+
672
+ // === SWITCH MODELS PHASE (model selection for switch_model flow) ===
673
+ if (phase === 'switch_models') {
674
+ if (key.escape) { setPhase('configured'); setModelsQuery(''); setModelsCursor(0); setSelectedProvider(null); return; }
675
+ if (key.upArrow) { setModelsCursor(c => (c > 0 ? c - 1 : allModels.length - 1)); return; }
676
+ if (key.downArrow) { setModelsCursor(c => (c < allModels.length - 1 ? c + 1 : 0)); return; }
677
+ if (key.return) {
678
+ if (allModels.length > 0) {
679
+ handleSwitchModelSelect(resolveModelAtCursor(modelsCursor));
680
+ }
681
+ return;
682
+ }
683
+ if (key.backspace || key.delete) { setModelsQuery(q => q.slice(0, -1)); setModelsCursor(0); return; }
684
+ if (isOnlySgrMouseInput(input)) return;
685
+ if (input && !key.ctrl && !key.meta) { setModelsQuery(q => q + input); setModelsCursor(0); }
686
+ return;
687
+ }
688
+
689
+ // === API KEY PHASE (full setup: server mode after model select, local mode before model fetch) ===
690
+ if (phase === 'api_key') {
691
+ if (key.escape) {
692
+ if (isLocal) {
693
+ if (selectedProvider?.needs_base_url) {
694
+ setPhase('base_url');
695
+ } else {
696
+ setPhase('catalog');
697
+ }
698
+ } else {
699
+ setPhase('models');
700
+ }
701
+ setApiKeyInput('');
702
+ setSettingError(null);
703
+ return;
704
+ }
705
+ if (key.return) {
706
+ if (isLocal) {
707
+ handleLocalApiKeySubmit();
708
+ } else {
709
+ handleApiKeySubmit();
710
+ }
711
+ return;
712
+ }
713
+ if (key.backspace || key.delete) { setApiKeyInput(t => t.slice(0, -1)); return; }
714
+ if (isOnlySgrMouseInput(input)) return;
715
+ if (input && !key.ctrl && !key.meta) { setApiKeyInput(t => t + input); }
716
+ return;
717
+ }
718
+
719
+ // === UPDATE KEY INPUT PHASE ===
720
+ if (phase === 'update_key_input') {
721
+ if (key.escape) { setPhase('configured'); setApiKeyInput(''); setSettingError(null); return; }
722
+ if (key.return) { handleUpdateKeySubmit(); return; }
723
+ if (key.backspace || key.delete) { setApiKeyInput(t => t.slice(0, -1)); return; }
724
+ if (isOnlySgrMouseInput(input)) return;
725
+ if (input && !key.ctrl && !key.meta) { setApiKeyInput(t => t + input); }
726
+ return;
727
+ }
728
+ });
729
+
730
+ if (!visible) return null;
731
+
732
+ const separator = '─'.repeat(52);
733
+ const { rows } = useWindowSize();
734
+
735
+ // === SETTING UP VIEW ===
736
+ if (settingUp) {
737
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
738
+ h(Text, { color: '#7aa2f7', bold: true }, TITLE),
739
+ h(Text, { color: '#3b3f52' }, separator),
740
+ h(Box, { marginTop: 1, paddingLeft: 2 },
741
+ h(Text, { color: '#e0af68' }, `${actionType === 'switch_model' ? 'Updating model' : actionType === 'update_key' ? 'Updating API key' : 'Setting up'} ${selectedProvider?.name || selectedProvider?.type || 'provider'}...`)
742
+ ),
743
+ );
744
+ }
745
+
746
+ // === BASE URL INPUT (local mode) ===
747
+ if (phase === 'base_url') {
748
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
749
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
750
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${selectedProvider?.name || 'Provider'} - Base URL`),
751
+ ),
752
+ h(Text, { color: '#3b3f52' }, separator),
753
+ h(Box, { marginTop: 1, paddingLeft: 2 },
754
+ h(Text, { color: '#e0af68' }, 'Enter API endpoint URL:')
755
+ ),
756
+ h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
757
+ h(Text, { color: '#7aa2f7' }, ' > '),
758
+ h(Text, { color: '#c0caf5' }, localBaseUrlInput || 'https://'),
759
+ h(Text, { color: '#565f89' }, !localBaseUrlInput ? ' Enter your endpoint URL' : '')
760
+ ),
761
+ h(Text, { color: '#3b3f52' }, separator),
762
+ h(Box, { flexDirection: 'row' },
763
+ h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Confirm'),
764
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
765
+ ),
766
+ );
767
+ }
768
+
769
+ // === API KEY VIEW ===
770
+ if (phase === 'api_key') {
771
+ const masked = apiKeyInput ? '\u2022'.repeat(apiKeyInput.length) : '';
772
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
773
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
774
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${selectedProvider?.name}`),
775
+ isLocal
776
+ ? null
777
+ : h(Text, { color: '#565f89' }, ` | Model: ${selectedModel || 'default'}`)
778
+ ),
779
+ h(Text, { color: '#3b3f52' }, separator),
780
+ settingError ? h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#f7768e' }, ` Error: ${settingError}`)) : null,
781
+ h(Box, { marginTop: 1, paddingLeft: 2 },
782
+ h(Text, { color: '#e0af68' }, 'Enter your API key:')
783
+ ),
784
+ h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
785
+ h(Text, { color: '#7aa2f7' }, ' > '),
786
+ h(Text, { color: '#c0caf5' }, masked || ' '),
787
+ h(Text, { color: '#565f89' }, !apiKeyInput ? ' Paste your key here' : '')
788
+ ),
789
+ h(Text, { color: '#3b3f52' }, separator),
790
+ h(Box, { flexDirection: 'row' },
791
+ h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Confirm'),
792
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
793
+ ),
794
+ );
795
+ }
796
+
797
+ // === UPDATE KEY INPUT VIEW ===
798
+ if (phase === 'update_key_input') {
799
+ const masked = apiKeyInput ? '\u2022'.repeat(apiKeyInput.length) : '';
800
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
801
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
802
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${selectedProvider?.name} - Update API Key`),
803
+ ),
804
+ h(Text, { color: '#3b3f52' }, separator),
805
+ settingError ? h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#f7768e' }, ` Error: ${settingError}`)) : null,
806
+ h(Box, { marginTop: 1, paddingLeft: 2, flexDirection: 'row' },
807
+ h(Text, { color: '#565f89' }, ` Current key: ${selectedProvider?.key_masked || 'none'}`)
808
+ ),
809
+ h(Box, { marginTop: 1, paddingLeft: 2 },
810
+ h(Text, { color: '#e0af68' }, 'Enter new API key:')
811
+ ),
812
+ h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
813
+ h(Text, { color: '#7aa2f7' }, ' > '),
814
+ h(Text, { color: '#c0caf5' }, masked || ' '),
815
+ h(Text, { color: '#565f89' }, !apiKeyInput ? ' Paste your new key here' : '')
816
+ ),
817
+ h(Text, { color: '#3b3f52' }, separator),
818
+ h(Box, { flexDirection: 'row' },
819
+ h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Confirm'),
820
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
821
+ ),
822
+ );
823
+ }
824
+
825
+ // === MODELS VIEW (reused for both full setup and switch_model) ===
826
+ const isSwitchModels = phase === 'switch_models';
827
+ if (phase === 'models' || isSwitchModels) {
828
+ if (modelsLoading) {
829
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
830
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${selectedProvider?.name} - Models`),
831
+ h(Text, { color: '#3b3f52' }, separator),
832
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#565f89' }, ' Fetching models...')),
833
+ );
834
+ }
835
+
836
+ const maxVis = Math.min(allModels.length, Math.max(5, (rows || 24) - 12));
837
+ let startIdx = 0;
838
+ if (allModels.length > maxVis) {
839
+ const half = Math.floor(maxVis / 2);
840
+ startIdx = Math.max(0, Math.min(modelsCursor - half, allModels.length - maxVis));
841
+ }
842
+
843
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
844
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
845
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${selectedProvider?.name} - Select Model`),
846
+ h(Text, { color: '#565f89' }, ` ${filteredModels.length} models`),
847
+ modelsData?.source ? h(Text, { color: '#73daca' }, ` (${modelsData.source})`) : null
848
+ ),
849
+ h(Text, { color: '#3b3f52' }, separator),
850
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
851
+ h(Text, { color: '#e0af68' }, ' Search: '),
852
+ h(Text, { color: '#c0caf5' }, modelsQuery || ' '),
853
+ h(Text, { color: '#565f89' }, modelsQuery ? '' : ' type to filter models')
854
+ ),
855
+ modelsData?.warning
856
+ ? h(Box, { paddingLeft: 2, marginBottom: 1 },
857
+ h(Text, { color: '#e0af68' }, ` ${modelsData.warning}`)
858
+ )
859
+ : null,
860
+ h(Text, { color: '#3b3f52' }, separator),
861
+ ...allModels.slice(startIdx, startIdx + maxVis).map((model, i) => {
862
+ const realIdx = startIdx + i;
863
+ const isHL = realIdx === modelsCursor;
864
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' > ') : h(Text, { color: '#3b3f52' }, ' ');
865
+ const isDefault = model === '__default__ (auto)';
866
+ const display = isDefault ? 'Default (auto)' : model;
867
+ return h(Box, { key: realIdx },
868
+ prefix,
869
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
870
+ ` ${display}`)
871
+ );
872
+ }),
873
+ allModels.length > maxVis
874
+ ? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${Math.min(startIdx + maxVis, allModels.length)} of ${allModels.length}`)
875
+ : null,
876
+ h(Text, { color: '#3b3f52' }, separator),
877
+ h(Box, { flexDirection: 'row' },
878
+ h(Text, { color: '#9ece6a' }, ' UP/DOWN'), h(Text, { color: '#565f89' }, ' Navigate'),
879
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Select'),
880
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
881
+ ),
882
+ );
883
+ }
884
+
885
+ // === CONFIGURED VIEW ===
886
+ if (phase === 'configured') {
887
+ if (configuredLoading) {
888
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
889
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${actionType === 'switch_model' ? 'Switch Model' : actionType === 'update_key' ? 'Update API Key' : 'Switch Provider'}`),
890
+ h(Text, { color: '#3b3f52' }, separator),
891
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#565f89' }, ' Loading configured providers...')),
892
+ );
893
+ }
894
+
895
+ if (configuredError) {
896
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
897
+ h(Text, { color: '#7aa2f7', bold: true }, ' Error'),
898
+ h(Text, { color: '#3b3f52' }, separator),
899
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#f7768e' }, ` ${configuredError}`)),
900
+ h(Text, { color: '#3b3f52' }, separator),
901
+ h(Box, { flexDirection: 'row' },
902
+ h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
903
+ ),
904
+ );
905
+ }
906
+
907
+ if (filteredConfigured.length === 0) {
908
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
909
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${actionType === 'switch_model' ? 'Switch Model' : actionType === 'update_key' ? 'Update API Key' : 'Switch Provider'}`),
910
+ h(Text, { color: '#3b3f52' }, separator),
911
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#565f89' }, ' No configured providers found.')),
912
+ h(Box, { paddingLeft: 2 }, h(Text, { color: '#e0af68' }, ' Use "Select Provider" to add one first.')),
913
+ h(Text, { color: '#3b3f52' }, separator),
914
+ h(Box, { flexDirection: 'row' },
915
+ h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
916
+ ),
917
+ );
918
+ }
919
+
920
+ const maxVis = Math.min(filteredConfigured.length, Math.max(5, (rows || 24) - 12));
921
+ let startIdx = 0;
922
+ if (filteredConfigured.length > maxVis) {
923
+ const half = Math.floor(maxVis / 2);
924
+ startIdx = Math.max(0, Math.min(configuredCursor - half, filteredConfigured.length - maxVis));
925
+ }
926
+
927
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
928
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
929
+ h(Text, { color: '#7aa2f7', bold: true }, ` ${actionType === 'switch_model' ? 'Switch Model' : actionType === 'update_key' ? 'Update API Key' : 'Switch Provider'}`),
930
+ ),
931
+ h(Text, { color: '#3b3f52' }, separator),
932
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
933
+ h(Text, { color: '#e0af68' }, ' Search: '),
934
+ h(Text, { color: '#c0caf5' }, configuredQuery || ' '),
935
+ h(Text, { color: '#565f89' }, configuredQuery ? '' : ' type to filter')
936
+ ),
937
+ h(Text, { color: '#3b3f52' }, separator),
938
+ ...filteredConfigured.slice(startIdx, startIdx + maxVis).map((p, i) => {
939
+ const realIdx = startIdx + i;
940
+ const isHL = realIdx === configuredCursor;
941
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' > ') : h(Text, { color: '#3b3f52' }, ' ');
942
+ const activeLabel = p.active ? h(Text, { color: '#73daca' }, ' [active]') : null;
943
+ const modelLabel = p.model ? h(Text, { color: '#565f89' }, ` ${p.model}`) : null;
944
+ const keyLabel = p.key_masked && p.key_masked !== 'N/A' ? h(Text, { color: '#565f89' }, ` ${p.key_masked}`) : null;
945
+ return h(Box, { key: `cfg-${realIdx}` },
946
+ prefix,
947
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
948
+ ` ${p.name || p.type}`.padEnd(25)
949
+ ),
950
+ activeLabel,
951
+ modelLabel,
952
+ keyLabel,
953
+ );
954
+ }),
955
+ filteredConfigured.length > maxVis
956
+ ? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${Math.min(startIdx + maxVis, filteredConfigured.length)} of ${filteredConfigured.length}`)
957
+ : null,
958
+ h(Text, { color: '#3b3f52' }, separator),
959
+ h(Box, { flexDirection: 'row' },
960
+ h(Text, { color: '#9ece6a' }, ' UP/DOWN'), h(Text, { color: '#565f89' }, ' Navigate'),
961
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Select'),
962
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back'),
963
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Type'), h(Text, { color: '#565f89' }, ' Search')
964
+ ),
965
+ );
966
+ }
967
+
968
+ // === CATALOG VIEW ===
969
+ if (phase === 'catalog') {
970
+ if (catalogLoading) {
971
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
972
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
973
+ h(Text, { color: '#7aa2f7', bold: true }, ' Provider Catalog'),
974
+ h(Text, { color: '#565f89' }, isLocal ? ' (Local)' : ''),
975
+ ),
976
+ h(Text, { color: '#3b3f52' }, separator),
977
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#565f89' }, ' Loading providers...')),
978
+ );
979
+ }
980
+
981
+ if (catalogError) {
982
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
983
+ h(Text, { color: '#7aa2f7', bold: true }, ' Provider Catalog'),
984
+ h(Text, { color: '#3b3f52' }, separator),
985
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#f7768e' }, ` ${catalogError}`)),
986
+ h(Text, { color: '#3b3f52' }, separator),
987
+ h(Box, { flexDirection: 'row' },
988
+ h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
989
+ ),
990
+ );
991
+ }
992
+
993
+ if (filteredCatalog.length === 0) {
994
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
995
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
996
+ h(Text, { color: '#7aa2f7', bold: true }, ' Provider Catalog'),
997
+ h(Text, { color: '#565f89' }, isLocal ? ' (Local)' : ''),
998
+ ),
999
+ h(Text, { color: '#3b3f52' }, separator),
1000
+ h(Box, { paddingLeft: 2, marginTop: 1 }, h(Text, { color: '#565f89' }, ' No providers found.')),
1001
+ h(Text, { color: '#3b3f52' }, separator),
1002
+ h(Box, { flexDirection: 'row' },
1003
+ h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back')
1004
+ ),
1005
+ );
1006
+ }
1007
+
1008
+ const maxVis = Math.min(filteredCatalog.length, Math.max(5, (rows || 24) - 12));
1009
+ let startIdx = 0;
1010
+ if (filteredCatalog.length > maxVis) {
1011
+ const half = Math.floor(maxVis / 2);
1012
+ startIdx = Math.max(0, Math.min(catalogCursor - half, filteredCatalog.length - maxVis));
1013
+ }
1014
+
1015
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
1016
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
1017
+ h(Text, { color: '#7aa2f7', bold: true }, ' Provider Catalog'),
1018
+ isLocal ? h(Text, { color: '#565f89' }, ' (Local)') : null,
1019
+ h(Text, { color: '#565f89' }, ` ${filteredCatalog.length} providers`),
1020
+ ),
1021
+ h(Text, { color: '#3b3f52' }, separator),
1022
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
1023
+ h(Text, { color: '#e0af68' }, ' Search: '),
1024
+ h(Text, { color: '#c0caf5' }, catalogQuery || ' '),
1025
+ h(Text, { color: '#565f89' }, catalogQuery ? '' : ' type to filter providers')
1026
+ ),
1027
+ h(Text, { color: '#3b3f52' }, separator),
1028
+ ...filteredCatalog.slice(startIdx, startIdx + maxVis).map((p, i) => {
1029
+ const realIdx = startIdx + i;
1030
+ const isHL = realIdx === catalogCursor;
1031
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' > ') : h(Text, { color: '#3b3f52' }, ' ');
1032
+ const freeBadge = p.free_tier ? h(Text, { color: '#73daca' }, ' [free]') : null;
1033
+ const keyBadge = p.needs_key ? h(Text, { color: '#e0af68' }, ' [key]') : h(Text, { color: '#565f89' }, ' [no key]');
1034
+ const urlBadge = p.needs_base_url ? h(Text, { color: '#bb9af7' }, ' [custom url]') : null;
1035
+ return h(Box, { key: `cat-${realIdx}` },
1036
+ prefix,
1037
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
1038
+ ` ${p.name || p.id}`.padEnd(25)
1039
+ ),
1040
+ freeBadge,
1041
+ keyBadge,
1042
+ urlBadge,
1043
+ );
1044
+ }),
1045
+ filteredCatalog.length > maxVis
1046
+ ? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${Math.min(startIdx + maxVis, filteredCatalog.length)} of ${filteredCatalog.length}`)
1047
+ : null,
1048
+ h(Text, { color: '#3b3f52' }, separator),
1049
+ h(Box, { flexDirection: 'row' },
1050
+ h(Text, { color: '#9ece6a' }, ' UP/DOWN'), h(Text, { color: '#565f89' }, ' Navigate'),
1051
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Select'),
1052
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Back'),
1053
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Type'), h(Text, { color: '#565f89' }, ' Search')
1054
+ ),
1055
+ );
1056
+ }
1057
+
1058
+ // === MENU VIEW ===
1059
+ return h(Box, { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
1060
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
1061
+ h(Text, { color: '#7aa2f7', bold: true }, TITLE),
1062
+ isLocal
1063
+ ? (currentProvider && currentProvider.type
1064
+ ? h(Text, { color: '#9ece6a' }, ` > ${currentProvider.type}${currentProvider.model ? '/' + currentProvider.model : ''}`)
1065
+ : h(Text, { color: '#565f89' }, ' > No provider configured'))
1066
+ : (currentProvider && currentProvider.type && currentProvider.type !== 'osai'
1067
+ ? h(Text, { color: '#9ece6a' }, ` > ${currentProvider.type}${currentProvider.model ? '/' + currentProvider.model : ''}`)
1068
+ : h(Text, { color: '#565f89' }, ' > OS AI Agent (auto)'))
1069
+ ),
1070
+ h(Text, { color: '#3b3f52' }, separator),
1071
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
1072
+ h(Text, { color: '#e0af68' }, ' Search: '),
1073
+ h(Text, { color: '#c0caf5' }, menuQuery || ' '),
1074
+ h(Text, { color: '#565f89' }, menuQuery ? '' : ' type to filter actions')
1075
+ ),
1076
+ h(Text, { color: '#3b3f52' }, separator),
1077
+ ...filteredMenu.map((action, i) => {
1078
+ const isHL = i === menuCursor;
1079
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' > ') : h(Text, { color: '#3b3f52' }, ' ');
1080
+ return h(Box, { key: action.action },
1081
+ prefix,
1082
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
1083
+ ` ${action.name.padEnd(22)} ${isHL ? action.desc : ''}`
1084
+ ),
1085
+ );
1086
+ }),
1087
+ h(Text, { color: '#3b3f52' }, separator),
1088
+ h(Box, { flexDirection: 'row' },
1089
+ h(Text, { color: '#9ece6a' }, ' UP/DOWN'), h(Text, { color: '#565f89' }, ' Navigate'),
1090
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Select'),
1091
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Cancel'),
1092
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Type'), h(Text, { color: '#565f89' }, ' Search')
1093
+ ),
1094
+ );
1095
+ }