bingocode 1.1.64 → 1.1.66
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/package.json +2 -1
- package/src/cli/ProviderPanel.tsx +193 -174
- package/src/components/HelpV2/General.tsx +16 -5
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +11 -2
- package/src/manager/CliMenuManager.tsx +230 -207
- package/src/manager/CliMenuUi.tsx +87 -27
- package/src/manager/TopToolbar.tsx +6 -6
- package/src/server/cli/providersMenu.tsx +46 -46
- package/src/server/config/providers.yaml +25 -24
- package/src/server/ensureSingletonLocalServer.ts +2 -2
- package/src/server/services/providerService.ts +18 -11
- package/src/utils/config.ts +3 -0
|
@@ -2,9 +2,9 @@ import React, { memo } from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Utils
|
|
6
6
|
const repeatChar = (ch: string, n: number) => ch.repeat(Math.max(0, n));
|
|
7
|
-
//
|
|
7
|
+
// Truncate by terminal display width
|
|
8
8
|
const charDisplayWidth = (c: string) => c.charCodeAt(0) > 127 ? 2 : 1;
|
|
9
9
|
const displayWidth = (s: string) => [...s].reduce((acc, c) => acc + charDisplayWidth(c), 0);
|
|
10
10
|
const truncate = (s: string, maxCols: number) => {
|
|
@@ -19,34 +19,45 @@ const truncate = (s: string, maxCols: number) => {
|
|
|
19
19
|
return out;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Padds a string to a target width, accounting for CJK characters.
|
|
24
|
+
*/
|
|
25
|
+
export const safePadEnd = (s: string, targetWidth: number, fillChar = ' ') => {
|
|
26
|
+
const currentWidth = displayWidth(s);
|
|
27
|
+
if (currentWidth >= targetWidth) return s;
|
|
28
|
+
return s + fillChar.repeat(targetWidth - currentWidth);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Kbd (keep compact visual)
|
|
23
32
|
export const Kbd: React.FC<{ children: React.ReactNode }> = memo(({ children }) => (
|
|
24
33
|
<Text color="black" backgroundColor="white">{String(children)}</Text>
|
|
25
34
|
));
|
|
26
35
|
|
|
27
|
-
// Hint
|
|
36
|
+
// Hint (default dim)
|
|
28
37
|
export const Hint: React.FC<{ children: React.ReactNode; dim?: boolean }> = memo(({ children, dim = true }) => (
|
|
29
38
|
<Text dimColor={dim}>{children}</Text>
|
|
30
39
|
));
|
|
31
40
|
|
|
32
|
-
// Title
|
|
41
|
+
// Title (keep configurable color)
|
|
33
42
|
export const Title: React.FC<{ children: React.ReactNode; color?: string }> = memo(({ children, color = 'magenta' }) => (
|
|
34
43
|
<Text color={color}>{children}</Text>
|
|
35
44
|
));
|
|
36
45
|
|
|
37
|
-
// Divider
|
|
38
|
-
export const Divider: React.FC<{ width?: number; pad?: boolean }> = memo(({ width, pad = false }) => {
|
|
39
|
-
const
|
|
46
|
+
// Divider (avoid fixed length overflow)
|
|
47
|
+
export const Divider: React.FC<{ width?: number; pad?: boolean }> = memo(({ width = 80, pad = false }) => {
|
|
48
|
+
const actualWidth = Math.max(10, width - (pad ? 2 : 0));
|
|
49
|
+
const line = '─'.repeat(actualWidth);
|
|
40
50
|
return <Text dimColor>{pad ? ` ${line} ` : line}</Text>;
|
|
41
51
|
});
|
|
42
52
|
|
|
43
|
-
// Panel
|
|
53
|
+
// Panel (flexbox-based layout, supports title slot/min-max width)
|
|
44
54
|
export const Panel: React.FC<{
|
|
45
55
|
width?: number;
|
|
46
56
|
height?: number;
|
|
47
57
|
minWidth?: number;
|
|
48
58
|
maxWidth?: number;
|
|
49
59
|
borderStyle?: 'round' | 'single' | 'double' | 'bold' | 'classic' | undefined;
|
|
60
|
+
borderColor?: string;
|
|
50
61
|
noBorder?: boolean;
|
|
51
62
|
paddingX?: number;
|
|
52
63
|
paddingY?: number;
|
|
@@ -59,6 +70,7 @@ export const Panel: React.FC<{
|
|
|
59
70
|
minWidth,
|
|
60
71
|
maxWidth,
|
|
61
72
|
borderStyle,
|
|
73
|
+
borderColor = 'gray',
|
|
62
74
|
noBorder = false,
|
|
63
75
|
paddingX = 1,
|
|
64
76
|
paddingY = 0,
|
|
@@ -72,8 +84,11 @@ export const Panel: React.FC<{
|
|
|
72
84
|
marginY,
|
|
73
85
|
flexDirection: 'column',
|
|
74
86
|
};
|
|
75
|
-
//
|
|
76
|
-
if (!noBorder && borderStyle !== undefined)
|
|
87
|
+
// Render border only if borderStyle is defined and noBorder is false
|
|
88
|
+
if (!noBorder && borderStyle !== undefined) {
|
|
89
|
+
props.borderStyle = borderStyle;
|
|
90
|
+
props.borderColor = borderColor;
|
|
91
|
+
}
|
|
77
92
|
if (typeof width === 'number') props.width = width;
|
|
78
93
|
if (typeof height === 'number') props.height = height;
|
|
79
94
|
if (typeof minWidth === 'number') props.minWidth = minWidth;
|
|
@@ -87,7 +102,54 @@ export const Panel: React.FC<{
|
|
|
87
102
|
);
|
|
88
103
|
});
|
|
89
104
|
|
|
90
|
-
//
|
|
105
|
+
// StateDisplay (Standardized loading/error/empty state)
|
|
106
|
+
export const StateDisplay: React.FC<{
|
|
107
|
+
type: 'loading' | 'error' | 'empty';
|
|
108
|
+
message?: string;
|
|
109
|
+
onRetry?: () => void;
|
|
110
|
+
}> = memo(({ type, message, onRetry }) => {
|
|
111
|
+
const configs = {
|
|
112
|
+
loading: { icon: '⏳', color: 'yellow', defaultMsg: 'Loading...' },
|
|
113
|
+
error: { icon: '❌', color: 'red', defaultMsg: 'Error occurred' },
|
|
114
|
+
empty: { icon: '📭', color: 'gray', defaultMsg: 'No data' },
|
|
115
|
+
};
|
|
116
|
+
const { icon, color, defaultMsg } = configs[type];
|
|
117
|
+
return (
|
|
118
|
+
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1}>
|
|
119
|
+
<Text color={color}>{icon} {message || defaultMsg}</Text>
|
|
120
|
+
{type === 'error' && onRetry && (
|
|
121
|
+
<Box marginTop={1}>
|
|
122
|
+
<Text dimColor>Press </Text>
|
|
123
|
+
<Kbd>R</Kbd>
|
|
124
|
+
<Text dimColor> to retry</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
)}
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ScrollBar (Simple ASCII scrollbar)
|
|
132
|
+
export const ScrollBar: React.FC<{
|
|
133
|
+
total: number;
|
|
134
|
+
offset: number;
|
|
135
|
+
height: number;
|
|
136
|
+
}> = memo(({ total, offset, height }) => {
|
|
137
|
+
if (total <= height) return null;
|
|
138
|
+
const progress = offset / (total - height);
|
|
139
|
+
const thumbPos = Math.floor(progress * (height - 1));
|
|
140
|
+
|
|
141
|
+
const bar = Array.from({ length: height }, (_, i) => (i === thumbPos ? '█' : '┃'));
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Box flexDirection="column" position="absolute" right={0}>
|
|
145
|
+
{bar.map((char, i) => (
|
|
146
|
+
<Text key={i} dimColor>{char}</Text>
|
|
147
|
+
))}
|
|
148
|
+
</Box>
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Fallback top (compact)
|
|
91
153
|
export const FallbackTop = memo(({ ip }: { ip?: string }) => (
|
|
92
154
|
<Box flexDirection="column">
|
|
93
155
|
<Text>Welcome Bingo Code</Text>
|
|
@@ -102,7 +164,7 @@ export const FallbackTop = memo(({ ip }: { ip?: string }) => (
|
|
|
102
164
|
</Box>
|
|
103
165
|
));
|
|
104
166
|
|
|
105
|
-
//
|
|
167
|
+
// Types
|
|
106
168
|
export type SecondaryMenuItem = { label: string; value: string };
|
|
107
169
|
export type SecondaryMenu = {
|
|
108
170
|
title: string;
|
|
@@ -110,22 +172,20 @@ export type SecondaryMenu = {
|
|
|
110
172
|
onSelect: (item: SecondaryMenuItem) => void;
|
|
111
173
|
} | null;
|
|
112
174
|
|
|
113
|
-
// BottomBar
|
|
175
|
+
// BottomBar (Compact menu display, right side hints truncated, secondary menu width limited)
|
|
114
176
|
export const BottomBar: React.FC<{
|
|
115
|
-
width
|
|
177
|
+
width: number;
|
|
116
178
|
height?: number;
|
|
117
179
|
menuItems: { label: string; value: string }[];
|
|
118
180
|
page: string | null;
|
|
119
181
|
navIndex: number;
|
|
120
182
|
tips: string;
|
|
121
183
|
secondaryMenu: SecondaryMenu;
|
|
122
|
-
}> = memo(({ width
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Panel paddingX=1 占去两侧各1,Box width=width-2,左侧Box不设width(自然宽),右侧需要精确限制
|
|
128
|
-
// 留出 4 字符作为两侧 padding 和分隔缓冲
|
|
184
|
+
}> = memo(({ width, height = 3, menuItems, page, navIndex, tips, secondaryMenu }) => {
|
|
185
|
+
// Calculate left menu actual display width
|
|
186
|
+
const leftActual = menuItems.reduce((acc, it) => acc + 1 + 1 + displayWidth(it.label) + 2, 0);
|
|
187
|
+
// Panel paddingX=1, Box width=width-2. Left box is natural width, right needs precise limit.
|
|
188
|
+
// Leave 4 characters for padding and separator buffer.
|
|
129
189
|
const rightSpace = Math.max(10, width - 4 - leftActual - 2);
|
|
130
190
|
return (
|
|
131
191
|
<Panel width={width} height={height} borderStyle="round" paddingX={1} paddingY={0}>
|
|
@@ -165,7 +225,7 @@ export const BottomBar: React.FC<{
|
|
|
165
225
|
);
|
|
166
226
|
});
|
|
167
227
|
|
|
168
|
-
// Chip
|
|
228
|
+
// Chip (Semantic colors, support optional margin)
|
|
169
229
|
export const Chip: React.FC<{
|
|
170
230
|
label: string;
|
|
171
231
|
value?: string | number;
|
|
@@ -189,14 +249,14 @@ export const Chip: React.FC<{
|
|
|
189
249
|
);
|
|
190
250
|
});
|
|
191
251
|
|
|
192
|
-
// ChipRow
|
|
252
|
+
// ChipRow (Support wrap and auto fold)
|
|
193
253
|
export const ChipRow: React.FC<{ children: React.ReactNode }> = memo(({ children }) => (
|
|
194
254
|
<Box flexDirection="row" flexWrap="wrap" alignItems="center">
|
|
195
255
|
{children as any}
|
|
196
256
|
</Box>
|
|
197
257
|
));
|
|
198
258
|
|
|
199
|
-
// TopBar
|
|
259
|
+
// TopBar (logo + toolbar horizontal arrangement, with ready state and placeholder)
|
|
200
260
|
export const TopBar: React.FC<{
|
|
201
261
|
ready: boolean;
|
|
202
262
|
page: string | null;
|
|
@@ -217,7 +277,7 @@ export const TopBar: React.FC<{
|
|
|
217
277
|
</Panel>
|
|
218
278
|
));
|
|
219
279
|
|
|
220
|
-
// InfoPair
|
|
280
|
+
// InfoPair (Label fixed width for column alignment)
|
|
221
281
|
export const InfoPair: React.FC<{ label: string; value: string; labelColor?: string; valueColor?: string; labelWidth?: number }> = memo(({
|
|
222
282
|
label,
|
|
223
283
|
value,
|
|
@@ -225,7 +285,7 @@ export const InfoPair: React.FC<{ label: string; value: string; labelColor?: str
|
|
|
225
285
|
valueColor,
|
|
226
286
|
labelWidth = 12
|
|
227
287
|
}) => {
|
|
228
|
-
const paddedLabel = label
|
|
288
|
+
const paddedLabel = safePadEnd(label, labelWidth);
|
|
229
289
|
return (
|
|
230
290
|
<Text>
|
|
231
291
|
<Text color={labelColor}>{paddedLabel} </Text>
|
|
@@ -5,7 +5,7 @@ import { Chip, ChipRow } from './CliMenuUi.tsx';
|
|
|
5
5
|
import { useTheme } from '../components/design-system/ThemeProvider.js';
|
|
6
6
|
import { getGlobalConfig, getCurrentProjectConfig, isPathTrusted, checkHasTrustDialogAccepted } from '../utils/config.ts';
|
|
7
7
|
import { getCwd } from '../utils/cwd.js';
|
|
8
|
-
//
|
|
8
|
+
// Update: Import respectively according to the new interface
|
|
9
9
|
import type { ClawdPose } from '../components/LogoV2/Clawd.tsx';
|
|
10
10
|
import { Clawd } from '../components/LogoV2/Clawd.tsx';
|
|
11
11
|
import { AnimatedClawd } from '../components/LogoV2/AnimatedClawd.tsx';
|
|
@@ -33,7 +33,7 @@ function ellipsisPath(p: string, keep = 2) {
|
|
|
33
33
|
export const TopToolbar: React.FC<Props> = memo(({ ready, page, animEnabled, tipsEnabled }) => {
|
|
34
34
|
const [theme] = useTheme();
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Only read config and trust status when ready
|
|
37
37
|
const { cwd, trustAccepted, trustedPath, projectName } = useMemo(() => {
|
|
38
38
|
if (!ready) {
|
|
39
39
|
return { cwd: '', trustAccepted: undefined as undefined|boolean, trustedPath: undefined as undefined|boolean, projectName: '' };
|
|
@@ -54,10 +54,10 @@ export const TopToolbar: React.FC<Props> = memo(({ ready, page, animEnabled, tip
|
|
|
54
54
|
const compact = page !== null;
|
|
55
55
|
const cwdShort = useMemo(() => ellipsisPath(cwd, compact ? 2 : 3), [cwd, compact]);
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// Theme name
|
|
58
58
|
const themeLabel = String(theme || (ready ? (getGlobalConfig()?.theme ?? 'system') : '…'));
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Static Clawd pose
|
|
61
61
|
const clawdPose: ClawdPose = useMemo(() => {
|
|
62
62
|
if (!ready) return 'default';
|
|
63
63
|
if (page === null) return animEnabled ? 'arms-up' : 'default';
|
|
@@ -69,7 +69,7 @@ export const TopToolbar: React.FC<Props> = memo(({ ready, page, animEnabled, tip
|
|
|
69
69
|
return (
|
|
70
70
|
<Box flexDirection="column" minHeight={3}>
|
|
71
71
|
<ChipRow>
|
|
72
|
-
{/*
|
|
72
|
+
{/* Left: Clawd + Core Status */}
|
|
73
73
|
<Box>
|
|
74
74
|
<Box marginRight={2}>
|
|
75
75
|
{animEnabled ? <AnimatedClawd /> : <Clawd pose={clawdPose} />}
|
|
@@ -87,7 +87,7 @@ export const TopToolbar: React.FC<Props> = memo(({ ready, page, animEnabled, tip
|
|
|
87
87
|
)}
|
|
88
88
|
</Box>
|
|
89
89
|
|
|
90
|
-
{/*
|
|
90
|
+
{/* Right: UI Status merged display */}
|
|
91
91
|
<Box>
|
|
92
92
|
<Chip
|
|
93
93
|
label="UI"
|
|
@@ -31,7 +31,7 @@ const FIXED_APIFMT = 'openai_chat';
|
|
|
31
31
|
const ProvidersMenu: React.FC = () => {
|
|
32
32
|
const { exit } = useApp();
|
|
33
33
|
const [modelSelectIdx, setModelSelectIdx] = useState(0);
|
|
34
|
-
const [addStep, setAddStep] = useState(0); // 0
|
|
34
|
+
const [addStep, setAddStep] = useState(0); // 0: Select Model, 1: Fill key
|
|
35
35
|
const [inputKey, setInputKey] = useState('');
|
|
36
36
|
const [addError, setAddError] = useState('');
|
|
37
37
|
const [mode, setMode] = useState<UiMode>('list');
|
|
@@ -40,7 +40,7 @@ const ProvidersMenu: React.FC = () => {
|
|
|
40
40
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
41
41
|
const [editKey, setEditKey] = useState('');
|
|
42
42
|
const [removeConfirm, setRemoveConfirm] = useState(false);
|
|
43
|
-
//
|
|
43
|
+
// Added
|
|
44
44
|
const [msg, setMsg] = useState<string>('');
|
|
45
45
|
const [detail, setDetail] = useState<SavedProvider | null>(null);
|
|
46
46
|
const [presets, setPresets] = useState<any[]>([]);
|
|
@@ -57,7 +57,7 @@ const ProvidersMenu: React.FC = () => {
|
|
|
57
57
|
if (mode !== 'add') { setAddStep(0); setInputKey(''); setAddError(''); setModelSelectIdx(0); }
|
|
58
58
|
}, [mode]);
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Main UI LIST mode
|
|
61
61
|
useInput((inputKey, key) => {
|
|
62
62
|
if (mode === 'list') {
|
|
63
63
|
if (key.downArrow) setSelectedIdx(i => Math.min(list.length - 1, i + 1));
|
|
@@ -88,15 +88,15 @@ const ProvidersMenu: React.FC = () => {
|
|
|
88
88
|
}
|
|
89
89
|
}, { isActive: mode === 'list' || mode === 'removeConfirm' });
|
|
90
90
|
|
|
91
|
-
// ADD
|
|
91
|
+
// ADD mode interaction
|
|
92
92
|
useInput((inputKey, key) => {
|
|
93
93
|
if (mode !== 'add') return;
|
|
94
|
-
//
|
|
94
|
+
// Global Back
|
|
95
95
|
if (inputKey === 'q' || key.escape) {
|
|
96
96
|
setMode('list'); setAddError(''); setInputKey(''); setAddStep(0);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
-
//
|
|
99
|
+
// Step 0: Main Model Selection
|
|
100
100
|
if (addStep === 0) {
|
|
101
101
|
if (key.downArrow) setModelSelectIdx(idx => Math.min(MODEL_OPTIONS.length - 1, idx + 1));
|
|
102
102
|
else if (key.upArrow) setModelSelectIdx(idx => Math.max(0, idx - 1));
|
|
@@ -104,16 +104,16 @@ const ProvidersMenu: React.FC = () => {
|
|
|
104
104
|
}
|
|
105
105
|
}, { isActive: mode === 'add' });
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// Add Form
|
|
108
108
|
const addSubmit = async (keyInput: string) => {
|
|
109
109
|
const selectedModel = MODEL_OPTIONS[modelSelectIdx];
|
|
110
110
|
const presetId = selectedModel.replace(/[^a-zA-Z0-9]/g, '') + '-preset';
|
|
111
111
|
const name = `${selectedModel} Provider`;
|
|
112
|
-
if (!keyInput.trim()) { setAddError('API Key
|
|
112
|
+
if (!keyInput.trim()) { setAddError('API Key cannot be empty'); return; }
|
|
113
113
|
const exists = (await ProviderManager.listProviders()).some(p => p.id === presetId);
|
|
114
|
-
if (exists) { setAddError('
|
|
114
|
+
if (exists) { setAddError('This model has already been added. Edit key in the list instead.'); return; }
|
|
115
115
|
setAddError('');
|
|
116
|
-
//
|
|
116
|
+
// Support preset override
|
|
117
117
|
const overrideBase = (global as any).__PM_BASEURL_OVERRIDE__ || FIXED_BASEURL;
|
|
118
118
|
const overrideFmt = (global as any).__PM_APIFMT_OVERRIDE__ || FIXED_APIFMT;
|
|
119
119
|
await ProviderManager.addProvider({
|
|
@@ -132,28 +132,28 @@ const ProvidersMenu: React.FC = () => {
|
|
|
132
132
|
// UI
|
|
133
133
|
return (
|
|
134
134
|
<Box flexDirection="column" margin={1}>
|
|
135
|
-
<Text bold color="cyan">Provider
|
|
136
|
-
<Text color="cyan">
|
|
135
|
+
<Text bold color="cyan">Provider Manager: ↑↓ move · s active · n new · e edit Key · d del · q quit</Text>
|
|
136
|
+
<Text color="cyan">Get API Key: https://mlaas.games.com/auth/token</Text>
|
|
137
137
|
<Newline />
|
|
138
|
-
{/*
|
|
138
|
+
{/* List */}
|
|
139
139
|
{mode === 'list' && (list.length === 0 ?
|
|
140
|
-
<Text color="gray"
|
|
140
|
+
<Text color="gray">No providers found. Press 'n' to add...</Text> :
|
|
141
141
|
list.map((p, idx) => (
|
|
142
142
|
<Box key={p.id}>
|
|
143
143
|
<Text>
|
|
144
144
|
{selectedIdx === idx
|
|
145
145
|
? chalk.bgHex('#ffc300').black(`> ${p.name.padEnd(18)} ${p.baseUrl}`)
|
|
146
146
|
: ` ${p.name.padEnd(18)} ${p.baseUrl}`}
|
|
147
|
-
{activeId === p.id ? chalk.green(' [
|
|
147
|
+
{activeId === p.id ? chalk.green(' [Current]') : ''}
|
|
148
148
|
</Text>
|
|
149
149
|
</Box>
|
|
150
150
|
))
|
|
151
151
|
)}
|
|
152
152
|
|
|
153
|
-
{/*
|
|
153
|
+
{/* Add */}
|
|
154
154
|
{mode === 'add' && (
|
|
155
155
|
<Box flexDirection="column">
|
|
156
|
-
<Text
|
|
156
|
+
<Text>Select Model and Input API Key:</Text>
|
|
157
157
|
{addStep === 0
|
|
158
158
|
? <>
|
|
159
159
|
{MODEL_OPTIONS.map((m, idx) => (
|
|
@@ -161,27 +161,27 @@ const ProvidersMenu: React.FC = () => {
|
|
|
161
161
|
{idx === modelSelectIdx ? '> ' : ' '}{m}
|
|
162
162
|
</Text>
|
|
163
163
|
))}
|
|
164
|
-
<Text color="gray"
|
|
164
|
+
<Text color="gray">↑↓ select, Enter next, q back</Text>
|
|
165
165
|
</>
|
|
166
166
|
: <>
|
|
167
|
-
<Text
|
|
167
|
+
<Text>Selected Model: {MODEL_OPTIONS[modelSelectIdx]}</Text>
|
|
168
168
|
<TextInput
|
|
169
169
|
value={inputKey}
|
|
170
170
|
onChange={setInputKey}
|
|
171
171
|
onSubmit={addSubmit}
|
|
172
|
-
placeholder="
|
|
172
|
+
placeholder="Please enter API Key"
|
|
173
173
|
/>
|
|
174
|
-
<Text color="gray"
|
|
174
|
+
<Text color="gray">Enter submit, q back</Text>
|
|
175
175
|
</>
|
|
176
176
|
}
|
|
177
177
|
{addError && <Text color="red">{addError}</Text>}
|
|
178
178
|
</Box>
|
|
179
179
|
)}
|
|
180
180
|
|
|
181
|
-
{/*
|
|
181
|
+
{/* Edit key */}
|
|
182
182
|
{mode === 'editKey' && (
|
|
183
183
|
<Box flexDirection="column">
|
|
184
|
-
<Text
|
|
184
|
+
<Text>Enter New API Key:</Text>
|
|
185
185
|
<TextInput
|
|
186
186
|
value={editKey}
|
|
187
187
|
onChange={setEditKey}
|
|
@@ -193,38 +193,38 @@ const ProvidersMenu: React.FC = () => {
|
|
|
193
193
|
setMode('list'); setEditKey(''); refresh();
|
|
194
194
|
}
|
|
195
195
|
}}
|
|
196
|
-
placeholder="
|
|
196
|
+
placeholder="New API Key"
|
|
197
197
|
/>
|
|
198
|
-
<Text
|
|
198
|
+
<Text>Enter confirm, q cancel</Text>
|
|
199
199
|
</Box>
|
|
200
200
|
)}
|
|
201
201
|
|
|
202
|
-
{/*
|
|
202
|
+
{/* Remove */}
|
|
203
203
|
{mode === 'removeConfirm' && (
|
|
204
204
|
<Box flexDirection="column">
|
|
205
205
|
<Text color="red">
|
|
206
|
-
|
|
206
|
+
Confirm delete "{list[selectedIdx]?.name}" provider? (y confirm / q cancel)
|
|
207
207
|
</Text>
|
|
208
208
|
</Box>
|
|
209
209
|
)}
|
|
210
210
|
|
|
211
|
-
{/* Provider
|
|
211
|
+
{/* Provider detail mode */}
|
|
212
212
|
{mode === 'detail' && detail && (
|
|
213
213
|
<Box flexDirection="column">
|
|
214
|
-
<Text color="cyan">Provider
|
|
215
|
-
<Text
|
|
214
|
+
<Text color="cyan">Provider Details</Text>
|
|
215
|
+
<Text>Name: {detail.name}</Text>
|
|
216
216
|
<Text>BaseURL: {detail.baseUrl}</Text>
|
|
217
|
-
<Text>API
|
|
218
|
-
<Text
|
|
219
|
-
<Text
|
|
217
|
+
<Text>API Format: {detail.apiFormat}</Text>
|
|
218
|
+
<Text>Main Model: {detail.models?.main || '-'}</Text>
|
|
219
|
+
<Text>Notes: {detail.notes || '-'}</Text>
|
|
220
220
|
{msg && <Text color="green">{msg}</Text>}
|
|
221
221
|
<SelectInput
|
|
222
222
|
items={[
|
|
223
|
-
{ label: activeId === detail.id ? '✓
|
|
224
|
-
{ label: '
|
|
225
|
-
{ label: '
|
|
226
|
-
{ label: '
|
|
227
|
-
{ label: '←
|
|
223
|
+
{ label: activeId === detail.id ? '✓ Already Active' : 'Set as Active', value: '__set' },
|
|
224
|
+
{ label: 'Test Connectivity', value: '__test' },
|
|
225
|
+
{ label: 'Edit Key', value: '__editKey' },
|
|
226
|
+
{ label: 'Delete', value: '__delete' },
|
|
227
|
+
{ label: '← Back to List', value: '__back' },
|
|
228
228
|
]}
|
|
229
229
|
onSelect={async it => {
|
|
230
230
|
setMsg('');
|
|
@@ -234,28 +234,28 @@ const ProvidersMenu: React.FC = () => {
|
|
|
234
234
|
if (it.value === '__set' && activeId !== detail.id) {
|
|
235
235
|
await ProviderManager.setCurrentProvider(detail.id);
|
|
236
236
|
setActiveId(detail.id);
|
|
237
|
-
setMsg('
|
|
237
|
+
setMsg('Set as active');
|
|
238
238
|
await refresh();
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
241
|
if (it.value === '__test') {
|
|
242
242
|
const r = await ProviderManager.testProvider(detail);
|
|
243
|
-
setMsg(r.ok ?
|
|
243
|
+
setMsg(r.ok ? `Connectivity OK, latency ${r.latencyMs || 0}ms` : `Failed: ${r.message || 'Unknown error'}`);
|
|
244
244
|
}
|
|
245
245
|
}}
|
|
246
246
|
/>
|
|
247
247
|
</Box>
|
|
248
248
|
)}
|
|
249
249
|
|
|
250
|
-
{/*
|
|
250
|
+
{/* Apply Preset Add Provider */}
|
|
251
251
|
{mode === 'applyPreset' && (
|
|
252
252
|
<Box flexDirection="column">
|
|
253
|
-
<Text color="cyan"
|
|
253
|
+
<Text color="cyan">Add Provider from Preset</Text>
|
|
254
254
|
{!presets.length ? (
|
|
255
|
-
<Text color="gray"
|
|
255
|
+
<Text color="gray">No presets available. Press 'q' to back.</Text>
|
|
256
256
|
) : (
|
|
257
257
|
<>
|
|
258
|
-
<Text
|
|
258
|
+
<Text>Select a Preset:</Text>
|
|
259
259
|
<SelectInput
|
|
260
260
|
items={presets.map((p: any) => ({ label: `${p.name || p.id} ${p.baseUrl || ''}`, value: p.id }))}
|
|
261
261
|
onSelect={async it => {
|
|
@@ -269,10 +269,10 @@ const ProvidersMenu: React.FC = () => {
|
|
|
269
269
|
(global as any).__PM_BASEURL_OVERRIDE__ = presetBase;
|
|
270
270
|
(global as any).__PM_APIFMT_OVERRIDE__ = presetFmt;
|
|
271
271
|
setMode('add');
|
|
272
|
-
setAddStep(1); //
|
|
272
|
+
setAddStep(1); // Jump to Key input
|
|
273
273
|
}}
|
|
274
274
|
/>
|
|
275
|
-
<Text color="gray"
|
|
275
|
+
<Text color="gray">Enter select, q back</Text>
|
|
276
276
|
</>
|
|
277
277
|
)}
|
|
278
278
|
</Box>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
|
|
3
|
-
# Provider
|
|
4
|
-
# fields
|
|
5
|
-
# key: 'name' | 'apiKey' | 'baseUrl'
|
|
6
|
-
# secret: true
|
|
3
|
+
# Provider Preset Configuration
|
|
4
|
+
# fields array declares fields to be filled when adding
|
|
5
|
+
# key: 'name' | 'apiKey' | 'baseUrl' mapped directly to top-level fields, others saved in extra.<key>
|
|
6
|
+
# secret: true uses mask display in frontend
|
|
7
7
|
#
|
|
8
|
-
# modelsUrl:
|
|
8
|
+
# modelsUrl: model list path relative to baseUrl, empty string means dynamic pulling not supported
|
|
9
9
|
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
10
|
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
-
# modelsDataPath:
|
|
11
|
+
# modelsDataPath: record field name for models array in JSON response (almost always 'data')
|
|
12
12
|
|
|
13
13
|
presets:
|
|
14
14
|
- id: official
|
|
@@ -22,7 +22,7 @@ presets:
|
|
|
22
22
|
modelsDataPath: data
|
|
23
23
|
fields:
|
|
24
24
|
- key: name
|
|
25
|
-
label: Provider
|
|
25
|
+
label: Provider Nickname
|
|
26
26
|
required: true
|
|
27
27
|
secret: false
|
|
28
28
|
placeholder: 'e.g. Claude Official'
|
|
@@ -38,7 +38,7 @@ presets:
|
|
|
38
38
|
modelsDataPath: data
|
|
39
39
|
fields:
|
|
40
40
|
- key: name
|
|
41
|
-
label: Provider
|
|
41
|
+
label: Provider Nickname
|
|
42
42
|
required: true
|
|
43
43
|
secret: false
|
|
44
44
|
placeholder: 'e.g. My OpenAI'
|
|
@@ -65,7 +65,7 @@ presets:
|
|
|
65
65
|
modelsDataPath: data
|
|
66
66
|
fields:
|
|
67
67
|
- key: name
|
|
68
|
-
label: Provider
|
|
68
|
+
label: Provider Nickname
|
|
69
69
|
required: true
|
|
70
70
|
secret: false
|
|
71
71
|
placeholder: 'e.g. My Gemini'
|
|
@@ -86,7 +86,7 @@ presets:
|
|
|
86
86
|
modelsDataPath: data
|
|
87
87
|
fields:
|
|
88
88
|
- key: name
|
|
89
|
-
label: Provider
|
|
89
|
+
label: Provider Nickname
|
|
90
90
|
required: true
|
|
91
91
|
secret: false
|
|
92
92
|
placeholder: 'e.g. My Mistral'
|
|
@@ -99,15 +99,16 @@ presets:
|
|
|
99
99
|
- id: deepseek
|
|
100
100
|
name: DeepSeek
|
|
101
101
|
baseUrl: https://api.deepseek.com/anthropic
|
|
102
|
-
apiFormat:
|
|
102
|
+
apiFormat: anthropic
|
|
103
103
|
needsApiKey: true
|
|
104
104
|
websiteUrl: https://platform.deepseek.com
|
|
105
|
-
|
|
105
|
+
# Models list follows OpenAI endpoint (no /v1/models under /anthropic)
|
|
106
|
+
modelsUrl: https://api.deepseek.com/v1/models
|
|
106
107
|
modelsAuthStyle: bearer
|
|
107
108
|
modelsDataPath: data
|
|
108
109
|
fields:
|
|
109
110
|
- key: name
|
|
110
|
-
label: Provider
|
|
111
|
+
label: Provider Nickname
|
|
111
112
|
required: true
|
|
112
113
|
secret: false
|
|
113
114
|
placeholder: 'e.g. My DeepSeek'
|
|
@@ -123,12 +124,12 @@ presets:
|
|
|
123
124
|
apiFormat: openai_chat
|
|
124
125
|
needsApiKey: true
|
|
125
126
|
websiteUrl: https://open.bigmodel.cn
|
|
126
|
-
modelsUrl: /models
|
|
127
|
+
modelsUrl: /v4/models
|
|
127
128
|
modelsAuthStyle: bearer
|
|
128
129
|
modelsDataPath: data
|
|
129
130
|
fields:
|
|
130
131
|
- key: name
|
|
131
|
-
label: Provider
|
|
132
|
+
label: Provider Nickname
|
|
132
133
|
required: true
|
|
133
134
|
secret: false
|
|
134
135
|
placeholder: 'e.g. My GLM'
|
|
@@ -136,11 +137,11 @@ presets:
|
|
|
136
137
|
label: API Key
|
|
137
138
|
required: true
|
|
138
139
|
secret: true
|
|
139
|
-
placeholder: '
|
|
140
|
+
placeholder: 'Zhipu API Key (glm-5.1)'
|
|
140
141
|
|
|
141
142
|
- id: kimi
|
|
142
143
|
name: Kimi
|
|
143
|
-
baseUrl: https://api.moonshot.
|
|
144
|
+
baseUrl: https://api.moonshot.ai/v1
|
|
144
145
|
apiFormat: openai_chat
|
|
145
146
|
needsApiKey: true
|
|
146
147
|
websiteUrl: https://platform.moonshot.cn
|
|
@@ -149,7 +150,7 @@ presets:
|
|
|
149
150
|
modelsDataPath: data
|
|
150
151
|
fields:
|
|
151
152
|
- key: name
|
|
152
|
-
label: Provider
|
|
153
|
+
label: Provider Nickname
|
|
153
154
|
required: true
|
|
154
155
|
secret: false
|
|
155
156
|
placeholder: 'e.g. My Kimi'
|
|
@@ -157,11 +158,11 @@ presets:
|
|
|
157
158
|
label: API Key
|
|
158
159
|
required: true
|
|
159
160
|
secret: true
|
|
160
|
-
placeholder: 'Moonshot API Key'
|
|
161
|
+
placeholder: 'Moonshot API Key (kimi-k2.6)'
|
|
161
162
|
|
|
162
163
|
- id: minimax
|
|
163
164
|
name: MiniMax
|
|
164
|
-
baseUrl: https://api.
|
|
165
|
+
baseUrl: https://api.minimax.io/v1
|
|
165
166
|
apiFormat: openai_chat
|
|
166
167
|
needsApiKey: true
|
|
167
168
|
websiteUrl: https://platform.minimaxi.com
|
|
@@ -170,7 +171,7 @@ presets:
|
|
|
170
171
|
modelsDataPath: data
|
|
171
172
|
fields:
|
|
172
173
|
- key: name
|
|
173
|
-
label: Provider
|
|
174
|
+
label: Provider Nickname
|
|
174
175
|
required: true
|
|
175
176
|
secret: false
|
|
176
177
|
placeholder: 'e.g. My MiniMax'
|
|
@@ -178,7 +179,7 @@ presets:
|
|
|
178
179
|
label: API Key
|
|
179
180
|
required: true
|
|
180
181
|
secret: true
|
|
181
|
-
placeholder: 'MiniMax API Key'
|
|
182
|
+
placeholder: 'MiniMax API Key (MiniMax-M2.7)'
|
|
182
183
|
|
|
183
184
|
- id: custom
|
|
184
185
|
name: Custom
|
|
@@ -191,7 +192,7 @@ presets:
|
|
|
191
192
|
modelsDataPath: data
|
|
192
193
|
fields:
|
|
193
194
|
- key: name
|
|
194
|
-
label: Provider
|
|
195
|
+
label: Provider Nickname
|
|
195
196
|
required: true
|
|
196
197
|
secret: false
|
|
197
198
|
placeholder: 'e.g. My Custom Provider'
|
|
@@ -204,4 +205,4 @@ presets:
|
|
|
204
205
|
label: API Key
|
|
205
206
|
required: false
|
|
206
207
|
secret: true
|
|
207
|
-
placeholder: '
|
|
208
|
+
placeholder: '(Optional) API Key'
|