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.
@@ -3,6 +3,7 @@ import { Box, Text, useApp } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import TextInput from 'ink-text-input';
5
5
  import axios from 'axios';
6
+ import { Panel, Title, Chip, Hint, StateDisplay, ScrollBar, safePadEnd } from '../manager/CliMenuUi.tsx';
6
7
 
7
8
  type ProviderField = {
8
9
  key: string;
@@ -61,6 +62,9 @@ export const ProviderPanel: React.FC<{
61
62
  const [loading, setLoading] = useState(false);
62
63
  const [err, setErr] = useState<string | null>(null);
63
64
 
65
+ // Scrolling
66
+ const [listOffset, setListOffset] = useState(0);
67
+
64
68
  const [providers, setProviders] = useState<Provider[]>([]);
65
69
  const [currentId, setCurrentId] = useState<string | null>(null);
66
70
 
@@ -116,7 +120,7 @@ export const ProviderPanel: React.FC<{
116
120
  setProviders(list || []);
117
121
  setCurrentId(currentId);
118
122
  } catch (e: any) {
119
- setErr(e?.message || '获取 Provider 列表失败');
123
+ setErr(e?.message || 'Failed to fetch provider list');
120
124
  } finally {
121
125
  setLoading(false);
122
126
  }
@@ -160,6 +164,7 @@ export const ProviderPanel: React.FC<{
160
164
  setEditId(null);
161
165
  setEditName('');
162
166
  setEditKey('');
167
+ setListOffset(0);
163
168
  } else {
164
169
  onBack ? onBack() : exit();
165
170
  }
@@ -199,12 +204,12 @@ export const ProviderPanel: React.FC<{
199
204
  };
200
205
  if (extra && Object.keys(extra).length > 0) body.extra = extra;
201
206
  await axios.post(`${base}/api/providers`, body);
202
- setOpMsg(`创建成功 -> ${name}`);
207
+ setOpMsg(`Success -> ${name}`);
203
208
  await loadProviders();
204
209
  setStage('list');
205
210
  } catch (e: any) {
206
- setErr(e?.response?.data?.message || e?.message || '创建失败');
207
- // 回到最后一个字段让用户看到错误,而不是触发再次提交
211
+ setErr(e?.response?.data?.message || e?.message || 'Create failed');
212
+ // Go back to the last field so user sees error instead of re-triggering submit
208
213
  setAddFieldIndex(Math.max(0, addFields.length - 1));
209
214
  setStage('add_input_fields');
210
215
  }
@@ -212,20 +217,20 @@ export const ProviderPanel: React.FC<{
212
217
 
213
218
  const doTest = async (id: string) => {
214
219
  setStage('testing');
215
- setErr(null); setOpMsg('测试中...');
220
+ setErr(null); setOpMsg('Testing...');
216
221
  try {
217
222
  const res = await axios.post(`${base}/api/providers/${encodeURIComponent(id)}/test`);
218
223
  const result = res?.data?.result;
219
224
  const conn = result?.connectivity;
220
225
  if (conn?.success) {
221
- setOpMsg(`连通性正常 -> ${id} (${conn.latencyMs}ms)`);
226
+ setOpMsg(`Connectivity OK -> ${id} (${conn.latencyMs}ms)`);
222
227
  setErr(null);
223
228
  } else {
224
- setErr(`连通性异常: ${conn?.error || '未知错误'}`);
229
+ setErr(`Connectivity error: ${conn?.error || 'Unknown error'}`);
225
230
  setOpMsg(null);
226
231
  }
227
232
  } catch (e: any) {
228
- setErr(e?.response?.data?.message || e?.message || `测试失败 -> ${id}`);
233
+ setErr(e?.response?.data?.message || e?.message || `Test failed -> ${id}`);
229
234
  setOpMsg(null);
230
235
  } finally {
231
236
  if (stage !== 'list') setStage('list');
@@ -241,11 +246,11 @@ export const ProviderPanel: React.FC<{
241
246
  if (name.trim()) updates.name = name.trim();
242
247
  if (apiKey.trim()) updates.apiKey = apiKey.trim();
243
248
  await axios.put(`${base}/api/providers/${encodeURIComponent(id)}`, updates);
244
- setOpMsg(`已更新 Provider -> ${name.trim() || id}`);
249
+ setOpMsg(`Updated Provider -> ${name.trim() || id}`);
245
250
  await loadProviders();
246
251
  setStage('list');
247
252
  } catch (e: any) {
248
- setErr(e?.response?.data?.message || e?.message || '编辑失败');
253
+ setErr(e?.response?.data?.message || e?.message || 'Edit failed');
249
254
  setStage('edit_input_key');
250
255
  }
251
256
  };
@@ -255,51 +260,51 @@ export const ProviderPanel: React.FC<{
255
260
  setErr(null); setOpMsg(null);
256
261
  try {
257
262
  await axios.delete(`${base}/api/providers/${encodeURIComponent(id)}`);
258
- setOpMsg(`已删除 Provider -> ${id}`);
263
+ setOpMsg(`Deleted Provider -> ${id}`);
259
264
  await loadProviders();
260
265
  setStage('list');
261
266
  } catch (e: any) {
262
- setErr(e?.response?.data?.message || e?.message || '删除失败');
267
+ setErr(e?.response?.data?.message || e?.message || 'Delete failed');
263
268
  setStage('list');
264
269
  }
265
270
  };
266
271
 
267
272
  const MAX_LIST = 5;
268
273
 
269
- // 渲染块
274
+ // Render function for main list
270
275
  const renderList = () => {
271
276
  const visibleProviders = providers.slice(0, MAX_LIST);
272
277
  const overflow = providers.length - MAX_LIST;
273
278
  return (
274
- <Box flexDirection="column">
275
- <Text color="cyan">Provider 列表</Text>
276
- {!providers.length && !loading && <Text>暂无 Provider</Text>}
277
- {visibleProviders.map(p => {
278
- const isCur = currentProvider && (currentProvider.id === p.id);
279
- return (
280
- <Text key={p.id} color={isCur ? 'green' : undefined} bold={isCur}>
281
- {p.name || '-'}{isCur ? ' ← 当前' : ''}
282
- </Text>
283
- );
284
- })}
285
- {overflow > 0 && <Text dimColor> ...还有 {overflow} 个</Text>}
286
- {currentProvider && (
287
- <Text dimColor>
288
- 当前 Provider: {currentProvider.name || currentProvider.id}
289
- </Text>
290
- )}
291
- {loading && <Text color="yellow">加载中...</Text>}
279
+ <Box flexDirection="column" flexGrow={1}>
280
+ <Title color="cyan">Provider List</Title>
281
+ {!providers.length && !loading && <StateDisplay type="empty" message="No providers found" />}
282
+ <Box flexDirection="column" marginBottom={1}>
283
+ {visibleProviders.map(p => {
284
+ const isCur = currentProvider && (currentProvider.id === p.id);
285
+ return (
286
+ <Box key={p.id}>
287
+ <Text color={isCur ? 'green' : undefined} bold={isCur}>
288
+ {isCur ? '● ' : ' '}{p.name || '-'}
289
+ {isCur ? <Text dimColor> (Current)</Text> : ''}
290
+ </Text>
291
+ </Box>
292
+ );
293
+ })}
294
+ {overflow > 0 && <Text dimColor> ...and {overflow} more</Text>}
295
+ </Box>
296
+
297
+ {loading && <StateDisplay type="loading" message="Loading..." />}
292
298
 
293
- <Box marginTop={1} flexDirection="column">
299
+ <Box flexDirection="column" flexGrow={1}>
294
300
  <SelectInput
295
301
  items={[
296
- { label: '新增 Provider', value: 'add' },
297
- { label: '编辑 Provider(名称/API Key', value: 'edit' },
298
- { label: '配置槽位(main/haiku/sonnet/opus)', value: 'slots' },
299
- { label: '连通性测试', value: 'test' },
300
- { label: '删除 Provider', value: 'delete' },
301
- { label: '刷新', value: 'refresh' },
302
- { label: '返回主菜单(ESC)', value: 'back' },
302
+ { label: 'Add Provider', value: 'add' },
303
+ { label: 'Edit Provider (Name/Key)', value: 'edit' },
304
+ { label: 'Configure Slots', value: 'slots' },
305
+ { label: 'Connectivity Test', value: 'test' },
306
+ { label: 'Delete Provider', value: 'delete' },
307
+ { label: 'Refresh', value: 'refresh' },
303
308
  ]}
304
309
  onSelect={item => {
305
310
  switch (item.value) {
@@ -308,12 +313,14 @@ export const ProviderPanel: React.FC<{
308
313
  setAddFields([]);
309
314
  setAddFieldValues({});
310
315
  setAddFieldIndex(0);
316
+ setListOffset(0);
311
317
  setStage('add_select_preset');
312
318
  break;
313
319
  case 'edit':
314
320
  setEditId(null);
315
321
  setEditName('');
316
322
  setEditKey('');
323
+ setListOffset(0);
317
324
  setStage('edit_select');
318
325
  break;
319
326
  case 'slots':
@@ -321,34 +328,26 @@ export const ProviderPanel: React.FC<{
321
328
  .then(r => setSlotTable(r.data as Record<string, SlotEntry>))
322
329
  .catch(() => {});
323
330
  setStage('slot_config');
331
+ setListOffset(0);
324
332
  break;
325
333
  case 'test':
326
334
  setStage('test_select');
335
+ setListOffset(0);
327
336
  break;
328
337
  case 'delete':
329
338
  setStage('delete_select');
339
+ setListOffset(0);
330
340
  break;
331
341
  case 'refresh':
332
342
  loadProviders();
333
343
  break;
334
- case 'back':
335
- onBack ? onBack() : null;
336
- break;
337
344
  }
338
345
  }}
339
346
  />
340
- {err && (
341
- <Box marginTop={1}>
342
- <Text color="red">{err}</Text>
343
- </Box>
344
- )}
345
- {opMsg && (
346
- <Box marginTop={1}>
347
- <Text color="green">{opMsg}</Text>
348
- </Box>
349
- )}
350
- <Text dimColor>提示:ESC 返回。↑↓/回车 选择操作</Text>
347
+ {err && <StateDisplay type="error" message={err} />}
348
+ {opMsg && <Box marginTop={1}><Text color="green">{opMsg}</Text></Box>}
351
349
  </Box>
350
+ <Hint>ESC: Back · ↑↓/Enter: Select Action</Hint>
352
351
  </Box>
353
352
  );
354
353
  };
@@ -364,38 +363,48 @@ export const ProviderPanel: React.FC<{
364
363
  }));
365
364
  if (!items.length) {
366
365
  return (
367
- <Box flexDirection="column">
368
- <Text color="yellow">未获取到预设,请先确保主服务已启动。</Text>
366
+ <Box flexDirection="column" flexGrow={1}>
367
+ <StateDisplay type="error" message="No presets available. Please ensure the backend is running." />
369
368
  <SelectInput
370
- items={[{ label: '← 返回', value: 'back' }]}
369
+ items={[{ label: '← Back', value: 'back' }]}
371
370
  onSelect={() => setStage('list')}
372
371
  />
373
372
  </Box>
374
373
  );
375
374
  }
375
+
376
+ const MAX_VISIBLE = 8;
377
+ const start = Math.min(listOffset, Math.max(0, items.length - MAX_VISIBLE));
378
+ const sliced = items.slice(start, start + MAX_VISIBLE);
379
+
376
380
  return (
377
- <Box flexDirection="column">
378
- <Text>选择预设:</Text>
379
- <SelectInput
380
- items={items}
381
- onSelect={it => {
382
- const preset = presets.find(p => p.id === (it.value as string));
383
- // preset.fields 获取字段列表;若为空则用默认最小集
384
- const fields: ProviderField[] =
385
- preset?.fields && preset.fields.length > 0
386
- ? preset.fields
387
- : [
388
- { key: 'name', label: 'Provider 昵称', required: true },
389
- { key: 'apiKey', label: 'API Key', required: true, secret: true },
390
- ];
391
- setSelectedPresetId(it.value as string);
392
- setAddFields(fields);
393
- setAddFieldValues({});
394
- setAddFieldIndex(0);
395
- setStage('add_input_fields');
396
- }}
397
- />
398
- <Text dimColor>ESC 返回</Text>
381
+ <Box flexDirection="column" flexGrow={1}>
382
+ <Title color="cyan">Select Preset</Title>
383
+ <Box flexDirection="row" flexGrow={1}>
384
+ <Box flexDirection="column" flexGrow={1}>
385
+ <SelectInput
386
+ items={sliced}
387
+ onSelect={it => {
388
+ const preset = presets.find(p => p.id === (it.value as string));
389
+ const fields: ProviderField[] =
390
+ preset?.fields && preset.fields.length > 0
391
+ ? preset.fields
392
+ : [
393
+ { key: 'name', label: 'Provider Nickname', required: true },
394
+ { key: 'apiKey', label: 'API Key', required: true, secret: true },
395
+ ];
396
+ setSelectedPresetId(it.value as string);
397
+ setAddFields(fields);
398
+ setAddFieldValues({});
399
+ setAddFieldIndex(0);
400
+ setListOffset(0);
401
+ setStage('add_input_fields');
402
+ }}
403
+ />
404
+ </Box>
405
+ <ScrollBar total={items.length} offset={start} height={MAX_VISIBLE} />
406
+ </Box>
407
+ <Hint>↑↓: Select · ESC: Back</Hint>
399
408
  </Box>
400
409
  );
401
410
  }
@@ -403,18 +412,15 @@ export const ProviderPanel: React.FC<{
403
412
  if (stage === 'add_input_fields') {
404
413
  const field = addFields[addFieldIndex];
405
414
  if (!field) {
406
- // 防御性分支:所有字段已填完但还没触发提交(一般不应走到这里)
407
- return <Text color="yellow">创建中...</Text>;
415
+ return <StateDisplay type="loading" message="Creating..." />;
408
416
  }
409
417
 
410
418
  const currentVal = addFieldValues[field.key] ?? field.default ?? '';
411
419
 
412
420
  const handleSubmit = (submittedVal: string) => {
413
- // ink-text-input passes the current value to onSubmit
414
421
  const val = submittedVal;
415
- if (field.required && !val.trim()) return; // 必填不允许空
422
+ if (field.required && !val.trim()) return;
416
423
 
417
- // 确保最新值已写入
418
424
  const merged = { ...addFieldValues, [field.key]: val };
419
425
 
420
426
  const nextIndex = addFieldIndex + 1;
@@ -422,7 +428,6 @@ export const ProviderPanel: React.FC<{
422
428
  setAddFieldValues(merged);
423
429
  setAddFieldIndex(nextIndex);
424
430
  } else {
425
- // 最后一个字段提交,触发创建
426
431
  const name = merged['name'] || '';
427
432
  const apiKey = merged['apiKey'] || '';
428
433
  const baseUrl = merged['baseUrl'] || '';
@@ -436,29 +441,35 @@ export const ProviderPanel: React.FC<{
436
441
  };
437
442
 
438
443
  return (
439
- <Box flexDirection="column">
440
- <Text color="cyan">
441
- 新增 Provider — 字段 {addFieldIndex + 1}/{addFields.length}
442
- </Text>
443
- <Text>
444
- {field.label}{field.required ? <Text color="red"> *</Text> : ''}
445
- {field.placeholder ? <Text dimColor> ({field.placeholder})</Text> : ''}:
446
- </Text>
444
+ <Box flexDirection="column" flexGrow={1}>
445
+ <Title color="cyan">
446
+ Add Provider — Field {addFieldIndex + 1}/{addFields.length}
447
+ </Title>
448
+ <Box marginBottom={1} flexDirection="row">
449
+ <Box width={20}>
450
+ <Text>
451
+ {field.label}{field.required ? <Text color="red"> *</Text> : ''}
452
+ </Text>
453
+ </Box>
454
+ <Box flexGrow={1}>
455
+ {field.placeholder ? <Text dimColor>({field.placeholder})</Text> : <Text />}
456
+ </Box>
457
+ </Box>
447
458
  <TextInput
448
459
  value={currentVal}
449
460
  onChange={v => setAddFieldValues(prev => ({ ...prev, [field.key]: v }))}
450
- // @ts-ignore - ink-text-input supports mask prop
461
+ // @ts-ignore
451
462
  mask={field.secret ? '*' : undefined}
452
463
  onSubmit={handleSubmit}
453
464
  />
454
- {err && <Text color="red">{err}</Text>}
455
- <Text dimColor>回车继续,ESC 返回列表</Text>
465
+ {err && <StateDisplay type="error" message={err} />}
466
+ <Hint>Enter: Continue · ESC: Back to List</Hint>
456
467
  </Box>
457
468
  );
458
469
  }
459
470
 
460
471
  if (stage === 'creating') {
461
- return <Text color="yellow">创建中...</Text>;
472
+ return <StateDisplay type="loading" message="Creating..." />;
462
473
  }
463
474
 
464
475
  if (stage === 'test_select') {
@@ -467,20 +478,20 @@ export const ProviderPanel: React.FC<{
467
478
  value: p.id
468
479
  }));
469
480
  return (
470
- <Box flexDirection="column">
471
- <Text>选择要测试的 Provider:</Text>
481
+ <Box flexDirection="column" flexGrow={1}>
482
+ <Title color="cyan">Select Provider to Test</Title>
472
483
  <SelectInput
473
484
  items={items}
474
485
  onSelect={it => doTest(it.value as string)}
475
486
  />
476
- {err && <Text color="red">{err}</Text>}
477
- <Text dimColor>ESC 返回</Text>
487
+ {err && <StateDisplay type="error" message={err} />}
488
+ <Hint>ESC: Back</Hint>
478
489
  </Box>
479
490
  );
480
491
  }
481
492
 
482
493
  if (stage === 'testing') {
483
- return <Text color="yellow">测试中...</Text>;
494
+ return <StateDisplay type="loading" message="Testing..." />;
484
495
  }
485
496
 
486
497
  if (stage === 'delete_select') {
@@ -489,8 +500,8 @@ export const ProviderPanel: React.FC<{
489
500
  value: p.id
490
501
  }));
491
502
  return (
492
- <Box flexDirection="column">
493
- <Text color="red">选择要删除的 Provider:</Text>
503
+ <Box flexDirection="column" flexGrow={1}>
504
+ <Title color="red">Select Provider to Delete</Title>
494
505
  <SelectInput
495
506
  items={items}
496
507
  onSelect={it => {
@@ -498,7 +509,7 @@ export const ProviderPanel: React.FC<{
498
509
  setStage('delete_confirm');
499
510
  }}
500
511
  />
501
- <Text dimColor>ESC 返回</Text>
512
+ <Hint>ESC: Back</Hint>
502
513
  </Box>
503
514
  );
504
515
  }
@@ -506,12 +517,12 @@ export const ProviderPanel: React.FC<{
506
517
  if (stage === 'delete_confirm') {
507
518
  if (!selectedId) { setStage('list'); return null; }
508
519
  return (
509
- <Box flexDirection="column">
510
- <Text color="red">确认删除 Provider:{selectedId} ?</Text>
520
+ <Box flexDirection="column" flexGrow={1}>
521
+ <Title color="red">Confirm Delete: {selectedId}?</Title>
511
522
  <SelectInput
512
523
  items={[
513
- { label: '是,删除', value: 'yes' },
514
- { label: '否,返回', value: 'no' }
524
+ { label: 'Yes, Delete', value: 'yes' },
525
+ { label: 'No, Back', value: 'no' }
515
526
  ]}
516
527
  onSelect={it => {
517
528
  if (it.value === 'no') {
@@ -521,32 +532,32 @@ export const ProviderPanel: React.FC<{
521
532
  }
522
533
  }}
523
534
  />
524
- {err && <Text color="red">{err}</Text>}
525
- <Text dimColor>ESC 返回</Text>
535
+ {err && <StateDisplay type="error" message={err} />}
536
+ <Hint>ESC: Back</Hint>
526
537
  </Box>
527
538
  );
528
539
  }
529
540
 
530
541
  if (stage === 'removing') {
531
- return <Text color="yellow">删除中...</Text>;
542
+ return <StateDisplay type="loading" message="Deleting..." />;
532
543
  }
533
544
 
534
545
  if (stage === 'edit_select') {
535
546
  const items = providers.map(p => ({
536
- label: `${p.name || p.id}${(currentId === p.id || p.isCurrent) ? ' ← 当前' : ''}`,
547
+ label: `${p.name || p.id}${(currentId === p.id || p.isCurrent) ? ' ← Current' : ''}`,
537
548
  value: p.id,
538
549
  }));
539
550
  if (!items.length) {
540
551
  return (
541
- <Box flexDirection="column">
542
- <Text color="yellow">暂无 Provider 可编辑。</Text>
543
- <SelectInput items={[{ label: '← 返回', value: 'back' }]} onSelect={() => setStage('list')} />
552
+ <Box flexDirection="column" flexGrow={1}>
553
+ <StateDisplay type="empty" message="No providers available to edit." />
554
+ <SelectInput items={[{ label: '← Back', value: 'back' }]} onSelect={() => setStage('list')} />
544
555
  </Box>
545
556
  );
546
557
  }
547
558
  return (
548
- <Box flexDirection="column">
549
- <Text>选择要编辑的 Provider:</Text>
559
+ <Box flexDirection="column" flexGrow={1}>
560
+ <Title color="cyan">Select Provider to Edit</Title>
550
561
  <SelectInput
551
562
  items={items}
552
563
  onSelect={it => {
@@ -557,29 +568,31 @@ export const ProviderPanel: React.FC<{
557
568
  setStage('edit_input_name');
558
569
  }}
559
570
  />
560
- <Text dimColor>ESC 返回</Text>
571
+ <Hint>ESC: Back</Hint>
561
572
  </Box>
562
573
  );
563
574
  }
564
575
 
565
576
  if (stage === 'edit_input_name') {
566
577
  return (
567
- <Box flexDirection="column">
568
- <Text>编辑名称(当前:<Text color="cyan">{editName}</Text>,回车保留不变):</Text>
578
+ <Box flexDirection="column" flexGrow={1}>
579
+ <Title color="cyan">Edit Name</Title>
580
+ <Text>Current: <Text color="cyan">{editName}</Text> (Enter to keep):</Text>
569
581
  <TextInput
570
582
  value={editName}
571
583
  onChange={setEditName}
572
584
  onSubmit={() => setStage('edit_input_key')}
573
585
  />
574
- <Text dimColor>回车继续,ESC 返回</Text>
586
+ <Hint>Enter: Continue · ESC: Back</Hint>
575
587
  </Box>
576
588
  );
577
589
  }
578
590
 
579
591
  if (stage === 'edit_input_key') {
580
592
  return (
581
- <Box flexDirection="column">
582
- <Text>输入新 API Key(留空则不修改):</Text>
593
+ <Box flexDirection="column" flexGrow={1}>
594
+ <Title color="cyan">Edit API Key</Title>
595
+ <Text>Enter new API Key (Leave empty to keep current):</Text>
583
596
  <TextInput
584
597
  value={editKey}
585
598
  onChange={setEditKey}
@@ -590,49 +603,48 @@ export const ProviderPanel: React.FC<{
590
603
  doEdit(editId, editName, editKey);
591
604
  }}
592
605
  />
593
- {err && <Text color="red">{err}</Text>}
594
- <Text dimColor>回车保存,ESC 返回</Text>
606
+ {err && <StateDisplay type="error" message={err} />}
607
+ <Hint>Enter: Save · ESC: Back</Hint>
595
608
  </Box>
596
609
  );
597
610
  }
598
611
 
599
612
  if (stage === 'editing') {
600
- return <Text color="yellow">保存中...</Text>;
613
+ return <StateDisplay type="loading" message="Saving..." />;
601
614
  }
602
615
 
603
616
  if (stage === 'slot_config') {
604
617
  const SLOTS = ['main', 'haiku', 'sonnet', 'opus'] as const;
605
618
  const SLOT_DESCS: Record<string, string> = {
606
- main: '主力模型,复杂推理、长上下文、代码生成等高要求任务',
607
- haiku: '快速轻量,简单问答、自动补全、低延迟响应',
608
- sonnet: '均衡模型,兼顾质量与速度,适合日常对话',
609
- opus: '最强模型,深度推理与复杂分析(调用量通常最少)',
619
+ main: 'Main model for complex reasoning and long context.',
620
+ haiku: 'Fast & light for simple Q&A and low latency.',
621
+ sonnet: 'Balanced quality & speed for daily tasks.',
622
+ opus: 'Strongest reasoning for deep analysis.',
610
623
  };
611
624
  const items = SLOTS.map(s => {
612
625
  const entry = slotTable[s];
613
626
  const providerName = entry
614
627
  ? (providers.find(p => p.id === entry.providerId)?.name || entry.providerId)
615
628
  : null;
616
- const modelDisplayName = entry?.label || entry?.modelId || '未配置';
617
- const status = entry ? `${providerName} / ${modelDisplayName}` : '未配置';
618
- const label = `[${s}] ${status} — ${SLOT_DESCS[s]}`;
629
+ const modelDisplayName = entry?.label || entry?.modelId || 'Unconfigured';
630
+ const status = entry ? `${providerName} / ${modelDisplayName}` : 'Unconfigured';
631
+ const label = `[${s}] ${safePadEnd(status, 30)} — ${SLOT_DESCS[s]}`;
619
632
  return { label, value: s };
620
633
  });
621
634
  return (
622
- <Box flexDirection="column">
623
- <Text color="cyan">配置槽位(选中槽位后选择模型)</Text>
624
- {err && <Text color="red">{err}</Text>}
625
- {opMsg && <Text color="green">{opMsg}</Text>}
635
+ <Box flexDirection="column" flexGrow={1}>
636
+ <Title color="cyan">Configure Model Slots</Title>
637
+ {err && <StateDisplay type="error" message={err} />}
638
+ {opMsg && <Box marginBottom={1}><Text color="green">{opMsg}</Text></Box>}
626
639
  <SelectInput
627
- items={[...items, { label: '← 返回主菜单', value: 'back' }]}
640
+ items={[...items, { label: '← Back to Menu', value: 'back' }]}
628
641
  onSelect={it => {
629
642
  if (it.value === 'back') { setStage('list'); setErr(null); return; }
630
643
  const slotName = it.value as string;
631
644
  setCurrentSlotName(slotName);
632
645
  setErr(null);
633
- setSlotLoadingMsg(`正在拉取模型列表...`);
646
+ setSlotLoadingMsg(`Fetching model list...`);
634
647
  setStage('slot_loading');
635
- // Fetch model lists from each provider's API endpoint
636
648
  Promise.all(
637
649
  providers.map(p =>
638
650
  axios.get(`${base}/api/providers/${encodeURIComponent(p.id)}/models`)
@@ -647,27 +659,27 @@ export const ProviderPanel: React.FC<{
647
659
  const map: Record<string, string[]> = {};
648
660
  results.forEach(r => { map[r.id] = r.models; });
649
661
  setSlotProviderModels(map);
650
- // Check if any models available
651
662
  const hasAny = results.some(r => r.models.length > 0);
652
663
  if (!hasAny) {
653
- setErr('所有 Provider 均未返回可用模型,请检查 API Key 和网络连接');
664
+ setErr('No models returned from any provider. Check API keys.');
654
665
  setStage('slot_config');
655
666
  } else {
667
+ setListOffset(0);
656
668
  setStage('slot_select_model');
657
669
  }
658
670
  });
659
671
  }}
660
672
  />
661
- <Text dimColor>ESC 返回</Text>
673
+ <Hint>ESC: Back</Hint>
662
674
  </Box>
663
675
  );
664
676
  }
665
677
 
666
678
  if (stage === 'slot_loading') {
667
679
  return (
668
- <Box flexDirection="column">
669
- <Text color="yellow">{slotLoadingMsg || '正在拉取模型列表...'}</Text>
670
- <Text dimColor>ESC 取消</Text>
680
+ <Box flexDirection="column" flexGrow={1}>
681
+ <StateDisplay type="loading" message={slotLoadingMsg || 'Fetching models...'} />
682
+ <Hint>ESC: Cancel</Hint>
671
683
  </Box>
672
684
  );
673
685
  }
@@ -683,10 +695,10 @@ export const ProviderPanel: React.FC<{
683
695
 
684
696
  if (items.length === 0) {
685
697
  return (
686
- <Box flexDirection="column">
687
- <Text color="red">没有可用的模型(所有 Provider 均未返回模型,请检查 API Key 和网络)</Text>
698
+ <Box flexDirection="column" flexGrow={1}>
699
+ <StateDisplay type="error" message="No available models found." />
688
700
  <SelectInput
689
- items={[{ label: '← 返回', value: 'back' }]}
701
+ items={[{ label: '← Back', value: 'back' }]}
690
702
  onSelect={() => setStage('slot_config')}
691
703
  />
692
704
  </Box>
@@ -694,37 +706,43 @@ export const ProviderPanel: React.FC<{
694
706
  }
695
707
 
696
708
  return (
697
- <Box flexDirection="column">
698
- <Text color="cyan">配置槽位 [{currentSlotName}] — 选择模型</Text>
699
- {err && <Text color="red">{err}</Text>}
700
- <SelectInput
701
- items={items}
702
- onSelect={it => {
703
- const val = it.value as string;
704
- if (val.startsWith('__header__')) return;
705
- const sepIdx = val.indexOf('::');
706
- const providerId = val.slice(0, sepIdx);
707
- const modelId = val.slice(sepIdx + 2);
708
- setTempSlotProviderId(providerId);
709
- setTempSlotModelId(modelId);
710
- setSlotLabelInput(modelId); // 默认建议使用模型名作为 Label
711
- setStage('slot_input_label');
712
- }}
713
- />
714
- <Text dimColor>↑↓ 选择模型,回车确认,ESC 返回</Text>
709
+ <Box flexDirection="column" flexGrow={1}>
710
+ <Title color="cyan">Configure Slot [{currentSlotName}] — Select Model</Title>
711
+ {err && <StateDisplay type="error" message={err} />}
712
+
713
+ <Box flexDirection="row" flexGrow={1}>
714
+ <Box flexDirection="column" flexGrow={1}>
715
+ <SelectInput
716
+ items={items}
717
+ onSelect={it => {
718
+ const val = it.value as string;
719
+ if (val.startsWith('__header__')) return;
720
+ const sepIdx = val.indexOf('::');
721
+ const providerId = val.slice(0, sepIdx);
722
+ const modelId = val.slice(sepIdx + 2);
723
+ setTempSlotProviderId(providerId);
724
+ setTempSlotModelId(modelId);
725
+ setSlotLabelInput(modelId);
726
+ setStage('slot_input_label');
727
+ }}
728
+ />
729
+ </Box>
730
+ <ScrollBar total={items.length} offset={0} height={10} />
731
+ </Box>
732
+ <Hint>↑↓: Select Model · Enter: Confirm · ESC: Back</Hint>
715
733
  </Box>
716
734
  );
717
735
  }
718
736
 
719
737
  if (stage === 'slot_input_label') {
720
738
  return (
721
- <Box flexDirection="column">
722
- <Text color="cyan">配置槽位 [{currentSlotName}] — 设置显示名称</Text>
739
+ <Box flexDirection="column" flexGrow={1}>
740
+ <Title color="cyan">Configure Slot [{currentSlotName}] — Set Display Name</Title>
723
741
  <Text>
724
- 模型:{providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
742
+ Model: {providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
725
743
  </Text>
726
744
  <Box marginTop={1}>
727
- <Text>显示名称 (Label):</Text>
745
+ <Text>Display Name (Label): </Text>
728
746
  <TextInput
729
747
  value={slotLabelInput}
730
748
  onChange={setSlotLabelInput}
@@ -740,18 +758,19 @@ export const ProviderPanel: React.FC<{
740
758
  ...prev,
741
759
  [currentSlotName]: { providerId: tempSlotProviderId, modelId: tempSlotModelId, label }
742
760
  }));
743
- setOpMsg(`已配置 [${currentSlotName}] -> ${label}`);
761
+ setOpMsg(`Configured [${currentSlotName}] -> ${label}`);
744
762
  setErr(null);
763
+ setListOffset(0);
745
764
  setStage('slot_config');
746
765
  })
747
766
  .catch(e => {
748
- setErr((e as any)?.response?.data?.message || (e as any)?.message || '保存失败');
767
+ setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
749
768
  setStage('slot_config');
750
769
  });
751
770
  }}
752
771
  />
753
772
  </Box>
754
- <Text dimColor>回车保存组件名称(显示在 Claude UI),ESC 返回模型选择</Text>
773
+ <Hint>Enter: Save (Display name in UI) · ESC: Back to Models</Hint>
755
774
  </Box>
756
775
  );
757
776
  }