bingocode 1.1.65 → 1.1.67
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 +1 -1
- package/src/cli/ProviderPanel.tsx +210 -174
- package/src/components/HelpV2/General.tsx +16 -5
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +11 -2
- package/src/manager/CliMenuManager.tsx +244 -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 +18 -18
- package/src/server/ensureSingletonLocalServer.ts +2 -2
- 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'
|
|
@@ -102,13 +102,13 @@ presets:
|
|
|
102
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
106
|
modelsUrl: https://api.deepseek.com/v1/models
|
|
107
107
|
modelsAuthStyle: bearer
|
|
108
108
|
modelsDataPath: data
|
|
109
109
|
fields:
|
|
110
110
|
- key: name
|
|
111
|
-
label: Provider
|
|
111
|
+
label: Provider Nickname
|
|
112
112
|
required: true
|
|
113
113
|
secret: false
|
|
114
114
|
placeholder: 'e.g. My DeepSeek'
|
|
@@ -129,7 +129,7 @@ presets:
|
|
|
129
129
|
modelsDataPath: data
|
|
130
130
|
fields:
|
|
131
131
|
- key: name
|
|
132
|
-
label: Provider
|
|
132
|
+
label: Provider Nickname
|
|
133
133
|
required: true
|
|
134
134
|
secret: false
|
|
135
135
|
placeholder: 'e.g. My GLM'
|
|
@@ -137,7 +137,7 @@ presets:
|
|
|
137
137
|
label: API Key
|
|
138
138
|
required: true
|
|
139
139
|
secret: true
|
|
140
|
-
placeholder: '
|
|
140
|
+
placeholder: 'Zhipu API Key (glm-5.1)'
|
|
141
141
|
|
|
142
142
|
- id: kimi
|
|
143
143
|
name: Kimi
|
|
@@ -150,7 +150,7 @@ presets:
|
|
|
150
150
|
modelsDataPath: data
|
|
151
151
|
fields:
|
|
152
152
|
- key: name
|
|
153
|
-
label: Provider
|
|
153
|
+
label: Provider Nickname
|
|
154
154
|
required: true
|
|
155
155
|
secret: false
|
|
156
156
|
placeholder: 'e.g. My Kimi'
|
|
@@ -171,7 +171,7 @@ presets:
|
|
|
171
171
|
modelsDataPath: data
|
|
172
172
|
fields:
|
|
173
173
|
- key: name
|
|
174
|
-
label: Provider
|
|
174
|
+
label: Provider Nickname
|
|
175
175
|
required: true
|
|
176
176
|
secret: false
|
|
177
177
|
placeholder: 'e.g. My MiniMax'
|
|
@@ -192,7 +192,7 @@ presets:
|
|
|
192
192
|
modelsDataPath: data
|
|
193
193
|
fields:
|
|
194
194
|
- key: name
|
|
195
|
-
label: Provider
|
|
195
|
+
label: Provider Nickname
|
|
196
196
|
required: true
|
|
197
197
|
secret: false
|
|
198
198
|
placeholder: 'e.g. My Custom Provider'
|
|
@@ -205,4 +205,4 @@ presets:
|
|
|
205
205
|
label: API Key
|
|
206
206
|
required: false
|
|
207
207
|
secret: true
|
|
208
|
-
placeholder: '
|
|
208
|
+
placeholder: '(Optional) API Key'
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
} catch (e) { lastErr = e; }
|
|
40
40
|
await new Promise(r => setTimeout(r, HEALTH_RETRY_MS));
|
|
41
41
|
}
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error(`Health check timeout: ${lastErr?.message || 'unknown'}`);
|
|
43
43
|
}
|
|
44
44
|
function resolveBunPath() {
|
|
45
45
|
const fromEnv = process.env.BUN_PATH;
|
|
46
46
|
if (fromEnv) return fromEnv;
|
|
47
47
|
const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
48
48
|
if (r.status === 0) return 'bun';
|
|
49
|
-
throw new Error('
|
|
49
|
+
throw new Error('Bun not detected. Please install https://bun.sh or set BUN_PATH');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async function acquireLease(): Promise<string> {
|