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
|
@@ -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=
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
59
|
-
const
|
|
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
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
116
|
-
<Text color={
|
|
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
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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<
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
123
|
+
setSelectedIndex((i) => Math.min(flatList.length - 1, i + 1));
|
|
66
124
|
} else if (key.return) {
|
|
67
|
-
if (
|
|
68
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
143
|
+
// Count providers and models
|
|
144
|
+
const providerCount = Object.keys(groupedModels).length;
|
|
145
|
+
const modelCount = flatList.length;
|
|
88
146
|
|
|
89
|
-
|
|
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),
|
|
151
|
+
Math.min(selectedIndex - Math.floor(maxVisible / 2), flatList.length - maxVisible)
|
|
93
152
|
);
|
|
94
|
-
const
|
|
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
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
{
|
|
108
|
-
<
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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>
|