gencode-ai 0.1.0 → 0.1.1
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.
- package/README.md +8 -90
- package/dist/agent/agent.d.ts +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +8 -2
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/types.d.ts +9 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/cli/components/AllModelsSelector.d.ts +11 -0
- package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
- package/dist/cli/components/AllModelsSelector.js +153 -0
- package/dist/cli/components/AllModelsSelector.js.map +1 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +59 -25
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +1 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +15 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +41 -15
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/ModelSelector.d.ts +7 -7
- package/dist/cli/components/ModelSelector.d.ts.map +1 -1
- package/dist/cli/components/ModelSelector.js +116 -33
- package/dist/cli/components/ModelSelector.js.map +1 -1
- package/dist/cli/components/ProviderManager.d.ts +8 -0
- package/dist/cli/components/ProviderManager.d.ts.map +1 -0
- package/dist/cli/components/ProviderManager.js +280 -0
- package/dist/cli/components/ProviderManager.js.map +1 -0
- package/dist/cli/components/markdown.d.ts +9 -0
- package/dist/cli/components/markdown.d.ts.map +1 -0
- package/dist/cli/components/markdown.js +129 -0
- package/dist/cli/components/markdown.js.map +1 -0
- package/dist/cli/components/theme.d.ts +5 -0
- package/dist/cli/components/theme.d.ts.map +1 -1
- package/dist/cli/components/theme.js +7 -0
- package/dist/cli/components/theme.js.map +1 -1
- package/dist/cli/index.js +19 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/config/index.d.ts +3 -2
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/providers-config.d.ts +28 -0
- package/dist/config/providers-config.d.ts.map +1 -0
- package/dist/config/providers-config.js +79 -0
- package/dist/config/providers-config.js.map +1 -0
- package/dist/config/types.d.ts +31 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +14 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +5 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +13 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/registry.d.ts +66 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +158 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/search/brave.d.ts +14 -0
- package/dist/providers/search/brave.d.ts.map +1 -0
- package/dist/providers/search/brave.js +87 -0
- package/dist/providers/search/brave.js.map +1 -0
- package/dist/providers/search/exa.d.ts +12 -0
- package/dist/providers/search/exa.d.ts.map +1 -0
- package/dist/providers/search/exa.js +158 -0
- package/dist/providers/search/exa.js.map +1 -0
- package/dist/providers/search/index.d.ts +31 -0
- package/dist/providers/search/index.d.ts.map +1 -0
- package/dist/providers/search/index.js +75 -0
- package/dist/providers/search/index.js.map +1 -0
- package/dist/providers/search/serper.d.ts +14 -0
- package/dist/providers/search/serper.d.ts.map +1 -0
- package/dist/providers/search/serper.js +87 -0
- package/dist/providers/search/serper.js.map +1 -0
- package/dist/providers/search/types.d.ts +21 -0
- package/dist/providers/search/types.d.ts.map +1 -0
- package/dist/providers/search/types.js +5 -0
- package/dist/providers/search/types.js.map +1 -0
- package/dist/providers/store.d.ts +104 -0
- package/dist/providers/store.d.ts.map +1 -0
- package/dist/providers/store.js +171 -0
- package/dist/providers/store.js.map +1 -0
- package/dist/providers/types.d.ts +7 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/vertex-ai.d.ts +33 -0
- package/dist/providers/vertex-ai.d.ts.map +1 -0
- package/dist/providers/vertex-ai.js +407 -0
- package/dist/providers/vertex-ai.js.map +1 -0
- package/dist/tools/builtin/webfetch.d.ts +20 -0
- package/dist/tools/builtin/webfetch.d.ts.map +1 -0
- package/dist/tools/builtin/webfetch.js +231 -0
- package/dist/tools/builtin/webfetch.js.map +1 -0
- package/dist/tools/builtin/websearch.d.ts +17 -0
- package/dist/tools/builtin/websearch.d.ts.map +1 -0
- package/dist/tools/builtin/websearch.js +101 -0
- package/dist/tools/builtin/websearch.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +24 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +19 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js +8 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/utils/ssrf.d.ts +18 -0
- package/dist/tools/utils/ssrf.d.ts.map +1 -0
- package/dist/tools/utils/ssrf.js +70 -0
- package/dist/tools/utils/ssrf.js.map +1 -0
- package/docs/README.md +5 -4
- package/docs/proposals/0001-web-fetch-tool.md +32 -2
- package/docs/proposals/0002-web-search-tool.md +59 -2
- package/docs/proposals/0041-configuration-system.md +556 -0
- package/docs/proposals/README.md +3 -2
- package/docs/providers.md +220 -0
- package/package.json +7 -2
- package/src/agent/agent.ts +9 -2
- package/src/agent/types.ts +9 -1
- package/src/cli/components/App.tsx +72 -23
- package/src/cli/components/CommandSuggestions.tsx +1 -0
- package/src/cli/components/Messages.tsx +117 -29
- package/src/cli/components/ModelSelector.tsx +169 -52
- package/src/cli/components/ProviderManager.tsx +534 -0
- package/src/cli/components/markdown.ts +157 -0
- package/src/cli/components/theme.ts +7 -0
- package/src/cli/index.tsx +22 -7
- package/src/config/index.ts +3 -2
- package/src/config/providers-config.ts +85 -0
- package/src/config/types.ts +35 -1
- package/src/providers/gemini.ts +20 -4
- package/src/providers/index.ts +18 -3
- package/src/providers/registry.ts +198 -0
- package/src/providers/search/brave.ts +132 -0
- package/src/providers/search/exa.ts +217 -0
- package/src/providers/search/index.ts +79 -0
- package/src/providers/search/serper.ts +133 -0
- package/src/providers/search/types.ts +24 -0
- package/src/providers/store.ts +216 -0
- package/src/providers/types.ts +9 -1
- package/src/providers/vertex-ai.ts +594 -0
- package/src/tools/builtin/webfetch.ts +264 -0
- package/src/tools/builtin/websearch.ts +117 -0
- package/src/tools/index.ts +24 -2
- package/src/tools/types.ts +20 -0
- package/src/tools/utils/ssrf.ts +79 -0
- package/CLAUDE.md +0 -70
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Manager - Manage provider connections
|
|
3
|
+
*/
|
|
4
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
5
|
+
import { Box, Text, useInput } from 'ink';
|
|
6
|
+
import TextInput from 'ink-text-input';
|
|
7
|
+
import { colors, icons } from './theme.js';
|
|
8
|
+
import { LoadingSpinner } from './Spinner.js';
|
|
9
|
+
import {
|
|
10
|
+
getProvidersSorted,
|
|
11
|
+
getAvailableConnections,
|
|
12
|
+
isConnectionReady,
|
|
13
|
+
getSearchProvidersSorted,
|
|
14
|
+
type ProviderDefinition,
|
|
15
|
+
type ConnectionOption,
|
|
16
|
+
type SearchProviderDefinition,
|
|
17
|
+
} from '../../providers/registry.js';
|
|
18
|
+
import { getProviderStore, type ModelInfo } from '../../providers/store.js';
|
|
19
|
+
import { createProvider, type ProviderName } from '../../providers/index.js';
|
|
20
|
+
import { isSearchProviderAvailable, type SearchProviderName } from '../../providers/search/index.js';
|
|
21
|
+
|
|
22
|
+
interface ProviderManagerProps {
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onProviderChange?: (providerId: ProviderName, model: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type View = 'list' | 'select-connection' | 'confirm-remove' | 'search-list';
|
|
28
|
+
type Tab = 'llm' | 'search';
|
|
29
|
+
|
|
30
|
+
interface ProviderItem {
|
|
31
|
+
provider: ProviderDefinition;
|
|
32
|
+
connected: boolean;
|
|
33
|
+
modelCount: number;
|
|
34
|
+
connectionMethod?: string;
|
|
35
|
+
readyConnections: ConnectionOption[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SearchProviderItem {
|
|
39
|
+
provider: SearchProviderDefinition;
|
|
40
|
+
isSelected: boolean;
|
|
41
|
+
isAvailable: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ProviderManager({ onClose }: ProviderManagerProps) {
|
|
45
|
+
const store = getProviderStore();
|
|
46
|
+
|
|
47
|
+
const [view, setView] = useState<View>('list');
|
|
48
|
+
const [tab, setTab] = useState<Tab>('llm');
|
|
49
|
+
const [filter, setFilter] = useState('');
|
|
50
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
51
|
+
const [connectionIndex, setConnectionIndex] = useState(0);
|
|
52
|
+
const [loading, setLoading] = useState(false);
|
|
53
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
54
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderDefinition | null>(null);
|
|
55
|
+
const [searchSelectedIndex, setSearchSelectedIndex] = useState(0);
|
|
56
|
+
|
|
57
|
+
// Build provider list
|
|
58
|
+
const buildProviderList = useCallback((): ProviderItem[] => {
|
|
59
|
+
const allProviders = getProvidersSorted();
|
|
60
|
+
return allProviders.map((provider) => {
|
|
61
|
+
const connected = store.isConnected(provider.id);
|
|
62
|
+
const connection = store.getConnection(provider.id);
|
|
63
|
+
const readyConnections = getAvailableConnections(provider);
|
|
64
|
+
return {
|
|
65
|
+
provider,
|
|
66
|
+
connected,
|
|
67
|
+
modelCount: store.getModelCount(provider.id),
|
|
68
|
+
connectionMethod: connection?.method,
|
|
69
|
+
readyConnections,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}, [store]);
|
|
73
|
+
|
|
74
|
+
const [providerList, setProviderList] = useState<ProviderItem[]>(buildProviderList);
|
|
75
|
+
|
|
76
|
+
// Refresh list
|
|
77
|
+
const refreshList = useCallback(() => {
|
|
78
|
+
setProviderList(buildProviderList());
|
|
79
|
+
}, [buildProviderList]);
|
|
80
|
+
|
|
81
|
+
// Build search provider list
|
|
82
|
+
const buildSearchProviderList = useCallback((): SearchProviderItem[] => {
|
|
83
|
+
const currentSearch = store.getSearchProvider();
|
|
84
|
+
return getSearchProvidersSorted().map((provider) => ({
|
|
85
|
+
provider,
|
|
86
|
+
isSelected: provider.id === currentSearch || (!currentSearch && provider.id === 'exa'),
|
|
87
|
+
isAvailable: isSearchProviderAvailable(provider.id),
|
|
88
|
+
}));
|
|
89
|
+
}, [store]);
|
|
90
|
+
|
|
91
|
+
const searchProviders = buildSearchProviderList();
|
|
92
|
+
|
|
93
|
+
// Select search provider
|
|
94
|
+
const selectSearchProvider = (id: SearchProviderName) => {
|
|
95
|
+
if (id === 'exa') {
|
|
96
|
+
store.clearSearchProvider(); // Use default
|
|
97
|
+
} else {
|
|
98
|
+
store.setSearchProvider(id);
|
|
99
|
+
}
|
|
100
|
+
setMessage(`Search provider set to ${id}`);
|
|
101
|
+
setTimeout(() => setMessage(null), 2000);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Filter providers
|
|
105
|
+
const filterLower = filter.toLowerCase();
|
|
106
|
+
const filteredProviders = providerList.filter(
|
|
107
|
+
(item) =>
|
|
108
|
+
item.provider.name.toLowerCase().includes(filterLower) ||
|
|
109
|
+
item.provider.id.toLowerCase().includes(filterLower)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Split into connected and available
|
|
113
|
+
const connected = filteredProviders.filter((p) => p.connected);
|
|
114
|
+
const available = filteredProviders.filter((p) => !p.connected);
|
|
115
|
+
|
|
116
|
+
// Combined list for navigation
|
|
117
|
+
const allItems = [...connected, ...available];
|
|
118
|
+
|
|
119
|
+
// Reset selection when filter changes
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
setSelectedIndex(0);
|
|
122
|
+
}, [filter]);
|
|
123
|
+
|
|
124
|
+
// Fetch and cache models for a provider (use providerImpl if specified)
|
|
125
|
+
const fetchModels = async (
|
|
126
|
+
providerId: ProviderName,
|
|
127
|
+
connOption?: ConnectionOption
|
|
128
|
+
): Promise<ModelInfo[]> => {
|
|
129
|
+
try {
|
|
130
|
+
// Use providerImpl if specified, otherwise use the provider id
|
|
131
|
+
const implId = connOption?.providerImpl || providerId;
|
|
132
|
+
const provider = createProvider({ provider: implId });
|
|
133
|
+
const models = await provider.listModels();
|
|
134
|
+
store.cacheModels(providerId, models);
|
|
135
|
+
return models;
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Connect with a specific connection option
|
|
142
|
+
const connectWithOption = async (item: ProviderItem, connOption: ConnectionOption) => {
|
|
143
|
+
setLoading(true);
|
|
144
|
+
setMessage(`Connecting via ${connOption.name}...`);
|
|
145
|
+
store.connect(item.provider.id, connOption.method);
|
|
146
|
+
const models = await fetchModels(item.provider.id, connOption);
|
|
147
|
+
setLoading(false);
|
|
148
|
+
setMessage(`Connected! Cached ${models.length} models`);
|
|
149
|
+
refreshList();
|
|
150
|
+
setView('list');
|
|
151
|
+
setSelectedProvider(null);
|
|
152
|
+
setTimeout(() => setMessage(null), 2000);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Handle connect/refresh
|
|
156
|
+
const handleConnect = async (item: ProviderItem) => {
|
|
157
|
+
if (item.connected) {
|
|
158
|
+
// Refresh: re-fetch models
|
|
159
|
+
setLoading(true);
|
|
160
|
+
setMessage(`Refreshing ${item.provider.name}...`);
|
|
161
|
+
// Get the connection method to find the right provider impl
|
|
162
|
+
const connMethod = item.connectionMethod;
|
|
163
|
+
const connOption = item.provider.connections.find((c) => c.method === connMethod);
|
|
164
|
+
const models = await fetchModels(item.provider.id, connOption);
|
|
165
|
+
setLoading(false);
|
|
166
|
+
setMessage(`Cached ${models.length} models`);
|
|
167
|
+
refreshList();
|
|
168
|
+
setTimeout(() => setMessage(null), 2000);
|
|
169
|
+
} else {
|
|
170
|
+
// Check ready connections
|
|
171
|
+
const readyConns = item.readyConnections;
|
|
172
|
+
|
|
173
|
+
if (readyConns.length === 1) {
|
|
174
|
+
// One ready connection - auto-connect
|
|
175
|
+
await connectWithOption(item, readyConns[0]);
|
|
176
|
+
} else {
|
|
177
|
+
// Zero or multiple ready connections - show selection view
|
|
178
|
+
setSelectedProvider(item.provider);
|
|
179
|
+
setConnectionIndex(0);
|
|
180
|
+
setView('select-connection');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Handle remove
|
|
186
|
+
const handleRemove = (item: ProviderItem) => {
|
|
187
|
+
if (!item.connected) return;
|
|
188
|
+
setSelectedProvider(item.provider);
|
|
189
|
+
setView('confirm-remove');
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Confirm remove
|
|
193
|
+
const confirmRemove = () => {
|
|
194
|
+
if (selectedProvider) {
|
|
195
|
+
store.disconnect(selectedProvider.id);
|
|
196
|
+
refreshList();
|
|
197
|
+
setView('list');
|
|
198
|
+
setSelectedProvider(null);
|
|
199
|
+
setMessage('Provider removed');
|
|
200
|
+
setTimeout(() => setMessage(null), 2000);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Go back to list
|
|
205
|
+
const goBack = () => {
|
|
206
|
+
setView('list');
|
|
207
|
+
setSelectedProvider(null);
|
|
208
|
+
setConnectionIndex(0);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Keyboard navigation for list view (LLM providers)
|
|
212
|
+
useInput(
|
|
213
|
+
(input, key) => {
|
|
214
|
+
if (key.tab || input === 's') {
|
|
215
|
+
// Switch to search tab
|
|
216
|
+
setTab('search');
|
|
217
|
+
setSearchSelectedIndex(0);
|
|
218
|
+
} else if (key.upArrow) {
|
|
219
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
220
|
+
} else if (key.downArrow) {
|
|
221
|
+
setSelectedIndex((i) => Math.min(allItems.length - 1, i + 1));
|
|
222
|
+
} else if (key.return && allItems.length > 0) {
|
|
223
|
+
handleConnect(allItems[selectedIndex]);
|
|
224
|
+
} else if (input === 'r' && allItems.length > 0 && allItems[selectedIndex].connected) {
|
|
225
|
+
handleRemove(allItems[selectedIndex]);
|
|
226
|
+
} else if (key.escape) {
|
|
227
|
+
onClose();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
{ isActive: view === 'list' && tab === 'llm' && !loading }
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Keyboard navigation for search providers tab
|
|
234
|
+
useInput(
|
|
235
|
+
(input, key) => {
|
|
236
|
+
if (key.tab || input === 'l') {
|
|
237
|
+
// Switch to LLM tab
|
|
238
|
+
setTab('llm');
|
|
239
|
+
setSelectedIndex(0);
|
|
240
|
+
} else if (key.upArrow) {
|
|
241
|
+
setSearchSelectedIndex((i) => Math.max(0, i - 1));
|
|
242
|
+
} else if (key.downArrow) {
|
|
243
|
+
setSearchSelectedIndex((i) => Math.min(searchProviders.length - 1, i + 1));
|
|
244
|
+
} else if (key.return && searchProviders.length > 0) {
|
|
245
|
+
const selected = searchProviders[searchSelectedIndex];
|
|
246
|
+
if (selected.isAvailable) {
|
|
247
|
+
selectSearchProvider(selected.provider.id);
|
|
248
|
+
}
|
|
249
|
+
} else if (key.escape) {
|
|
250
|
+
onClose();
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{ isActive: view === 'list' && tab === 'search' && !loading }
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Keyboard for select-connection view
|
|
257
|
+
useInput(
|
|
258
|
+
(_input, key) => {
|
|
259
|
+
if (!selectedProvider) return;
|
|
260
|
+
const allConns = selectedProvider.connections;
|
|
261
|
+
|
|
262
|
+
if (key.upArrow) {
|
|
263
|
+
setConnectionIndex((i) => Math.max(0, i - 1));
|
|
264
|
+
} else if (key.downArrow) {
|
|
265
|
+
setConnectionIndex((i) => Math.min(allConns.length - 1, i + 1));
|
|
266
|
+
} else if (key.return && allConns.length > 0) {
|
|
267
|
+
const selectedConn = allConns[connectionIndex];
|
|
268
|
+
if (isConnectionReady(selectedConn)) {
|
|
269
|
+
const item = allItems.find((i) => i.provider.id === selectedProvider.id);
|
|
270
|
+
if (item) {
|
|
271
|
+
connectWithOption(item, selectedConn);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// If not ready, do nothing (user needs to set env vars first)
|
|
275
|
+
} else if (key.escape) {
|
|
276
|
+
goBack();
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{ isActive: view === 'select-connection' }
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Keyboard for confirm-remove view
|
|
283
|
+
useInput(
|
|
284
|
+
(input, key) => {
|
|
285
|
+
if (input === 'y' || input === 'Y') {
|
|
286
|
+
confirmRemove();
|
|
287
|
+
} else if (input === 'n' || input === 'N' || key.escape) {
|
|
288
|
+
goBack();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
{ isActive: view === 'confirm-remove' }
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
// Loading state
|
|
296
|
+
if (loading) {
|
|
297
|
+
return (
|
|
298
|
+
<Box flexDirection="column">
|
|
299
|
+
<Box>
|
|
300
|
+
<LoadingSpinner />
|
|
301
|
+
<Text color={colors.textMuted}> {message || 'Loading...'}</Text>
|
|
302
|
+
</Box>
|
|
303
|
+
</Box>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Confirm remove view
|
|
308
|
+
if (view === 'confirm-remove' && selectedProvider) {
|
|
309
|
+
return (
|
|
310
|
+
<Box flexDirection="column">
|
|
311
|
+
<Text color={colors.warning}>Remove {selectedProvider.name}?</Text>
|
|
312
|
+
<Text color={colors.textMuted}>
|
|
313
|
+
This will clear cached models for this provider.
|
|
314
|
+
</Text>
|
|
315
|
+
<Box marginTop={1}>
|
|
316
|
+
<Text color={colors.textMuted}>[Y] Confirm [N] Cancel</Text>
|
|
317
|
+
</Box>
|
|
318
|
+
</Box>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Select connection method view
|
|
323
|
+
if (view === 'select-connection' && selectedProvider) {
|
|
324
|
+
const allConns = selectedProvider.connections;
|
|
325
|
+
const selectedConn = allConns[connectionIndex];
|
|
326
|
+
const isSelectedReady = selectedConn ? isConnectionReady(selectedConn) : false;
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<Box flexDirection="column">
|
|
330
|
+
<Text color={colors.primary}>Connect to {selectedProvider.name}</Text>
|
|
331
|
+
<Text color={colors.textMuted}>Select connection method:</Text>
|
|
332
|
+
|
|
333
|
+
<Box flexDirection="column" marginTop={1}>
|
|
334
|
+
{allConns.map((conn, idx) => {
|
|
335
|
+
const isSelected = idx === connectionIndex;
|
|
336
|
+
const ready = isConnectionReady(conn);
|
|
337
|
+
return (
|
|
338
|
+
<Box key={conn.method} paddingLeft={2} flexDirection="column">
|
|
339
|
+
<Box>
|
|
340
|
+
<Text color={isSelected ? colors.primary : colors.textMuted}>
|
|
341
|
+
{isSelected ? icons.arrow : ' '}
|
|
342
|
+
</Text>
|
|
343
|
+
<Text color={isSelected ? colors.text : colors.textSecondary} bold={isSelected}>
|
|
344
|
+
{conn.name}
|
|
345
|
+
</Text>
|
|
346
|
+
{ready ? (
|
|
347
|
+
<Text color={colors.success}> (ready)</Text>
|
|
348
|
+
) : (
|
|
349
|
+
<Text color={colors.textMuted}> (not configured)</Text>
|
|
350
|
+
)}
|
|
351
|
+
{conn.description && (
|
|
352
|
+
<Text color={colors.textMuted}> - {conn.description}</Text>
|
|
353
|
+
)}
|
|
354
|
+
</Box>
|
|
355
|
+
{isSelected && !ready && (
|
|
356
|
+
<Text color={colors.textMuted} dimColor>
|
|
357
|
+
{' '}Set: {conn.envVars.join(' or ')}
|
|
358
|
+
</Text>
|
|
359
|
+
)}
|
|
360
|
+
</Box>
|
|
361
|
+
);
|
|
362
|
+
})}
|
|
363
|
+
</Box>
|
|
364
|
+
|
|
365
|
+
<Box marginTop={1}>
|
|
366
|
+
<Text color={colors.textMuted}>
|
|
367
|
+
↑↓ navigate · {isSelectedReady ? 'Enter connect · ' : ''}Esc back
|
|
368
|
+
</Text>
|
|
369
|
+
</Box>
|
|
370
|
+
</Box>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Main list view
|
|
375
|
+
return (
|
|
376
|
+
<Box flexDirection="column">
|
|
377
|
+
<Text color={colors.primary} bold>
|
|
378
|
+
Provider Management
|
|
379
|
+
</Text>
|
|
380
|
+
|
|
381
|
+
{/* Tabs */}
|
|
382
|
+
<Box marginTop={1}>
|
|
383
|
+
<Text
|
|
384
|
+
color={tab === 'llm' ? colors.primary : colors.textMuted}
|
|
385
|
+
bold={tab === 'llm'}
|
|
386
|
+
>
|
|
387
|
+
[L] LLM Providers
|
|
388
|
+
</Text>
|
|
389
|
+
<Text color={colors.textMuted}> | </Text>
|
|
390
|
+
<Text
|
|
391
|
+
color={tab === 'search' ? colors.primary : colors.textMuted}
|
|
392
|
+
bold={tab === 'search'}
|
|
393
|
+
>
|
|
394
|
+
[S] Search Providers
|
|
395
|
+
</Text>
|
|
396
|
+
</Box>
|
|
397
|
+
|
|
398
|
+
{message && (
|
|
399
|
+
<Box marginTop={1}>
|
|
400
|
+
<Text color={colors.success}>{icons.success} {message}</Text>
|
|
401
|
+
</Box>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{/* LLM Providers Tab */}
|
|
405
|
+
{tab === 'llm' && (
|
|
406
|
+
<>
|
|
407
|
+
<Box marginTop={1}>
|
|
408
|
+
<Text color={colors.textMuted}>{icons.prompt} </Text>
|
|
409
|
+
<TextInput value={filter} onChange={setFilter} placeholder="Filter providers..." />
|
|
410
|
+
</Box>
|
|
411
|
+
|
|
412
|
+
<Box flexDirection="column" marginTop={1}>
|
|
413
|
+
{/* Connected section */}
|
|
414
|
+
{connected.length > 0 && (
|
|
415
|
+
<>
|
|
416
|
+
<Text color={colors.textMuted}>Connected:</Text>
|
|
417
|
+
{connected.map((item, idx) => {
|
|
418
|
+
const isSelected = idx === selectedIndex;
|
|
419
|
+
// Find connection name
|
|
420
|
+
const connOption = item.provider.connections.find(
|
|
421
|
+
(c) => c.method === item.connectionMethod
|
|
422
|
+
);
|
|
423
|
+
const connName = connOption?.name || item.connectionMethod;
|
|
424
|
+
return (
|
|
425
|
+
<Box key={item.provider.id} paddingLeft={2}>
|
|
426
|
+
<Text color={isSelected ? colors.primary : colors.textMuted}>
|
|
427
|
+
{isSelected ? icons.arrow : ' '}
|
|
428
|
+
</Text>
|
|
429
|
+
<Text color={colors.success}>{icons.success} </Text>
|
|
430
|
+
<Text color={isSelected ? colors.text : colors.textSecondary} bold={isSelected}>
|
|
431
|
+
{item.provider.name}
|
|
432
|
+
</Text>
|
|
433
|
+
<Text color={colors.textMuted}>
|
|
434
|
+
{' '}({connName}) · {item.modelCount} models
|
|
435
|
+
</Text>
|
|
436
|
+
</Box>
|
|
437
|
+
);
|
|
438
|
+
})}
|
|
439
|
+
</>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{/* Available section */}
|
|
443
|
+
{available.length > 0 && (
|
|
444
|
+
<>
|
|
445
|
+
<Text color={colors.textMuted} dimColor={connected.length > 0}>
|
|
446
|
+
{connected.length > 0 ? '\n' : ''}Available:
|
|
447
|
+
</Text>
|
|
448
|
+
{available.map((item, idx) => {
|
|
449
|
+
const actualIndex = connected.length + idx;
|
|
450
|
+
const isSelected = actualIndex === selectedIndex;
|
|
451
|
+
const hasReady = item.readyConnections.length > 0;
|
|
452
|
+
|
|
453
|
+
// Show which connection methods are ready
|
|
454
|
+
const readyNames = item.readyConnections.map((c) => c.name);
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<Box key={item.provider.id} paddingLeft={2}>
|
|
458
|
+
<Text color={isSelected ? colors.primary : colors.textMuted}>
|
|
459
|
+
{isSelected ? icons.arrow : ' '}
|
|
460
|
+
</Text>
|
|
461
|
+
<Text color={isSelected ? colors.text : colors.textSecondary} bold={isSelected}>
|
|
462
|
+
{item.provider.name}
|
|
463
|
+
</Text>
|
|
464
|
+
{hasReady && (
|
|
465
|
+
<Text color={colors.success}> ({readyNames.join(', ')})</Text>
|
|
466
|
+
)}
|
|
467
|
+
</Box>
|
|
468
|
+
);
|
|
469
|
+
})}
|
|
470
|
+
</>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
{allItems.length === 0 && (
|
|
474
|
+
<Text color={colors.textMuted}>No providers match "{filter}"</Text>
|
|
475
|
+
)}
|
|
476
|
+
</Box>
|
|
477
|
+
|
|
478
|
+
<Box marginTop={1}>
|
|
479
|
+
<Text color={colors.textMuted}>
|
|
480
|
+
↑↓ navigate · Enter connect · r remove · Tab/s search · Esc exit
|
|
481
|
+
</Text>
|
|
482
|
+
</Box>
|
|
483
|
+
</>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{/* Search Providers Tab */}
|
|
487
|
+
{tab === 'search' && (
|
|
488
|
+
<>
|
|
489
|
+
<Box flexDirection="column" marginTop={1}>
|
|
490
|
+
<Text color={colors.textMuted}>Select search provider:</Text>
|
|
491
|
+
{searchProviders.map((item, idx) => {
|
|
492
|
+
const isSelected = idx === searchSelectedIndex;
|
|
493
|
+
const envVars = item.provider.connections[0]?.envVars || [];
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<Box key={item.provider.id} paddingLeft={2} flexDirection="column">
|
|
497
|
+
<Box>
|
|
498
|
+
<Text color={isSelected ? colors.primary : colors.textMuted}>
|
|
499
|
+
{isSelected ? icons.arrow : ' '}
|
|
500
|
+
</Text>
|
|
501
|
+
{item.isSelected && <Text color={colors.success}>{icons.success} </Text>}
|
|
502
|
+
<Text color={isSelected ? colors.text : colors.textSecondary} bold={isSelected}>
|
|
503
|
+
{item.provider.name}
|
|
504
|
+
</Text>
|
|
505
|
+
{!item.provider.requiresKey && (
|
|
506
|
+
<Text color={colors.success}> (no key required)</Text>
|
|
507
|
+
)}
|
|
508
|
+
{item.provider.requiresKey && item.isAvailable && (
|
|
509
|
+
<Text color={colors.success}> (configured)</Text>
|
|
510
|
+
)}
|
|
511
|
+
{item.provider.requiresKey && !item.isAvailable && (
|
|
512
|
+
<Text color={colors.textMuted}> (not configured)</Text>
|
|
513
|
+
)}
|
|
514
|
+
</Box>
|
|
515
|
+
{isSelected && item.provider.requiresKey && !item.isAvailable && (
|
|
516
|
+
<Text color={colors.textMuted} dimColor>
|
|
517
|
+
{' '}Set: {envVars.join(' or ')}
|
|
518
|
+
</Text>
|
|
519
|
+
)}
|
|
520
|
+
</Box>
|
|
521
|
+
);
|
|
522
|
+
})}
|
|
523
|
+
</Box>
|
|
524
|
+
|
|
525
|
+
<Box marginTop={1}>
|
|
526
|
+
<Text color={colors.textMuted}>
|
|
527
|
+
↑↓ navigate · Enter select · Tab/l LLM providers · Esc exit
|
|
528
|
+
</Text>
|
|
529
|
+
</Box>
|
|
530
|
+
</>
|
|
531
|
+
)}
|
|
532
|
+
</Box>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Terminal Markdown Renderer
|
|
3
|
+
* Uses marked for parsing, chalk for ANSI colors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { marked, type Tokens, type RendererObject, type Token } from 'marked';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
// Helper type for renderer context with parser access
|
|
10
|
+
type RendererContext = {
|
|
11
|
+
parser?: {
|
|
12
|
+
parseInline(tokens: Token[]): string;
|
|
13
|
+
parse(tokens: Token[]): string;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper to parse inline tokens from renderer context
|
|
18
|
+
function parseInline(ctx: unknown, tokens: Token[], fallback: string): string {
|
|
19
|
+
const context = ctx as RendererContext;
|
|
20
|
+
return context.parser?.parseInline(tokens) ?? fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to parse block tokens (like list item content)
|
|
24
|
+
function parseTokens(ctx: unknown, tokens: Token[], fallback: string): string {
|
|
25
|
+
const context = ctx as RendererContext;
|
|
26
|
+
// For list items, tokens may contain text tokens with nested inline tokens
|
|
27
|
+
// Use parse() for block-level token arrays
|
|
28
|
+
return context.parser?.parse(tokens).trim() ?? fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Custom terminal renderer
|
|
32
|
+
const terminalRenderer: RendererObject = {
|
|
33
|
+
// Headings: colored and bold
|
|
34
|
+
heading(token: Tokens.Heading): string {
|
|
35
|
+
const colors = [
|
|
36
|
+
chalk.bold.magenta, // h1
|
|
37
|
+
chalk.bold.cyan, // h2
|
|
38
|
+
chalk.bold.green, // h3
|
|
39
|
+
chalk.bold.yellow, // h4+
|
|
40
|
+
];
|
|
41
|
+
const colorFn = colors[Math.min(token.depth - 1, 3)];
|
|
42
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
43
|
+
return '\n' + colorFn(content) + '\n\n';
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Paragraphs - must parse inline tokens for bold/italic/etc
|
|
47
|
+
paragraph(token: Tokens.Paragraph): string {
|
|
48
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
49
|
+
return content + '\n\n';
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Bold text
|
|
53
|
+
strong(token: Tokens.Strong): string {
|
|
54
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
55
|
+
return chalk.bold(content);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Italic text
|
|
59
|
+
em(token: Tokens.Em): string {
|
|
60
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
61
|
+
return chalk.italic(content);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Inline code
|
|
65
|
+
codespan({ text }: Tokens.Codespan): string {
|
|
66
|
+
return chalk.yellow('`' + text + '`');
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Code blocks - clean format without ``` markers
|
|
70
|
+
code({ text, lang }: Tokens.Code): string {
|
|
71
|
+
const langHeader = lang ? chalk.dim(` [${lang}]`) + '\n' : '';
|
|
72
|
+
const lines = text.split('\n').map(line => chalk.cyan(' ' + line)).join('\n');
|
|
73
|
+
return '\n' + langHeader + lines + '\n\n';
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// List - parse block tokens for each item (they may contain nested text+inline tokens)
|
|
77
|
+
list(token: Tokens.List): string {
|
|
78
|
+
const result = token.items.map((item, i) => {
|
|
79
|
+
const bullet = token.ordered ? chalk.dim(`${i + 1}.`) : chalk.dim('•');
|
|
80
|
+
const content = parseTokens(this, item.tokens, item.text);
|
|
81
|
+
return ` ${bullet} ${content}`;
|
|
82
|
+
}).join('\n');
|
|
83
|
+
return result + '\n\n';
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// List item - use block parser since items contain text tokens with nested inline
|
|
87
|
+
listitem(token: Tokens.ListItem): string {
|
|
88
|
+
return parseTokens(this, token.tokens, token.text);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Links
|
|
92
|
+
link(token: Tokens.Link): string {
|
|
93
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
94
|
+
return chalk.blue.underline(content) + chalk.dim(` (${token.href})`);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Blockquotes
|
|
98
|
+
blockquote(token: Tokens.Blockquote): string {
|
|
99
|
+
const text = token.text.trim();
|
|
100
|
+
const lines = text.split('\n').map(line =>
|
|
101
|
+
chalk.dim('│ ') + chalk.italic(line)
|
|
102
|
+
).join('\n');
|
|
103
|
+
return '\n' + lines + '\n\n';
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Horizontal rule
|
|
107
|
+
hr(): string {
|
|
108
|
+
return '\n' + chalk.dim('─'.repeat(40)) + '\n\n';
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Line break
|
|
112
|
+
br(): string {
|
|
113
|
+
return '\n';
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Delete/strikethrough
|
|
117
|
+
del(token: Tokens.Del): string {
|
|
118
|
+
const content = parseInline(this, token.tokens, token.text);
|
|
119
|
+
return chalk.strikethrough(content);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Plain text - may contain nested inline tokens (like in list items)
|
|
123
|
+
text(token: Tokens.Text | Tokens.Escape): string {
|
|
124
|
+
// Tokens.Text can have nested tokens array with inline formatting
|
|
125
|
+
if ('tokens' in token && token.tokens && token.tokens.length > 0) {
|
|
126
|
+
return parseInline(this, token.tokens, token.text);
|
|
127
|
+
}
|
|
128
|
+
return token.text ?? '';
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// HTML (strip tags)
|
|
132
|
+
html(token: Tokens.HTML | Tokens.Tag): string {
|
|
133
|
+
const text = 'text' in token ? token.text : '';
|
|
134
|
+
return text.replace(/<[^>]*>/g, '');
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Configure marked with custom renderer
|
|
139
|
+
marked.use({ renderer: terminalRenderer });
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Render markdown text to terminal-formatted string
|
|
143
|
+
*/
|
|
144
|
+
export function renderMarkdown(text: string): string {
|
|
145
|
+
try {
|
|
146
|
+
const result = marked.parse(text);
|
|
147
|
+
// marked.parse can return string or Promise<string>
|
|
148
|
+
if (typeof result === 'string') {
|
|
149
|
+
return result.trim();
|
|
150
|
+
}
|
|
151
|
+
// If async, return original text (shouldn't happen with sync renderer)
|
|
152
|
+
return text;
|
|
153
|
+
} catch {
|
|
154
|
+
// On error, return original text
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
}
|