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.
@@ -10,26 +10,26 @@ import fs from 'fs';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
12
  import { ensureSingletonLocalServer } from '../server/ensureSingletonLocalServer.ts';
13
- // 新增:通用 UI 元素与顶部工具栏
14
- import { TopBar, BottomBar, Panel, Hint, Kbd, SecondaryMenu } from '../manager/CliMenuUi.tsx';
13
+ // New: Common UI elements and top toolbar
14
+ import { TopBar, BottomBar, Panel, Hint, Kbd, SecondaryMenu, StateDisplay, ScrollBar } from '../manager/CliMenuUi.tsx';
15
15
  import { WelcomeV2 } from '../components/LogoV2/WelcomeV2.tsx';
16
16
  import { TopToolbar } from '../manager/TopToolbar.tsx';
17
17
 
18
- // 主题切换(Hook
18
+ // Theme switching (Hook)
19
19
  import { useTheme } from '../components/design-system/ThemeProvider.js';
20
- // Markdown 渲染(纯函数,不依赖 AppStateProvider context
20
+ // Markdown rendering (Pure function, no AppStateProvider context dependency)
21
21
  import { applyMarkdown } from '../utils/markdown.js';
22
22
  import { Ansi } from '../ink/Ansi.js';
23
23
 
24
- // 配置相关(仅使用可用接口)
24
+ // Config related (using available interfaces)
25
25
  import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
26
26
 
27
- // markedSessions 存到 ~/.claude-cli/ 固定目录,不受 cwd 影响
27
+ // markedSessions stored in ~/.claude-cli/ fixed directory, regardless of cwd
28
28
  const MARKED_FILE = path.join(os.homedir(), '.claude-cli', 'markedSessions.json');
29
29
 
30
30
  /**
31
- * 判断是否处于"官方"模式(没有激活任何自定义 provider)。
32
- * 逻辑与 ConversationService.shouldMarkManagedOAuth() 保持一致。
31
+ * Determine if in "official" mode (no custom provider active).
32
+ * Logic matches ConversationService.shouldMarkManagedOAuth().
33
33
  */
34
34
  function isOfficialMode(): boolean {
35
35
  const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
@@ -42,20 +42,20 @@ function isOfficialMode(): boolean {
42
42
  .some(key => typeof env[key] === 'string' && env[key]!.trim().length > 0);
43
43
  return !hasProviderEnv;
44
44
  } catch {
45
- return true; // 读不到 settings.json 按官方模式处理
45
+ return true; // Cannot read settings.json -> Treat as official mode
46
46
  }
47
47
  }
48
48
 
49
49
  /**
50
- * 构造子进程的 spawn env
51
- * 官方模式下注入 CLAUDE_CODE_ENTRYPOINT=claude-desktop + CLAUDE_CODE_OAUTH_TOKEN
52
- * 使新建/恢复的 bingocode 窗口能直接走 OAuth,不显示"未登录"。
50
+ * Build spawn env for child process.
51
+ * In official mode, inject CLAUDE_CODE_ENTRYPOINT=claude-desktop + CLAUDE_CODE_OAUTH_TOKEN,
52
+ * so new/resumed bingocode windows can use OAuth directly.
53
53
  */
