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
@@ -1,5 +1,12 @@
1
+ import { useState, useEffect } from 'react';
1
2
  import { Box, Text } from 'ink';
3
+ import InkSpinner from 'ink-spinner';
2
4
  import { colors, icons } from './theme.js';
5
+ import { renderMarkdown } from './markdown.js';
6
+
7
+ // Truncate string with ellipsis
8
+ const truncate = (str: string, maxLen: number) =>
9
+ str.length > maxLen ? str.slice(0, maxLen - 3) + '...' : str;
3
10
 
4
11
  // Word wrap text to terminal width
5
12
  function wrapText(text: string, width: number): string[] {
@@ -36,7 +43,7 @@ export function UserMessage({ text }: UserMessageProps) {
36
43
  {lines.map((line, i) => (
37
44
  <Box key={i}>
38
45
  <Text color={colors.brand}>{icons.userPrompt} </Text>
39
- <Text backgroundColor="#1E293B" color={colors.text}> {line} </Text>
46
+ <Text backgroundColor={colors.inputBg} color={colors.text}> {line} </Text>
40
47
  </Box>
41
48
  ))}
42
49
  </Box>
@@ -51,29 +58,39 @@ interface AssistantMessageProps {
51
58
  export function AssistantMessage({ text, streaming }: AssistantMessageProps) {
52
59
  if (!text) return null;
53
60
 
54
- // Get terminal width for wrapping
55
- const termWidth = process.stdout.columns || 80;
56
- const contentWidth = termWidth - 4; // Account for prefix
61
+ // Streaming: use simple text display (markdown incomplete during stream)
62
+ if (streaming) {
63
+ const termWidth = process.stdout.columns || 80;
64
+ const contentWidth = termWidth - 4;
65
+ const lines = wrapText(text.trimEnd(), contentWidth);
66
+
67
+ return (
68
+ <Box flexDirection="column" marginTop={1} marginBottom={0}>
69
+ {lines.map((line, i) => (
70
+ <Box key={i}>
71
+ {i === 0 && <Text color={colors.success}>{icons.assistant} </Text>}
72
+ {i > 0 && <Text> </Text>}
73
+ <Text>
74
+ {line}
75
+ {i === lines.length - 1 && (
76
+ <Text color={colors.brandLight}>{icons.cursor}</Text>
77
+ )}
78
+ </Text>
79
+ </Box>
80
+ ))}
81
+ </Box>
82
+ );
83
+ }
57
84
 
58
- // Wrap text to terminal width
59
- const lines = wrapText(text.trimEnd(), contentWidth);
85
+ // Completed: render with markdown
86
+ const rendered = renderMarkdown(text);
60
87
 
61
88
  return (
62
89
  <Box flexDirection="column" marginTop={1} marginBottom={0}>
63
- {lines.map((line, i) => (
64
- <Box key={i}>
65
- {i === 0 && <Text color={colors.success}>{icons.assistant} </Text>}
66
- {i > 0 && <Text> </Text>}
67
- <Text>
68
- {line}
69
- {streaming && i === lines.length - 1 ? (
70
- <Text color={colors.brandLight}>{icons.cursor}</Text>
71
- ) : (
72
- ''
73
- )}
74
- </Text>
75
- </Box>
76
- ))}
90
+ <Box>
91
+ <Text color={colors.success}>{icons.assistant} </Text>
92
+ <Text>{rendered}</Text>
93
+ </Box>
77
94
  </Box>
78
95
  );
79
96
  }
@@ -84,11 +101,24 @@ interface ToolCallProps {
84
101
  }
85
102
 
86
103
  export function ToolCall({ name, input }: ToolCallProps) {
87
- const inputStr = JSON.stringify(input);
88
- const shortInput = inputStr.length > 50 ? inputStr.slice(0, 47) + '...' : inputStr;
104
+ // WebFetch: Show "Fetch(url)" instead of JSON (Claude Code style)
105
+ if (name === 'WebFetch' && input?.url) {
106
+ const shortUrl = truncate(input.url as string, 60);
107
+ return (
108
+ <Box marginTop={1}>
109
+ <Text color={colors.tool}>{icons.fetch}</Text>
110
+ <Text> Fetch(</Text>
111
+ <Text color={colors.info}>{shortUrl}</Text>
112
+ <Text>)</Text>
113
+ </Box>
114
+ );
115
+ }
116
+
117
+ // Default: Show tool name with JSON input
118
+ const shortInput = truncate(JSON.stringify(input), 50);
89
119
 
90
120
  return (
91
- <Box marginLeft={2}>
121
+ <Box marginTop={1}>
92
122
  <Text dimColor>
93
123
  <Text color={colors.tool}>{icons.tool}</Text> {name}{' '}
94
124
  <Text color={colors.textMuted}>{shortInput}</Text>
@@ -97,23 +127,81 @@ export function ToolCall({ name, input }: ToolCallProps) {
97
127
  );
98
128
  }
99
129
 
130
+ // Pending tool call with spinning indicator
131
+ interface PendingToolCallProps {
132
+ name: string;
133
+ input: Record<string, unknown>;
134
+ }
135
+
136
+ export function PendingToolCall({ name, input }: PendingToolCallProps) {
137
+ // WebFetch: Show "Fetch(url)" with spinner
138
+ if (name === 'WebFetch' && input?.url) {
139
+ const shortUrl = truncate(input.url as string, 60);
140
+ return (
141
+ <Box marginTop={1}>
142
+ <Text color={colors.tool}>
143
+ <InkSpinner type="dots" />
144
+ </Text>
145
+ <Text> Fetch(</Text>
146
+ <Text color={colors.info}>{shortUrl}</Text>
147
+ <Text>)</Text>
148
+ </Box>
149
+ );
150
+ }
151
+
152
+ // Default: Show tool name with spinner
153
+ const shortInput = truncate(JSON.stringify(input), 50);
154
+
155
+ return (
156
+ <Box marginTop={1}>
157
+ <Text color={colors.tool}>
158
+ <InkSpinner type="dots" />
159
+ </Text>
160
+ <Text> {name} </Text>
161
+ <Text color={colors.textMuted}>{shortInput}</Text>
162
+ </Box>
163
+ );
164
+ }
165
+
166
+ interface ToolResultMetadata {
167
+ title?: string;
168
+ subtitle?: string;
169
+ size?: number;
170
+ statusCode?: number;
171
+ contentType?: string;
172
+ duration?: number;
173
+ }
174
+
100
175
  interface ToolResultProps {
101
176
  name: string;
102
177
  success: boolean;
103
178
  output: string;
179
+ metadata?: ToolResultMetadata;
104
180
  }
105
181
 
106
- export function ToolResult({ name, success, output }: ToolResultProps) {
107
- const firstLine = output.split('\n')[0]?.trim() || '';
108
- const displayOutput = firstLine.length > 50 ? firstLine.slice(0, 47) + '...' : firstLine;
182
+ export function ToolResult({ name, success, output, metadata }: ToolResultProps) {
109
183
  const statusColor = success ? colors.success : colors.error;
110
- const statusIcon = success ? icons.success : icons.error;
184
+
185
+ // If metadata has subtitle (e.g., "Received 540.3KB (200 OK)"), show it
186
+ if (metadata?.subtitle) {
187
+ return (
188
+ <Box marginLeft={2}>
189
+ <Text dimColor>
190
+ <Text>{icons.treeEnd}</Text>{' '}
191
+ <Text color={statusColor}>{metadata.subtitle}</Text>
192
+ </Text>
193
+ </Box>
194
+ );
195
+ }
196
+
197
+ // Default: Show first line of output
198
+ const displayOutput = truncate(output.split('\n')[0]?.trim() || '', 50);
111
199
 
112
200
  return (
113
201
  <Box marginLeft={2}>
114
202
  <Text dimColor>
115
- <Text color={statusColor}>{statusIcon}</Text> {name}{' '}
116
- <Text color={colors.textMuted}>{displayOutput}</Text>
203
+ <Text>{icons.treeEnd}</Text> {name}{' '}
204
+ <Text color={statusColor}>{displayOutput}</Text>
117
205
  </Text>
118
206
  </Box>
119
207
  );
@@ -1,22 +1,26 @@
1
1
  /**
2
- * Model Selector Component - Interactive model selection with fuzzy filter
2
+ * Model Selector Component - Interactive model selection from cached providers
3
3
  */
4
- import { useState, useEffect } from 'react';
4
+ import { useState, useEffect, useMemo } from 'react';
5
5
  import { Box, Text, useInput } from 'ink';
6
6
  import TextInput from 'ink-text-input';
7
7
  import { colors, icons } from './theme.js';
8
8
  import { LoadingSpinner } from './Spinner.js';
9
+ import { getProviderStore, type ModelInfo } from '../../providers/store.js';
10
+ import { getProvider } from '../../providers/registry.js';
11
+ import type { ProviderName } from '../../providers/index.js';
9
12
 
10
- interface Model {
11
- id: string;
12
- name: string;
13
+ interface ModelItem {
14
+ providerId: ProviderName;
15
+ providerName: string;
16
+ model: ModelInfo;
13
17
  }
14
18
 
15
19
  interface ModelSelectorProps {
16
20
  currentModel: string;
17
- onSelect: (modelId: string) => void;
21
+ onSelect: (modelId: string, providerId: ProviderName) => void;
18
22
  onCancel: () => void;
19
- listModels: () => Promise<Model[]>;
23
+ listModels: () => Promise<{ id: string; name: string }[]>; // Fallback for current provider
20
24
  }
21
25
 
22
26
  export function ModelSelector({
@@ -25,32 +29,86 @@ export function ModelSelector({
25
29
  onCancel,
26
30
  listModels,
27
31
  }: ModelSelectorProps) {
28
- const [models, setModels] = useState<Model[]>([]);
32
+ const store = getProviderStore();
29
33
  const [loading, setLoading] = useState(true);
30
- const [error, setError] = useState<string | null>(null);
31
34
  const [filter, setFilter] = useState('');
32
35
  const [selectedIndex, setSelectedIndex] = useState(0);
36
+ const [allModels, setAllModels] = useState<ModelItem[]>([]);
33
37
 
38
+ // Load models from cache
34
39
  useEffect(() => {
35
- const fetchModels = async () => {
36
- try {
37
- const result = await listModels();
38
- setModels(result);
39
- } catch (err) {
40
- setError(err instanceof Error ? err.message : 'Failed to fetch models');
41
- } finally {
42
- setLoading(false);
40
+ const loadModels = async () => {
41
+ const connectedProviders = store.getConnectedProviders();
42
+ const items: ModelItem[] = [];
43
+
44
+ for (const providerId of connectedProviders) {
45
+ const cachedModels = store.getModels(providerId);
46
+ const providerDef = getProvider(providerId);
47
+ const providerName = providerDef?.name || providerId;
48
+
49
+ for (const model of cachedModels) {
50
+ items.push({
51
+ providerId,
52
+ providerName,
53
+ model,
54
+ });
55
+ }
43
56
  }
57
+
58
+ // If no cached models, fallback to listModels for current provider
59
+ if (items.length === 0) {
60
+ try {
61
+ const models = await listModels();
62
+ for (const model of models) {
63
+ items.push({
64
+ providerId: 'anthropic' as ProviderName, // Default, will be overridden
65
+ providerName: 'Current Provider',
66
+ model,
67
+ });
68
+ }
69
+ } catch {
70
+ // Ignore errors
71
+ }
72
+ }
73
+
74
+ setAllModels(items);
75
+ setLoading(false);
44
76
  };
45
- fetchModels();
46
- }, [listModels]);
47
-
48
- // Fuzzy filter models
49
- const filtered = models.filter(
50
- (m) =>
51
- m.id.toLowerCase().includes(filter.toLowerCase()) ||
52
- m.name.toLowerCase().includes(filter.toLowerCase())
53
- );
77
+
78
+ loadModels();
79
+ }, [store, listModels]);
80
+
81
+ // Filter models
82
+ const filterLower = filter.toLowerCase();
83
+ const filtered = useMemo(() => {
84
+ return allModels.filter(
85
+ (item) =>
86
+ item.model.id.toLowerCase().includes(filterLower) ||
87
+ item.model.name.toLowerCase().includes(filterLower) ||
88
+ item.providerName.toLowerCase().includes(filterLower)
89
+ );
90
+ }, [allModels, filterLower]);
91
+
92
+ // Group by provider for display
93
+ const groupedModels = useMemo(() => {
94
+ const groups: Record<string, ModelItem[]> = {};
95
+ for (const item of filtered) {
96
+ if (!groups[item.providerId]) {
97
+ groups[item.providerId] = [];
98
+ }
99
+ groups[item.providerId].push(item);
100
+ }
101
+ return groups;
102
+ }, [filtered]);
103
+
104
+ // Flat list for navigation
105
+ const flatList = useMemo(() => {
106
+ const items: ModelItem[] = [];
107
+ for (const providerId of Object.keys(groupedModels)) {
108
+ items.push(...groupedModels[providerId]);
109
+ }
110
+ return items;
111
+ }, [groupedModels]);
54
112
 
55
113
  // Reset selection when filter changes
56
114
  useEffect(() => {
@@ -62,10 +120,11 @@ export function ModelSelector({
62
120
  if (key.upArrow) {
63
121
  setSelectedIndex((i) => Math.max(0, i - 1));
64
122
  } else if (key.downArrow) {
65
- setSelectedIndex((i) => Math.min(filtered.length - 1, i + 1));
123
+ setSelectedIndex((i) => Math.min(flatList.length - 1, i + 1));
66
124
  } else if (key.return) {
67
- if (filtered.length > 0) {
68
- onSelect(filtered[selectedIndex].id);
125
+ if (flatList.length > 0) {
126
+ const selected = flatList[selectedIndex];
127
+ onSelect(selected.model.id, selected.providerId);
69
128
  }
70
129
  } else if (key.escape) {
71
130
  onCancel();
@@ -81,43 +140,99 @@ export function ModelSelector({
81
140
  );
82
141
  }
83
142
 
84
- if (error) {
85
- onCancel();
86
- return null;
87
- }
143
+ // Count providers and models
144
+ const providerCount = Object.keys(groupedModels).length;
145
+ const modelCount = flatList.length;
88
146
 
89
- const maxVisible = 8;
147
+ // Calculate visible window
148
+ const maxVisible = 10;
90
149
  const startIndex = Math.max(
91
150
  0,
92
- Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible)
151
+ Math.min(selectedIndex - Math.floor(maxVisible / 2), flatList.length - maxVisible)
93
152
  );
94
- const visibleModels = filtered.slice(startIndex, startIndex + maxVisible);
153
+ const endIndex = Math.min(startIndex + maxVisible, flatList.length);
154
+
155
+ // Build visible items with provider headers
156
+ let currentIdx = 0;
157
+ const renderItems: Array<{ type: 'header' | 'model'; content: string; item?: ModelItem }> = [];
158
+
159
+ for (const providerId of Object.keys(groupedModels)) {
160
+ const models = groupedModels[providerId];
161
+ const firstIdx = currentIdx;
162
+ const lastIdx = currentIdx + models.length - 1;
163
+
164
+ // Check if any model from this provider is in visible range
165
+ if (lastIdx >= startIndex && firstIdx < endIndex) {
166
+ // Add header if first visible item is from this provider
167
+ const providerDef = getProvider(providerId as ProviderName);
168
+ const connection = store.getConnection(providerId as ProviderName);
169
+ const headerText = `${providerDef?.name || providerId}${connection ? ` (${connection.method})` : ''}:`;
170
+
171
+ // Only add header if we're showing models from this provider
172
+ const visibleModelsFromProvider = models.filter((_, i) => {
173
+ const globalIdx = currentIdx + i;
174
+ return globalIdx >= startIndex && globalIdx < endIndex;
175
+ });
176
+
177
+ if (visibleModelsFromProvider.length > 0 && (firstIdx >= startIndex || renderItems.length === 0)) {
178
+ renderItems.push({ type: 'header', content: headerText });
179
+ }
180
+
181
+ for (let i = 0; i < models.length; i++) {
182
+ const globalIdx = currentIdx + i;
183
+ if (globalIdx >= startIndex && globalIdx < endIndex) {
184
+ renderItems.push({ type: 'model', content: '', item: models[i] });
185
+ }
186
+ }
187
+ }
188
+
189
+ currentIdx += models.length;
190
+ }
95
191
 
96
192
  return (
97
193
  <Box flexDirection="column">
98
- <Box>
99
- <Text color={colors.primary}>{icons.prompt} </Text>
100
- <TextInput
101
- value={filter}
102
- onChange={setFilter}
103
- placeholder="Type to filter models..."
104
- />
194
+ <Text color={colors.primary} bold>
195
+ Select Model
196
+ </Text>
197
+
198
+ <Box marginTop={1}>
199
+ <Text color={colors.textMuted}>{icons.prompt} </Text>
200
+ <TextInput value={filter} onChange={setFilter} placeholder="Filter models..." />
105
201
  </Box>
202
+
106
203
  <Box flexDirection="column" marginTop={1}>
107
- {visibleModels.length === 0 ? (
108
- <Text color={colors.textMuted}>No models match "{filter}"</Text>
204
+ {flatList.length === 0 ? (
205
+ <Box flexDirection="column">
206
+ <Text color={colors.textMuted}>No cached models.</Text>
207
+ <Text color={colors.textMuted}>Use /provider to connect and cache models.</Text>
208
+ </Box>
109
209
  ) : (
110
- visibleModels.map((m, i) => {
111
- const actualIndex = startIndex + i;
112
- const isSelected = actualIndex === selectedIndex;
113
- const isCurrent = m.id === currentModel;
210
+ renderItems.map((renderItem, i) => {
211
+ if (renderItem.type === 'header') {
212
+ return (
213
+ <Text key={`header-${i}`} color={colors.textSecondary}>
214
+ {renderItem.content}
215
+ </Text>
216
+ );
217
+ }
218
+
219
+ const item = renderItem.item!;
220
+ const globalIndex = flatList.findIndex(
221
+ (f) => f.providerId === item.providerId && f.model.id === item.model.id
222
+ );
223
+ const isSelected = globalIndex === selectedIndex;
224
+ const isCurrent = item.model.id === currentModel;
225
+
114
226
  return (
115
- <Box key={m.id}>
227
+ <Box key={`${item.providerId}-${item.model.id}`} paddingLeft={2}>
116
228
  <Text color={isSelected ? colors.primary : colors.textMuted}>
117
229
  {isSelected ? icons.arrow : ' '}
118
230
  </Text>
231
+ <Text color={isCurrent ? colors.primary : colors.textMuted}>
232
+ {isCurrent ? icons.radio : icons.radioEmpty}
233
+ </Text>
119
234
  <Text color={isSelected ? colors.text : colors.textSecondary} bold={isSelected}>
120
- {m.name}
235
+ {' '}{item.model.name || item.model.id}
121
236
  </Text>
122
237
  {isCurrent && <Text color={colors.success}> (current)</Text>}
123
238
  </Box>
@@ -125,9 +240,11 @@ export function ModelSelector({
125
240
  })
126
241
  )}
127
242
  </Box>
243
+
128
244
  <Box marginTop={1}>
129
245
  <Text color={colors.textMuted}>
130
- {filtered.length} models · ↑↓ navigate · Enter select · Esc cancel
246
+ {providerCount} provider{providerCount !== 1 ? 's' : ''} · {modelCount} model
247
+ {modelCount !== 1 ? 's' : ''} · ↑↓ navigate · Enter select · Esc cancel
131
248
  </Text>
132
249
  </Box>
133
250
  </Box>