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.
Files changed (149) hide show
  1. package/README.md +8 -90
  2. package/dist/agent/agent.d.ts +1 -1
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +8 -2
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/types.d.ts +9 -1
  7. package/dist/agent/types.d.ts.map +1 -1
  8. package/dist/cli/components/AllModelsSelector.d.ts +11 -0
  9. package/dist/cli/components/AllModelsSelector.d.ts.map +1 -0
  10. package/dist/cli/components/AllModelsSelector.js +153 -0
  11. package/dist/cli/components/AllModelsSelector.js.map +1 -0
  12. package/dist/cli/components/App.d.ts.map +1 -1
  13. package/dist/cli/components/App.js +59 -25
  14. package/dist/cli/components/App.js.map +1 -1
  15. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  16. package/dist/cli/components/CommandSuggestions.js +1 -0
  17. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  18. package/dist/cli/components/Messages.d.ts +15 -1
  19. package/dist/cli/components/Messages.d.ts.map +1 -1
  20. package/dist/cli/components/Messages.js +41 -15
  21. package/dist/cli/components/Messages.js.map +1 -1
  22. package/dist/cli/components/ModelSelector.d.ts +7 -7
  23. package/dist/cli/components/ModelSelector.d.ts.map +1 -1
  24. package/dist/cli/components/ModelSelector.js +116 -33
  25. package/dist/cli/components/ModelSelector.js.map +1 -1
  26. package/dist/cli/components/ProviderManager.d.ts +8 -0
  27. package/dist/cli/components/ProviderManager.d.ts.map +1 -0
  28. package/dist/cli/components/ProviderManager.js +280 -0
  29. package/dist/cli/components/ProviderManager.js.map +1 -0
  30. package/dist/cli/components/markdown.d.ts +9 -0
  31. package/dist/cli/components/markdown.d.ts.map +1 -0
  32. package/dist/cli/components/markdown.js +129 -0
  33. package/dist/cli/components/markdown.js.map +1 -0
  34. package/dist/cli/components/theme.d.ts +5 -0
  35. package/dist/cli/components/theme.d.ts.map +1 -1
  36. package/dist/cli/components/theme.js +7 -0
  37. package/dist/cli/components/theme.js.map +1 -1
  38. package/dist/cli/index.js +19 -5
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/config/index.d.ts +3 -2
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +2 -1
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/providers-config.d.ts +28 -0
  45. package/dist/config/providers-config.d.ts.map +1 -0
  46. package/dist/config/providers-config.js +79 -0
  47. package/dist/config/providers-config.js.map +1 -0
  48. package/dist/config/types.d.ts +31 -1
  49. package/dist/config/types.d.ts.map +1 -1
  50. package/dist/config/types.js +1 -0
  51. package/dist/config/types.js.map +1 -1
  52. package/dist/providers/gemini.d.ts.map +1 -1
  53. package/dist/providers/gemini.js +14 -3
  54. package/dist/providers/gemini.js.map +1 -1
  55. package/dist/providers/index.d.ts +5 -3
  56. package/dist/providers/index.d.ts.map +1 -1
  57. package/dist/providers/index.js +13 -1
  58. package/dist/providers/index.js.map +1 -1
  59. package/dist/providers/registry.d.ts +66 -0
  60. package/dist/providers/registry.d.ts.map +1 -0
  61. package/dist/providers/registry.js +158 -0
  62. package/dist/providers/registry.js.map +1 -0
  63. package/dist/providers/search/brave.d.ts +14 -0
  64. package/dist/providers/search/brave.d.ts.map +1 -0
  65. package/dist/providers/search/brave.js +87 -0
  66. package/dist/providers/search/brave.js.map +1 -0
  67. package/dist/providers/search/exa.d.ts +12 -0
  68. package/dist/providers/search/exa.d.ts.map +1 -0
  69. package/dist/providers/search/exa.js +158 -0
  70. package/dist/providers/search/exa.js.map +1 -0
  71. package/dist/providers/search/index.d.ts +31 -0
  72. package/dist/providers/search/index.d.ts.map +1 -0
  73. package/dist/providers/search/index.js +75 -0
  74. package/dist/providers/search/index.js.map +1 -0
  75. package/dist/providers/search/serper.d.ts +14 -0
  76. package/dist/providers/search/serper.d.ts.map +1 -0
  77. package/dist/providers/search/serper.js +87 -0
  78. package/dist/providers/search/serper.js.map +1 -0
  79. package/dist/providers/search/types.d.ts +21 -0
  80. package/dist/providers/search/types.d.ts.map +1 -0
  81. package/dist/providers/search/types.js +5 -0
  82. package/dist/providers/search/types.js.map +1 -0
  83. package/dist/providers/store.d.ts +104 -0
  84. package/dist/providers/store.d.ts.map +1 -0
  85. package/dist/providers/store.js +171 -0
  86. package/dist/providers/store.js.map +1 -0
  87. package/dist/providers/types.d.ts +7 -1
  88. package/dist/providers/types.d.ts.map +1 -1
  89. package/dist/providers/vertex-ai.d.ts +33 -0
  90. package/dist/providers/vertex-ai.d.ts.map +1 -0
  91. package/dist/providers/vertex-ai.js +407 -0
  92. package/dist/providers/vertex-ai.js.map +1 -0
  93. package/dist/tools/builtin/webfetch.d.ts +20 -0
  94. package/dist/tools/builtin/webfetch.d.ts.map +1 -0
  95. package/dist/tools/builtin/webfetch.js +231 -0
  96. package/dist/tools/builtin/webfetch.js.map +1 -0
  97. package/dist/tools/builtin/websearch.d.ts +17 -0
  98. package/dist/tools/builtin/websearch.d.ts.map +1 -0
  99. package/dist/tools/builtin/websearch.js +101 -0
  100. package/dist/tools/builtin/websearch.js.map +1 -0
  101. package/dist/tools/index.d.ts +11 -0
  102. package/dist/tools/index.d.ts.map +1 -1
  103. package/dist/tools/index.js +24 -2
  104. package/dist/tools/index.js.map +1 -1
  105. package/dist/tools/types.d.ts +19 -0
  106. package/dist/tools/types.d.ts.map +1 -1
  107. package/dist/tools/types.js +8 -0
  108. package/dist/tools/types.js.map +1 -1
  109. package/dist/tools/utils/ssrf.d.ts +18 -0
  110. package/dist/tools/utils/ssrf.d.ts.map +1 -0
  111. package/dist/tools/utils/ssrf.js +70 -0
  112. package/dist/tools/utils/ssrf.js.map +1 -0
  113. package/docs/README.md +5 -4
  114. package/docs/proposals/0001-web-fetch-tool.md +32 -2
  115. package/docs/proposals/0002-web-search-tool.md +59 -2
  116. package/docs/proposals/0041-configuration-system.md +556 -0
  117. package/docs/proposals/README.md +3 -2
  118. package/docs/providers.md +220 -0
  119. package/package.json +7 -2
  120. package/src/agent/agent.ts +9 -2
  121. package/src/agent/types.ts +9 -1
  122. package/src/cli/components/App.tsx +72 -23
  123. package/src/cli/components/CommandSuggestions.tsx +1 -0
  124. package/src/cli/components/Messages.tsx +117 -29
  125. package/src/cli/components/ModelSelector.tsx +169 -52
  126. package/src/cli/components/ProviderManager.tsx +534 -0
  127. package/src/cli/components/markdown.ts +157 -0
  128. package/src/cli/components/theme.ts +7 -0
  129. package/src/cli/index.tsx +22 -7
  130. package/src/config/index.ts +3 -2
  131. package/src/config/providers-config.ts +85 -0
  132. package/src/config/types.ts +35 -1
  133. package/src/providers/gemini.ts +20 -4
  134. package/src/providers/index.ts +18 -3
  135. package/src/providers/registry.ts +198 -0
  136. package/src/providers/search/brave.ts +132 -0
  137. package/src/providers/search/exa.ts +217 -0
  138. package/src/providers/search/index.ts +79 -0
  139. package/src/providers/search/serper.ts +133 -0
  140. package/src/providers/search/types.ts +24 -0
  141. package/src/providers/store.ts +216 -0
  142. package/src/providers/types.ts +9 -1
  143. package/src/providers/vertex-ai.ts +594 -0
  144. package/src/tools/builtin/webfetch.ts +264 -0
  145. package/src/tools/builtin/websearch.ts +117 -0
  146. package/src/tools/index.ts +24 -2
  147. package/src/tools/types.ts +20 -0
  148. package/src/tools/utils/ssrf.ts +79 -0
  149. 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
+ }