bingocode 1.1.65 → 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.
@@ -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
- // 按终端显示宽度截断(中文/全角字符占 2 列,ASCII 1 列)
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
- // Kbd(去掉多余空格,保持紧凑视觉)
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(默认 dim
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 line = width ? repeatChar('─', Math.max(0, width - (pad ? 2 : 0))) : '────────────────────────────────────────────────────────────────';
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(更灵活,支持 title 插槽、最小/最大宽度,borderStyle=undefined 时无边框)
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
- // 只有明确传入 borderStyle 且未设置 noBorder 时才渲染边框
76
- if (!noBorder && borderStyle !== undefined) props.borderStyle = borderStyle;
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
- // Fallback top(更紧凑)
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(更紧凑的菜单显示、右侧提示会截断、secondary menu 宽度限制)
175
+ // BottomBar (Compact menu display, right side hints truncated, secondary menu width limited)
114
176
  export const BottomBar: React.FC<{
115
- width?: number;
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 = 60, height = 3, menuItems, page, navIndex, tips, secondaryMenu }) => {
123
- // 精确计算左侧菜单实际占用宽度:prefix(1) + 空格(1) + label + marginRight(2)
124
- // 中文字符宽度按 2 计算,ASCII 1 计算
125
- const charWidth = (s: string) => [...s].reduce((acc, c) => acc + (c.charCodeAt(0) > 127 ? 2 : 1), 0);
126
- const leftActual = menuItems.reduce((acc, it) => acc + 1 + 1 + charWidth(it.label) + 2, 0);
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
- // TopBarlogo + 工具栏水平排列,增加 ready 状态与占位)
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(label 固定宽度,便于列对齐)
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.padEnd(labelWidth, ' ');
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
- // 仅在 ready 时读取配置与信任状态
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
- // 主题名(仅在 ready 时访问全局配置)
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
- {/* 右侧:UI 状态合并显示 */}
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:选模型, 1:填key
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
- // 主界面 LIST 模式
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
- // 步骤0: 主模型选择
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 不能为空'); return; }
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('该模型已添加过,如要更换Key请在列表编辑。'); return; }
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 管理器 ↑↓切换 s激活 n新增 e编辑Key d删除 q退出</Text>
136
- <Text color="cyan">APIKey获取链接:https://mlaas.games.com/auth/token</Text>
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">无 provider,请按 n 新建…</Text> :
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>选择模型与输入API Key:</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">↑↓选择,回车下一步,q返回</Text>
164
+ <Text color="gray">↑↓ select, Enter next, q back</Text>
165
165
  </>
166
166
  : <>
167
- <Text>已选模型:{MODEL_OPTIONS[modelSelectIdx]}</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="请输入API Key"
172
+ placeholder="Please enter API Key"
173
173
  />
174
- <Text color="gray">输入后回车提交,q返回</Text>
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
- {/* 编辑key */}
181
+ {/* Edit key */}
182
182
  {mode === 'editKey' && (
183
183
  <Box flexDirection="column">
184
- <Text>输入新的 API Key:</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="API Key"
196
+ placeholder="New API Key"
197
197
  />
198
- <Text>回车确认,q取消</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
- 确认删除 "{list[selectedIdx]?.name}" provider (y确认 / q取消)
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 详情</Text>
215
- <Text>名称: {detail.name}</Text>
214
+ <Text color="cyan">Provider Details</Text>
215
+ <Text>Name: {detail.name}</Text>
216
216
  <Text>BaseURL: {detail.baseUrl}</Text>
217
- <Text>API格式: {detail.apiFormat}</Text>
218
- <Text>主模型: {detail.models?.main || '-'}</Text>
219
- <Text>备注: {detail.notes || '-'}</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 ? '✓ 已是当前' : '设为当前', value: '__set' },
224
- { label: '测试连通性', value: '__test' },
225
- { label: '编辑 Key', value: '__editKey' },
226
- { label: '删除', value: '__delete' },
227
- { label: '← 返回列表', value: '__back' },
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 ? `连通性正常,延迟 ${r.latencyMs || 0}ms` : `失败:${r.message || '未知错误'}`);
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
- {/* 预设新增 Provider */}
250
+ {/* Apply Preset Add Provider */}
251
251
  {mode === 'applyPreset' && (
252
252
  <Box flexDirection="column">
253
- <Text color="cyan">从预设新增 Provider</Text>
253
+ <Text color="cyan">Add Provider from Preset</Text>
254
254
  {!presets.length ? (
255
- <Text color="gray">暂无可用预设,按 q 返回。</Text>
255
+ <Text color="gray">No presets available. Press 'q' to back.</Text>
256
256
  ) : (
257
257
  <>
258
- <Text>选择一个预设:</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); // 直接跳到 Key 输入
272
+ setAddStep(1); // Jump to Key input
273
273
  }}
274
274
  />
275
- <Text color="gray">回车选择预设,q 返回</Text>
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' 直接映射到顶层字段,其余存入 extra.<key>
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: 相对于 baseUrl 的模型列表路径,空字符串表示不支持动态拉取
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: 响应 JSON 中模型数组的字段名(几乎总是 'data'
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
- # 模型列表走 OpenAI 端(/anthropic 路径下无 /v1/models
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: '智谱 API Key (glm-5.1)'
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: '(可选)API Key'
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(`健康检查超时:${lastErr?.message || 'unknown'}`);
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('未检测到 bun,请安装 https://bun.sh 或设置 BUN_PATH');
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> {