54
54
  async function buildSpawnEnv(): Promise<NodeJS.ProcessEnv> {
55
55
  const base = { ...process.env };
56
56
  if (!isOfficialMode()) return base;
57
57
 
58
- // 官方模式:标记为 managed-OAuth,并注入 OAuth token
58
+ // Official mode: mark as managed-OAuth and inject OAuth token
59
59
  base.CLAUDE_CODE_ENTRYPOINT = 'claude-desktop';
60
60
  try {
61
61
  const { hahaOAuthService } = await import('../server/services/hahaOAuthService.js');
@@ -63,7 +63,7 @@ async function buildSpawnEnv(): Promise<NodeJS.ProcessEnv> {
63
63
  if (token) {
64
64
  base.CLAUDE_CODE_OAUTH_TOKEN = token;
65
65
  } else {
66
- // 没有有效 token 时不注入,让 CLI 走正常登录流程
66
+ // No valid token -> don't inject, use normal login flow
67
67
  delete base.CLAUDE_CODE_OAUTH_TOKEN;
68
68
  }
69
69
  } catch {
@@ -72,32 +72,32 @@ async function buildSpawnEnv(): Promise<NodeJS.ProcessEnv> {
72
72
  return base;
73
73
  }
74
74
 
75
- // 顶部高度:首页适配 LogoV2 + Toolbar,其它页面更紧凑
75
+ // Top height: Home fits LogoV2 + Toolbar, other pages more compact
76
76
  const TOP_H_HOME = Number(process.env.CLI_TOP_H_HOME || 9);
77
77
  const TOP_H_COMPACT = Number(process.env.CLI_TOP_H_COMPACT || 6);
78
- // 底栏高度
78
+ // Bottom bar height
79
79
  const BOTTOM_H = Number(process.env.CLI_BOTTOM_H || 3);
80
80
 
81
81
  const i18nMap = {
82
- zh: {
83
- menu: {
84
- newSession: '新建会话',
85
- history: '历史会话',
86
- provider: 'API配置',
87
- settings: '设置',
88
- about: '关于',
89
- exit: '退出',
82
+ zh: {
83
+ menu: {
84
+ newSession: 'New Session',
85
+ history: 'Session History',
86
+ provider: 'API Config',
87
+ settings: 'Settings',
88
+ about: 'About',
89
+ exit: 'Exit',
90
+ },
91
+ about: 'Bingo CLI Terminal - Version Info & About',
92
+ mark: '→ Mark Session',
93
+ unmark: '→ Unmark Session',
94
+ tipsSimple: 'L Lang | ESC Back | ←→ Menu | ↩ Enter | ? Help',
95
+ noData: 'No data',
96
+ emptyHistory: 'Nothing here yet. Start a new session?',
97
+ deleting: 'Delete this session? (Irreversible)',
98
+ historyHint: 'Enter to open · j next · k first · q back',
99
+ helpTitle: 'Shortcuts',
90
100
  },
91
- about: 'Bingo CLI 终端 - 版本信息与产品说明',
92
- mark: '→ 标记会话',
93
- unmark: '→ 取消标记',
94
- tipsSimple: 'L 语言 | ESC 返回 | ←→ 菜单 | ↩ 进入 | ? 帮助',
95
- noData: '暂无数据',
96
- emptyHistory: '这里还空空的,不如先新建一个会话?',
97
- deleting: '确认删除本会话?(不可恢复)',
98
- historyHint: '回车查看详情 · j 下一页 · k 回第一页 · q 返回',
99
- helpTitle: '快捷键速查',
100
- },
101
101
  en: {
102
102
  menu: {
103
103
  newSession: 'New Session',
@@ -144,11 +144,11 @@ function saveMarkedSessionIds(set: Set<string>) {
144
144
  }
145
145
  fs.writeFileSync(MARKED_FILE, JSON.stringify([...set]), 'utf-8');
146
146
  } catch (err) {
147
- console.error('[saveMarkedSessionIds] 写入失败:', err);
147
+ console.error('[saveMarkedSessionIds] Save failed:', err);
148
148
  }
149
149
  }
150
150
 
151
- // 消息条目(与后端 MessageEntry 对齐)
151
+ // Message Entry (Aligned with backend MessageEntry)
152
152
  type MessageEntry = {
153
153
  id: string;
154
154
  type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result';
@@ -160,7 +160,7 @@ type MessageEntry = {
160
160
  isSidechain?: boolean;
161
161
  };
162
162
 
163
- /** MessageEntry.content 提取纯文本 */
163
+ /** Extract plain text from MessageEntry.content */
164
164
  function extractTextFromContent(content: unknown): string {
165
165
  if (typeof content === 'string') return content;
166
166
  if (Array.isArray(content)) {
@@ -186,7 +186,7 @@ function extractTextFromContent(content: unknown): string {
186
186
  return String(content ?? '');
187
187
  }
188
188
 
189
- //@C:F ID=F.CM.CliMenuManager;K=F;V=1.5;P=CLI 主菜单;D=CLI;M=cli;S=main;In=;Out=JSX.Element
189
+ //@C:F ID=F.CM.CliMenuManager;K=F;V=1.5;P=CLI Main Menu;D=CLI;M=cli;S=main;In=;Out=JSX.Element
190
190
  export const CliMenuManager: React.FC = () => {
191
191
  const { stdout } = useStdout();
192
192
  const [terminalSize, setTerminalSize] = useState({
@@ -205,7 +205,7 @@ export const CliMenuManager: React.FC = () => {
205
205
  return () => { stdout?.off('resize', onResize); };
206
206
  }, [stdout]);
207
207
 
208
- // 动态视口(默认优先使用环境变量,否则使用当前终端宽度并留一点余量)
208
+ // Dynamic viewport
209
209
  const VIEW_W = Number(process.env.CLI_VIEW_W || Math.min(terminalSize.columns, 96));
210
210
  const VIEW_H = Number(process.env.CLI_VIEW_H || terminalSize.rows);
211
211
 
@@ -214,31 +214,31 @@ export const CliMenuManager: React.FC = () => {
214
214
  const [bootErr, setBootErr] = useState<string | null>(null);
215
215
  const { exit } = useApp();
216
216
 
217
- // 主题(全局 Hook
217
+ // Theme (Global Hook)
218
218
  const [theme, setTheme] = useTheme();
219
219
 
220
- // 语言
221
- const [lang, setLang] = useState<Lang>('zh');
220
+ // Language
221
+ const [lang, setLang] = useState<Lang>(() => getGlobalConfig().language || 'en');
222
222
  const t = i18nMap[lang].menu;
223
223
 
224
- // 顶部时间
225
- const [nowStr, setNowStr] = useState<string>(new Date().toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US', { hour12: false }));
224
+ // Top time
225
+ const [nowStr, setNowStr] = useState<string>(new Date().toLocaleString('en-US', { hour12: false }));
226
226
  useEffect(() => {
227
- const id = setInterval(() => setNowStr(new Date().toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US', { hour12: false })), 1000);
227
+ const id = setInterval(() => setNowStr(new Date().toLocaleString('en-US', { hour12: false })), 1000);
228
228
  return () => clearInterval(id);
229
- }, [lang]);
229
+ }, []);
230
230
 
231
- // 主菜单
231
+ // Main Menu
232
232
  const [page, setPage] = useState<MenuKey | null>(null);
233
233
  const menuItems = useMemo(() => menuKeys.map(key => ({ label: t[key], value: key })), [t]);
234
234
  const [navIndex, setNavIndex] = useState(0);
235
235
 
236
- // 新建会话
236
+ // New Session
237
237
  const [newSessionId, setNewSessionId] = useState<string | null>(null);
238
238
  const [creating, setCreating] = useState(false);
239
239
  const [createErr, setCreateErr] = useState<string | null>(null);
240
240
 
241
- // 历史
241
+ // History
242
242
  const [loadingHist, setLoadingHist] = useState(false);
243
243
  const [historyList, setHistoryList] = useState<any[]>([]);
244
244
  const [historyCursor, setHistoryCursor] = useState<string | null>(null);
@@ -247,66 +247,69 @@ export const CliMenuManager: React.FC = () => {
247
247
  const [historyMenuStage, setHistoryMenuStage] = useState<'list'|'window'|'deleteConfirm'>('list');
248
248
  const [selectedHistory, setSelectedHistory] = useState<any|null>(null);
249
249
 
250
- // 历史-消息内容
250
+ // History Messages
251
251
  const [sessionMessages, setSessionMessages] = useState<MessageEntry[]>([]);
252
252
  const [loadingMsgs, setLoadingMsgs] = useState(false);
253
253
  const [msgsErr, setMsgsErr] = useState<string | null>(null);
254
- const [msgsPage, setMsgsPage] = useState(0); // 0=最新页(底部),1=向上翻一页
254
+ const [msgsPage, setMsgsPage] = useState(0);
255
255
 
256
- // 标记持久化
256
+ // Mark Persistence
257
257
  const [markedSessionIds, setMarkedSessionIds] = useState<Set<string>>(new Set());
258
258
 
259
- // 设置页滚动偏移
259
+ // Settings page scroll offset
260
260
  const [settingsOffset, setSettingsOffset] = useState(0);
261
261
  const [settingData, setSettingData] = useState<any>(null);
262
262
  const [loadingSetting, setLoadingSetting] = useState(false);
263
263
  const [setErr, setSetErr] = useState<string | null>(null);
264
264
 
265
- // 顶部工具栏状态
265
+ // Top toolbar state
266
266
  const [animEnabled, setAnimEnabled] = useState(true);
267
267
  const [tipsEnabled, setTipsEnabled] = useState(true);
268
268
 
269
- // 帮助覆盖层
269
+ // Help overlay
270
270
  const [showHelp, setShowHelp] = useState(false);
271
271
 
272
- // 快速恢复标志(R)
272
+ // Keyboard navigation for lists
273
+ const [listOffset, setListOffset] = useState(0);
274
+
275
+ // Quick Resume (R)
273
276
  const [quickResumeRequested, setQuickResumeRequested] = useState(false);
274
277
 
275
- // 计算视口
278
+ // Compute viewport
276
279
  const TOP_H = page === null ? TOP_H_HOME : TOP_H_COMPACT;
277
280
  const MID_H = Math.max(5, VIEW_H - TOP_H - BOTTOM_H);
278
281
  const MSGS_PAGE_SIZE = Math.max(1, MID_H - 2);
279
282
  const [expandMsgs, setExpandMsgs] = useState(false);
280
- // 配置就绪探测(用于避免 Logo 早期读取)
283
+ // Config ready probe (avoid Logo early read)
281
284
  const [configReady, setConfigReady] = useState(false);
282
285
 
283
- // 启动/复用本地唯一服务,并注入 apiUrl(含重试机制)
286
+ // Boot/Reuse singleton local server (with retry)
284
287
  useEffect(() => {
285
288
  let mounted = true;
286
289
  (async () => {
287
290
  if (apiUrl) return;
288
291
  const entry = path.resolve(import.meta.dir, '../server/index.ts');
289
292
  const MAX_RETRIES = 3;
290
- const RETRY_DELAYS = [0, 2000, 5000]; // 首次无延迟,第2次2秒,第3次5秒
293
+ const RETRY_DELAYS = [0, 2000, 5000]; // 0s, 2s, 5s
291
294
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
292
295
  if (!mounted) return;
293
- if (attempt > 0) {
294
- setBootErr(`第 ${attempt} 次启动失败,${RETRY_DELAYS[attempt] / 1000}秒后重试...`);
295
- await new Promise(r => setTimeout(r, RETRY_DELAYS[attempt]));
296
- }
297
- if (!mounted) return;
298
- try {
299
- const handle = await ensureSingletonLocalServer({ serverEntry: entry });
300
- if (!mounted) { await handle.stopIfLast(); return; }
301
- setApiUrl(handle.baseUrl);
302
- setStopIfLast(() => handle.stopIfLast);
303
- setBootErr(null);
304
- return; // 成功,退出重试
305
- } catch (e: any) {
306
- if (attempt === MAX_RETRIES - 1) {
307
- setBootErr(e.message || '本地服务启动失败');
296
+ if (attempt > 0) {
297
+ setBootErr(`Attempt ${attempt} failed, retrying in ${RETRY_DELAYS[attempt] / 1000}s...`);
298
+ await new Promise(r => setTimeout(r, RETRY_DELAYS[attempt]));
299
+ }
300
+ if (!mounted) return;
301
+ try {
302
+ const handle = await ensureSingletonLocalServer({ serverEntry: entry });
303
+ if (!mounted) { await handle.stopIfLast(); return; }
304
+ setApiUrl(handle.baseUrl);
305
+ setStopIfLast(() => handle.stopIfLast);
306
+ setBootErr(null);
307
+ return; // Success, exit retry
308
+ } catch (e: any) {
309
+ if (attempt === MAX_RETRIES - 1) {
310
+ setBootErr(e.message || 'Local server failed to start');
311
+ }
308
312
  }
309
- }
310
313
  }
311
314
  })();
312
315
  return () => { mounted = false; if (stopIfLast) stopIfLast(); };
@@ -325,12 +328,12 @@ export const CliMenuManager: React.FC = () => {
325
328
  return () => { cancelled = true; };
326
329
  }, []);
327
330
 
328
- // 初始化标记
331
+ // Init marks
329
332
  useEffect(() => {
330
333
  setMarkedSessionIds(loadMarkedSessionIds());
331
334
  }, []);
332
335
 
333
- // 页面切换复位
336
+ // Page switch reset
334
337
  useEffect(() => {
335
338
  if (page === 'newSession') {
336
339
  setNewSessionId(null);
@@ -340,11 +343,11 @@ export const CliMenuManager: React.FC = () => {
340
343
  if (page !== 'settings') {
341
344
  setSettingsOffset(0);
342
345
  }
343
- // 关闭帮助覆盖层
346
+ // Close help overlay
344
347
  setShowHelp(false);
345
348
  }, [page]);
346
349
 
347
- // 历史页进入复位
350
+ // History page entry reset
348
351
  useEffect(() => {
349
352
  if (page === 'history') {
350
353
  setHistoryMenuStage('list');
@@ -357,14 +360,14 @@ export const CliMenuManager: React.FC = () => {
357
360
  }
358
361
  }, [page]);
359
362
 
360
- // 创建会话
363
+ // Create Session
361
364
  const onCreateSession = async () => {
362
365
  setCreating(true); setCreateErr(null);
363
366
  try {
364
367
  const fsReq = require('fs');
365
368
  const pathReq = require('path');
366
369
  const { spawn } = require('child_process');
367
- // import.meta.dir 定位包根,避免 process.cwd() 指向用户目录
370
+ // Use import.meta.dir for pkg root
368
371
  const pkgPath = pathReq.resolve(import.meta.dir, '../../package.json');
369
372
  const pkgJson = JSON.parse(fsReq.readFileSync(pkgPath, 'utf-8'));
370
373
  const bins = pkgJson.bin || {};
@@ -373,7 +376,7 @@ export const CliMenuManager: React.FC = () => {
373
376
  ? (bins['claude-haha'] ? 'claude-haha' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]))
374
377
  : (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
375
378
  const spawnCmd = isWin ? 'cmd' : 'sh';
376
- // Windows 直接调全局 bingocode 命令,不用 bun 前缀
379
+ // Windows calls global bingocode directly
377
380
  const spawnArgs = isWin ? ['/c', 'start', 'cmd', '/k', 'bingocode'] : ['-c', `${binName}`];
378
381
  const spawnEnv = await buildSpawnEnv();
379
382
  spawn(spawnCmd, spawnArgs, {
@@ -382,15 +385,15 @@ export const CliMenuManager: React.FC = () => {
382
385
  detached: true,
383
386
  stdio: 'ignore'
384
387
  }).unref();
385
- setNewSessionId('CLI已启动: ' + binName);
388
+ setNewSessionId('Started: ' + binName);
386
389
  } catch(e: any) {
387
- setCreateErr(e.message || '新建失败');
390
+ setCreateErr(e.message || 'Failed to create');
388
391
  } finally {
389
392
  setCreating(false);
390
393
  }
391
394
  };
392
395
 
393
- // 历史分页加载
396
+ // Paged loading for history
394
397
  useEffect(() => {
395
398
  if (page === 'history' && historyMenuStage === 'list') {
396
399
  setLoadingHist(true); setHistErr(null);
@@ -406,7 +409,7 @@ export const CliMenuManager: React.FC = () => {
406
409
  }
407
410
  setHistoryHasMore(!!pageData?.has_more);
408
411
  } catch (e: any) {
409
- setHistErr(e.message || '获取历史失败');
412
+ setHistErr(e.message || 'Failed to fetch history');
410
413
  } finally {
411
414
  setLoadingHist(false);
412
415
  }
@@ -447,7 +450,7 @@ export const CliMenuManager: React.FC = () => {
447
450
  setSessionMessages(msgs);
448
451
  }
449
452
  } catch (e: any) {
450
- if (!cancelled) setMsgsErr(e.message || '消息加载失败');
453
+ if (!cancelled) setMsgsErr(e.message || 'Failed to load messages');
451
454
  } finally {
452
455
  if (!cancelled) setLoadingMsgs(false);
453
456
  }
@@ -460,7 +463,7 @@ export const CliMenuManager: React.FC = () => {
460
463
  }
461
464
  }, [page, historyMenuStage, selectedHistory, apiUrl]);
462
465
 
463
- // 设置页数据
466
+ // Settings data
464
467
  useEffect(() => {
465
468
  if (page === 'settings') {
466
469
  setLoadingSetting(true); setSetErr(null);
@@ -469,7 +472,7 @@ export const CliMenuManager: React.FC = () => {
469
472
  const data = (await import('../utils/settings/settings')).default;
470
473
  setSettingData(data);
471
474
  } catch(e: any) {
472
- setSetErr(e.message||'获取设置失败');
475
+ setSetErr(e.message||'Failed to load settings');
473
476
  } finally { setLoadingSetting(false); }
474
477
  })();
475
478
  } else {
@@ -479,15 +482,21 @@ export const CliMenuManager: React.FC = () => {
479
482
  }
480
483
  }, [page]);
481
484
 
482
- // 键盘交互
485
+ // Keyboard interactions
483
486
  useInput((input, key) => {
484
- // 语言切换
487
+ // Language toggle
485
488
  if (input === 'l' || input === 'L') {
486
- setLang(l => (l === 'zh' ? 'en' : 'zh'));
489
+ const nextLang = lang === 'zh' ? 'en' : 'zh';
490
+ setLang(nextLang);
491
+ try {
492
+ const cfg = getGlobalConfig();
493
+ cfg.language = nextLang;
494
+ saveGlobalConfig(cfg);
495
+ } catch {}
487
496
  return;
488
497
  }
489
498
 
490
- // 主题切换(G
499
+ // Theme toggle (G)
491
500
  if ((input === 'g' || input === 'G')) {
492
501
  const order = ['light', 'dark', 'highContrast'] as const;
493
502
  const curr = String(theme || 'light');
@@ -502,27 +511,27 @@ export const CliMenuManager: React.FC = () => {
502
511
  return;
503
512
  }
504
513
 
505
- // 顶部动画开关(O
514
+ // Top animation toggle (O)
506
515
  if (input === 'o' || input === 'O') {
507
516
  setAnimEnabled(v => !v);
508
517
  return;
509
518
  }
510
- // 顶部 Tips 开关(T
519
+ // Top Tips toggle (T)
511
520
  if (input === 't' || input === 'T') {
512
521
  setTipsEnabled(v => !v);
513
522
  return;
514
523
  }
515
524
 
516
- // 帮助覆盖层(?)
525
+ // Help overlay (?)
517
526
  if (input === '?') {
518
527
  setShowHelp(v => !v);
519
528
  return;
520
529
  }
521
530
 
522
- // ESC 返回主页面或关闭帮助
531
+ // ESC to back or close help
523
532
  if (key.escape) {
524
533
  if (showHelp) { setShowHelp(false); return; }
525
- if (page === 'provider') return; // provider 内部处理
534
+ if (page === 'provider') return; // Handled internally
526
535
  setPage(null);
527
536
  setHistoryMenuStage('list');
528
537
  setSelectedHistory(null);
@@ -533,7 +542,7 @@ export const CliMenuManager: React.FC = () => {
533
542
  return;
534
543
  }
535
544
 
536
- // 快速入口:N 新建、R 恢复、P Provider
545
+ // Quick entries: N New, R Resume, P Provider
537
546
  if (input === 'n' || input === 'N') {
538
547
  setPage('newSession');
539
548
  onCreateSession();
@@ -549,7 +558,7 @@ export const CliMenuManager: React.FC = () => {
549
558
  return;
550
559
  }
551
560
 
552
- // 主菜单左右移动
561
+ // Main menu navigation
553
562
  if (!showHelp && key.leftArrow && page === null) {
554
563
  setNavIndex(i => (i - 1 + menuItems.length) % menuItems.length);
555
564
  return;
@@ -566,22 +575,32 @@ export const CliMenuManager: React.FC = () => {
566
575
  return;
567
576
  }
568
577
 
569
- // 历史页快捷键
578
+ // History shortcuts
570
579
  if (!showHelp && page === 'history') {
571
580
  if (historyMenuStage === 'list') {
581
+ if (key.downArrow || input === 'j') {
582
+ // Internal SelectInput handles cursor, we just need to track offset for ScrollBar
583
+ setListOffset(o => o + 1);
584
+ }
585
+ if (key.upArrow || input === 'k') {
586
+ setListOffset(o => Math.max(0, o - 1));
587
+ }
572
588
  if (input === 'q') {
573
589
  setPage(null);
574
590
  setHistoryMenuStage('list');
575
591
  setSelectedHistory(null);
576
592
  setHistoryCursor(null);
593
+ setListOffset(0);
577
594
  return;
578
595
  }
579
596
  if (input === 'j' && historyHasMore) {
580
597
  setHistoryCursor(historyList[historyList.length - 1]?.id || null);
598
+ setListOffset(0);
581
599
  return;
582
600
  }
583
601
  if (input === 'k') {
584
602
  setHistoryCursor(null);
603
+ setListOffset(0);
585
604
  return;
586
605
  }
587
606
  } else if (historyMenuStage === 'window') {
@@ -601,7 +620,7 @@ export const CliMenuManager: React.FC = () => {
601
620
  handleHistoryMenuAction('__back');
602
621
  return;
603
622
  }
604
- // 消息滚动:↑/k 向上翻页,↓/j 向下翻页
623
+ // Message scrolling
605
624
  if (key.upArrow || input === 'k') {
606
625
  setMsgsPage(p => Math.max(0, p - 1));
607
626
  return;
@@ -619,7 +638,7 @@ export const CliMenuManager: React.FC = () => {
619
638
  }
620
639
  }
621
640
 
622
- // 设置页滚动
641
+ // Settings scrolling
623
642
  if (!showHelp && page === 'settings' && settingData && typeof settingData === 'object') {
624
643
  const total = Object.keys(settingData).length;
625
644
  const visible = Math.max(1, MID_H - 1);
@@ -732,14 +751,14 @@ export const CliMenuManager: React.FC = () => {
732
751
  ];
733
752
  }
734
753
  const items = [
735
- ...groupToItems(today, '—— 今天 ——'),
736
- ...groupToItems(week, '—— 本周 ——'),
737
- ...groupToItems(earlier, '—— 更早 ——'),
754
+ ...groupToItems(today, '—— Today ——'),
755
+ ...groupToItems(week, '—— This Week ——'),
756
+ ...groupToItems(earlier, '—— Earlier ——'),
738
757
  ];
739
758
  return items;
740
759
  }, [historyList, markedSessionIds]);
741
760
 
742
- // 标记切换
761
+ // Toggle Mark
743
762
  const toggleMarkSession = (sessionId: string) => {
744
763
  setMarkedSessionIds(prev => {
745
764
  const next = new Set(prev);
@@ -779,7 +798,7 @@ export const CliMenuManager: React.FC = () => {
779
798
  };
780
799
 
781
800
 
782
- // 刷新历史
801
+ // Refresh history
783
802
  const refreshHistoryList = () => {
784
803
  setLoadingHist(true); setHistErr(null);
785
804
  let url = apiUrl + '/api/sessions';
@@ -789,11 +808,11 @@ export const CliMenuManager: React.FC = () => {
789
808
  setHistoryCursor(pageData?.first_id || null);
790
809
  setHistoryHasMore(!!pageData?.has_more);
791
810
  }).catch(e => {
792
- setHistErr(e.message || '获取历史失败');
811
+ setHistErr(e.message || 'Failed to fetch history');
793
812
  }).finally(() => setLoadingHist(false));
794
813
  };
795
814
 
796
- // 删除会话
815
+ // Delete Session
797
816
  const handleDeleteSession = (sessionId: string) => {
798
817
  const url = apiUrl.replace(/\/+$/, '') + '/api/sessions/' + sessionId;
799
818
  axios.delete(url)
@@ -806,18 +825,18 @@ export const CliMenuManager: React.FC = () => {
806
825
  });
807
826
  };
808
827
 
809
- // 二级菜单(底栏右侧)
828
+ // Secondary menu (bottom bar right)
810
829
  const secondaryMenu: SecondaryMenu = useMemo(() => {
811
830
  if (page === 'history' && historyMenuStage === 'window' && selectedHistory) {
812
831
  const isMarked = markedSessionIds.has(selectedHistory.id);
813
832
  const markLabel = isMarked ? i18nMap[lang].unmark : i18nMap[lang].mark;
814
833
  return {
815
- title: lang === 'zh' ? '会话操作' : 'Session Actions',
834
+ title: 'Session Actions',
816
835
  items: [
817
836
  { label: markLabel, value: '__toggle_mark' },
818
- { label: '→ 继续会话聊天', value: '__continue' },
819
- { label: '→ 删除会话聊天', value: '__delete' },
820
- { label: '← 返回历史列表', value: '__back' },
837
+ { label: '→ Continue session', value: '__continue' },
838
+ { label: '→ Delete session', value: '__delete' },
839
+ { label: '← Back to list', value: '__back' },
821
840
  ],
822
841
  onSelect: (item: any) => {
823
842
  if (item.value === '__back') {
@@ -837,10 +856,10 @@ export const CliMenuManager: React.FC = () => {
837
856
 
838
857
  if (page === 'history' && historyMenuStage === 'deleteConfirm' && selectedHistory) {
839
858
  return {
840
- title: lang === 'zh' ? '删除确认' : 'Confirm Delete',
859
+ title: 'Confirm Delete',
841
860
  items: [
842
- { label: lang === 'zh' ? '是,确认删除' : 'Yes, delete', value: '__confirm_delete' },
843
- { label: lang === 'zh' ? '否,返回详情' : 'No, back', value: '__cancel_delete' },
861
+ { label: 'Yes, delete', value: '__confirm_delete' },
862
+ { label: 'No, back', value: '__cancel_delete' },
844
863
  ],
845
864
  onSelect: (item: any) => {
846
865
  if (item.value === '__cancel_delete') {
@@ -854,34 +873,32 @@ export const CliMenuManager: React.FC = () => {
854
873
  return null;
855
874
  }, [page, historyMenuStage, selectedHistory, markedSessionIds, lang]);
856
875
 
857
- // 帮助覆盖层
876
+ // Help Overlay
858
877
  function renderHelpOverlay() {
859
878
  return (
860
879
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
861
880
  <Text color="magenta">{i18nMap[lang].helpTitle}</Text>
862
881
  <Text> </Text>
863
- <Text color="cyan">N</Text><Text> 新建会话 / New Session</Text>
864
- <Text color="cyan">R</Text><Text> 快速恢复最近会话 / Quick Resume</Text>
865
- <Text color="cyan">P</Text><Text> 打开 Provider 管理 / Open Provider</Text>
866
- <Text color="cyan">G</Text><Text> 切换主题(light/dark/highContrast)</Text>
867
- <Text color="cyan">L</Text><Text> 中英切换 / Toggle Language</Text>
868
- <Text color="cyan">O</Text><Text> 切换顶部动画开关(显示用)</Text>
869
- <Text color="cyan">T</Text><Text> 切换顶部 Tips 开关(显示用)</Text>
870
- <Text color="cyan">?</Text><Text> 打开/关闭此帮助</Text>
882
+ <Text color="cyan">N</Text><Text> New Session</Text>
883
+ <Text color="cyan">R</Text><Text> Quick Resume</Text>
884
+ <Text color="cyan">P</Text><Text> Open Provider Config</Text>
885
+ <Text color="cyan">G</Text><Text> Toggle Theme (light/dark/highContrast)</Text>
886
+ <Text color="cyan">L</Text><Text> Toggle Language (en/zh)</Text>
887
+ <Text color="cyan">O</Text><Text> Toggle Top Animation</Text>
888
+ <Text color="cyan">T</Text><Text> Toggle Top Tips</Text>
889
+ <Text color="cyan">?</Text><Text> Toggle Help</Text>
871
890
  <Text> </Text>
872
- <Hint>{lang==='zh' ? 'ESC 关闭 · 在任意页面可用' : 'ESC to close · Works anywhere'}</Hint>
891
+ <Hint>ESC to close · Works anywhere</Hint>
873
892
  </Box>
874
893
  );
875
894
  }
876
895
 
877
- // 中心内容渲染
896
+ // Center Content
878
897
  function renderCenter() {
879
898
  if (showHelp) return renderHelpOverlay();
880
899
 
881
- // 首页:默认显示欢迎页图像,水平居中(WelcomeV2 固定 58 字符宽)
900
+ // Home: WelcomeV2 (58 cols wide)
882
901
  if (page === null) {
883
- // 首页 Panel 无边框无 padding,内容区宽 = VIEW_W = 96
884
- // WelcomeV2 宽 58,左偏移 = floor((96 - 58) / 2) = 19
885
902
  const WELCOME_W = 58;
886
903
  const leftPad = Math.max(0, Math.floor((VIEW_W - WELCOME_W) / 2));
887
904
  return (
@@ -891,75 +908,75 @@ export const CliMenuManager: React.FC = () => {
891
908
  <WelcomeV2 />
892
909
  </Box>
893
910
  {!apiUrl && !bootErr && (
894
- <Text color="yellow">⏳ 服务启动中...</Text>
911
+ <StateDisplay type="loading" message="Starting server..." />
895
912
  )}
896
913
  {bootErr && (
897
- <Text color="red">服务启动失败: {bootErr}</Text>
914
+ <StateDisplay type="error" message={`Server boot failed: ${bootErr}`} />
898
915
  )}
899
916
  </Box>
900
917
  );
901
918
  }
902
919
 
903
- // 新建
920
+ // New Session
904
921
  if (page === 'newSession') {
905
922
  return (
906
923
  <Box flexDirection="column" width={VIEW_W} height={MID_H}>
907
- {creating && <Text color="yellow">新建中...</Text>}
908
- {createErr && <Text color="red">新建失败: {createErr}</Text>}
909
- {newSessionId && <Text color="green">新建会话: {newSessionId}</Text>}
910
- {!creating && !createErr && !newSessionId && <Text dimColor>已进入新建会话页,等待创建结果...</Text>}
924
+ {creating && <StateDisplay type="loading" message="Creating..." />}
925
+ {createErr && <StateDisplay type="error" message={`Failed to create: ${createErr}`} />}
926
+ {newSessionId && <Box alignItems="center" justifyContent="center" flexGrow={1}><Text color="green">New Session: {newSessionId}</Text></Box>}
927
+ {!creating && !createErr && !newSessionId && <StateDisplay type="empty" message="Entered new session page, waiting for result..." />}
911
928
  </Box>
912
929
  );
913
930
  }
914
931
 
915
- // 历史
932
+ // History
916
933
  if (page === 'history') {
917
- if (histErr) return <Box width={VIEW_W} height={MID_H}><Text color="red">{histErr}</Text></Box>;
934
+ if (histErr) return <StateDisplay type="error" message={histErr} onRetry={refreshHistoryList} />;
918
935
  if (historyMenuStage === 'deleteConfirm' && selectedHistory) {
919
936
  const halfH = Math.floor(MID_H / 2);
920
937
  const items = [
921
- { label: lang === 'zh' ? '是,确认删除' : 'Yes, delete', value: '__confirm_delete' },
922
- { label: lang === 'zh' ? '否,返回详情' : 'No, back', value: '__cancel_delete' },
938
+ { label: 'Yes, delete', value: '__confirm_delete' },
939
+ { label: 'No, back', value: '__cancel_delete' },
923
940
  ];
924
941
  return (
925
942
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
926
943
  <Box height={halfH} flexDirection="column">
927
944
  <Text color="red">{i18nMap[lang].deleting}</Text>
928
945
  <Text>id: {selectedHistory.id}</Text>
929
- <Text>标题: {selectedHistory.title}</Text>
930
- <Text>创建时间: {selectedHistory.createdAt}</Text>
946
+ <Text>Title: {selectedHistory.title}</Text>
947
+ <Text>Created At: {selectedHistory.createdAt}</Text>
931
948
  </Box>
932
- <Box height={MID_H - halfH} flexDirection="column" borderStyle="round" borderColor="red" paddingLeft={1} paddingRight={1}>
933
- <Text>{lang === 'zh' ? '删除确认' : 'Confirm Delete'}</Text>
949
+ <Panel height={MID_H - halfH} borderStyle="round" borderColor="red" paddingX={1}>
950
+ <Text>Confirm Delete</Text>
934
951
  <SelectInput
935
952
  items={items}
936
953
  onSelect={(item) => handleHistoryMenuAction(String(item.value))}
937
954
  />
938
- <Hint>↩ 执行 · q 返回</Hint>
939
- </Box>
955
+ <Hint>↩ Enter · q Back</Hint>
956
+ </Panel>
940
957
  </Box>
941
958
  );
942
959
  }
943
960
  if (!historyList.length && loadingHist) {
944
- return <Box width={VIEW_W} height={MID_H}><Text color="yellow">加载中...</Text></Box>;
961
+ return <StateDisplay type="loading" message="Fetching history..." />;
945
962
  }
946
963
  if (!historyList.length) {
947
- return <Box width={VIEW_W} height={MID_H}><Text dimColor>{i18nMap[lang].emptyHistory}</Text></Box>;
964
+ return <StateDisplay type="empty" message={i18nMap[lang].emptyHistory} />;
948
965
  }
949
966
 
950
967
  if (historyMenuStage === 'window' && selectedHistory) {
951
968
  const isMarked = markedSessionIds.has(selectedHistory.id);
952
969
 
953
- // ── 信息栏高度固定 3 行(标题 + 元信息 + 提示) ──
970
+ // INFO_H: Title + Metadata + Hint (3 lines)
954
971
  const INFO_H = 3;
955
972
  const MSGS_H = Math.max(3, MID_H - INFO_H);
956
973
 
957
- // ── 过滤出可展示的消息(user / assistant / system,跳过 tool_use / tool_result) ──
974
+ // Filter messages
958
975
  const displayMsgs = sessionMessages.filter(
959
976
  m => m.type === 'user' || m.type === 'assistant' || m.type === 'system'
960
977
  );
961
978
 
962
- // ── 分页:每页 MSGS_PAGE_SIZE 条消息 ──
979
+ // Pagination
963
980
  const totalPages = Math.max(1, Math.ceil(displayMsgs.length / MSGS_PAGE_SIZE));
964
981
  const safePage = Math.min(msgsPage, totalPages - 1);
965
982
  const pageStart = safePage * MSGS_PAGE_SIZE;
@@ -967,24 +984,24 @@ export const CliMenuManager: React.FC = () => {
967
984
 
968
985
  return (
969
986
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
970
- {/* ── 顶部信息栏 ── */}
987
+ {/* Top Info Bar */}
971
988
  <Box height={INFO_H} flexDirection="column">
972
989
  <Text color={isMarked ? 'yellow' : 'cyan'}>
973
990
  {isMarked ? '★ ' : ''}{selectedHistory.title || 'Untitled'}
974
991
  <Text dimColor> {selectedHistory.createdAt?.slice(0, 16).replace('T', ' ') || ''} · {displayMsgs.length} msgs</Text>
975
992
  </Text>
976
993
  <Hint>
977
- j/↓ 下翻 · k/↑ 上翻 · m 标记 · c 继续 · d 删除 · q 返回
994
+ j/↓ Down · k/↑ Up · m Mark · c Continue · d Delete · q Back
978
995
  {displayMsgs.length > MSGS_PAGE_SIZE ? ` [${safePage + 1}/${totalPages}]` : ''}
979
996
  </Hint>
980
997
  </Box>
981
998
 
982
- {/* ── 消息区 ── */}
999
+ {/* Message Area */}
983
1000
  <Box height={MSGS_H} flexDirection="column">
984
- {loadingMsgs && <Text color="yellow">加载消息中...</Text>}
985
- {msgsErr && <Text color="red">错误: {msgsErr}</Text>}
1001
+ {loadingMsgs && <StateDisplay type="loading" message="Loading messages..." />}
1002
+ {msgsErr && <StateDisplay type="error" message={`Error: ${msgsErr}`} />}
986
1003
  {!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
987
- <Text dimColor>无消息记录</Text>
1004
+ <StateDisplay type="empty" message="No message history" />
988
1005
  )}
989
1006
  {pageMsgs.map((msg) => {
990
1007
  const text = extractTextFromContent(msg.content);
@@ -1010,32 +1027,35 @@ export const CliMenuManager: React.FC = () => {
1010
1027
  }
1011
1028
 
1012
1029
 
1013
- // 历史列表
1030
+ // History List
1014
1031
  return (
1015
- <Box width={VIEW_W} height={MID_H} flexDirection="column">
1016
- <SelectInput
1017
- key={`${historyCursor ?? 'first'}:${groupedHistoryItems.length}`}
1018
- items={groupedHistoryItems}
1019
- onSelect={item => {
1020
- if (String(item.value).startsWith('__group_')) return; // 忽略分组标题选择
1021
- const session = historyList.find(h => h.id === item.value);
1022
- if (session) {
1023
- setSelectedHistory(session);
1024
- setHistoryMenuStage('window');
1025
- }
1026
- }}
1027
- itemComponent={({ isSelected, label }) => {
1028
- const it = groupedHistoryItems.find(i => i.label === label);
1029
- const isGroup = it?.isGroup;
1030
- const color = it?.color;
1031
- return (
1032
- <Text color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
1033
- {label}
1034
- </Text>
1035
- )
1036
- }}
1037
- />
1038
- <Hint>{i18nMap[lang].historyHint}</Hint>
1032
+ <Box width={VIEW_W} height={MID_H} flexDirection="row">
1033
+ <Box flexDirection="column" flexGrow={1}>
1034
+ <SelectInput
1035
+ key={`${historyCursor ?? 'first'}:${groupedHistoryItems.length}`}
1036
+ items={groupedHistoryItems}
1037
+ onSelect={item => {
1038
+ if (String(item.value).startsWith('__group_')) return; // Ignore group headers
1039
+ const session = historyList.find(h => h.id === item.value);
1040
+ if (session) {
1041
+ setSelectedHistory(session);
1042
+ setHistoryMenuStage('window');
1043
+ }
1044
+ }}
1045
+ itemComponent={({ isSelected, label }) => {
1046
+ const it = groupedHistoryItems.find(i => i.label === label);
1047
+ const isGroup = it?.isGroup;
1048
+ const color = it?.color;
1049
+ return (
1050
+ <Text color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
1051
+ {label}
1052
+ </Text>
1053
+ )
1054
+ }}
1055
+ />
1056
+ <Hint>{i18nMap[lang].historyHint}</Hint>
1057
+ </Box>
1058
+ <ScrollBar total={groupedHistoryItems.length} offset={listOffset} height={MID_H} />
1039
1059
  </Box>
1040
1060
  );
1041
1061
  }
@@ -1045,8 +1065,12 @@ export const CliMenuManager: React.FC = () => {
1045
1065
  if (!apiUrl) {
1046
1066
  return (
1047
1067
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1048
- <Text color="yellow">{bootErr ? `服务启动失败: ${bootErr}` : '⏳ 服务启动中,请稍候...'}</Text>
1049
- <Text dimColor>ESC 返回主菜单</Text>
1068
+ <StateDisplay
1069
+ type={bootErr ? "error" : "loading"}
1070
+ message={bootErr ? `Server boot failed: ${bootErr}` : 'Starting server, please wait...'}
1071
+ onRetry={() => process.exit(1)} // Or another way to trigger reboot
1072
+ />
1073
+ <Text dimColor alignSelf="center">ESC for main menu</Text>
1050
1074
  </Box>
1051
1075
  );
1052
1076
  }
@@ -1057,61 +1081,62 @@ export const CliMenuManager: React.FC = () => {
1057
1081
  );
1058
1082
  }
1059
1083
 
1060
- // 设置
1084
+ // Settings
1061
1085
  if (page === 'settings') {
1062
- if (loadingSetting) return <Box width={VIEW_W} height={MID_H}><Text color="yellow">加载设置中...</Text></Box>;
1063
- if (setErr) return <Box width={VIEW_W} height={MID_H}><Text color="red">{setErr}</Text></Box>;
1064
- if (!settingData || typeof settingData !== 'object') return <Box width={VIEW_W} height={MID_H}><Text dimColor>暂无设置项</Text></Box>;
1086
+ if (loadingSetting) return <StateDisplay type="loading" message="Loading settings..." />;
1087
+ if (setErr) return <StateDisplay type="error" message={setErr} />;
1088
+ if (!settingData || typeof settingData !== 'object') return <StateDisplay type="empty" message="No settings found" />;
1065
1089
  const entries = Object.entries(settingData);
1066
1090
  const visible = Math.max(1, MID_H - 1);
1067
1091
  const start = Math.min(settingsOffset, Math.max(0, entries.length - visible));
1068
1092
  const sliced = entries.slice(start, start + visible);
1069
1093
  return (
1070
1094
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1095
+ <ScrollBar total={entries.length} offset={start} height={visible} />
1071
1096
  {sliced.map(([k, v]) => <Text key={k}>{k}: {typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>)}
1072
1097
  <Hint>
1073
- {lang==='zh' ? '↑/k 与 ↓/j 滚动' : '↑/k and ↓/j to scroll'} · {start+1}-{Math.min(start+visible, entries.length)}/{entries.length}
1098
+ ↑/k and ↓/j scroll · {start+1}-{Math.min(start+visible, entries.length)}/{entries.length}
1074
1099
  </Hint>
1075
1100
  </Box>
1076
1101
  );
1077
1102
  }
1078
1103
 
1079
- // 关于
1104
+ // About
1080
1105
  if (page === 'about') {
1081
1106
  return (
1082
1107
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1083
1108
  <Text>{i18nMap[lang].about}</Text>
1084
1109
  <Hint>
1085
- {lang==='zh' ? 'API 地址' : 'API Base'}: {apiUrl}
1110
+ API Base: {apiUrl}
1086
1111
  </Hint>
1087
1112
  </Box>
1088
1113
  );
1089
1114
  }
1090
1115
 
1091
- // 退出
1116
+ // Exit
1092
1117
  if (page === 'exit') {
1093
1118
  exit();
1094
- return <Box width={VIEW_W} height={MID_H}><Text>退出...</Text></Box>;
1119
+ return <Box width={VIEW_W} height={MID_H}><Text>Exiting...</Text></Box>;
1095
1120
  }
1096
1121
 
1097
1122
  return <Box width={VIEW_W} height={MID_H} />;
1098
1123
  }
1099
1124
 
1100
- // 退出逻辑
1125
+ // Exit logic
1101
1126
  if (terminalSize.columns < 60 || terminalSize.rows < 15) {
1102
1127
  return (
1103
1128
  <Box flexDirection="column" padding={2}>
1104
- <Text color="red">终端窗口太小! / Terminal too small!</Text>
1105
- <Text>当前 / Current: {terminalSize.columns}x{terminalSize.rows}</Text>
1106
- <Text>请调节窗口大小以继续... / Please resize to continue...</Text>
1129
+ <Text color="red">Terminal too small!</Text>
1130
+ <Text>Current: {terminalSize.columns}x{terminalSize.rows}</Text>
1131
+ <Text>Please resize to continue...</Text>
1107
1132
  </Box>
1108
1133
  );
1109
1134
  }
1110
1135
 
1111
- // 根渲染:上-中-下三段
1136
+ // Root Render
1112
1137
  return (
1113
1138
  <Box flexDirection="column" width={VIEW_W}>
1114
- {/* 顶部欢迎/Logo + 工具栏 */}
1139
+ {/* Top Welcome / Logo Area + Toolbar */}
1115
1140
  <TopBar
1116
1141
  ready={configReady}
1117
1142
  page={page}
@@ -1130,9 +1155,7 @@ export const CliMenuManager: React.FC = () => {
1130
1155
  }
1131
1156
  />
1132
1157
 
1133
- {/* 中心业务区(固定高度,内部处理分页/滚动)
1134
- 首页:无边框无 margin,WelcomeV2 直接撑满,避免 border 额外占行导致超出;
1135
- 其它页面:single border + marginY */}
1158
+ {/* Center Center Area */}
1136
1159
  {page === null ? (
1137
1160
  <Panel width={VIEW_W} height={MID_H} noBorder paddingX={0} paddingY={0} marginY={0}>
1138
1161
  {renderCenter()}
@@ -1143,7 +1166,7 @@ export const CliMenuManager: React.FC = () => {
1143
1166
  </Panel>
1144
1167
  )}
1145
1168
 
1146
- {/* 底部菜单与二级菜单 */}
1169
+ {/* Bottom Menu & Secondary Menu */}
1147
1170
  <BottomBar
1148
1171
  width={VIEW_W}
1149
1172
  height={BOTTOM_H}