bingocode 1.1.65 → 1.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,45 @@ 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>('en');
222
+
223
+ useEffect(() => {
224
+ if (configReady) {
225
+ try {
226
+ const cfg = getGlobalConfig();
227
+ if (cfg.language && (cfg.language === 'en' || cfg.language === 'zh')) {
228
+ setLang(cfg.language as Lang);
229
+ }
230
+ } catch (e) {
231
+ // Silently fail if config has issues
232
+ }
233
+ }
234
+ }, [configReady]);
235
+
222
236
  const t = i18nMap[lang].menu;
223
237
 
224
- // 顶部时间
225
- const [nowStr, setNowStr] = useState<string>(new Date().toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US', { hour12: false }));
238
+ // Top time
239
+ const [nowStr, setNowStr] = useState<string>(new Date().toLocaleString('en-US', { hour12: false }));
226
240
  useEffect(() => {
227
- const id = setInterval(() => setNowStr(new Date().toLocaleString(lang === 'zh' ? 'zh-CN' : 'en-US', { hour12: false })), 1000);
241
+ const id = setInterval(() => setNowStr(new Date().toLocaleString('en-US', { hour12: false })), 1000);
228
242
  return () => clearInterval(id);
229
- }, [lang]);
243
+ }, []);
230
244
 
231
- // 主菜单
245
+ // Main Menu
232
246
  const [page, setPage] = useState<MenuKey | null>(null);
233
247
  const menuItems = useMemo(() => menuKeys.map(key => ({ label: t[key], value: key })), [t]);
234
248
  const [navIndex, setNavIndex] = useState(0);
235
249
 
236
- // 新建会话
250
+ // New Session
237
251
  const [newSessionId, setNewSessionId] = useState<string | null>(null);
238
252
  const [creating, setCreating] = useState(false);
239
253
  const [createErr, setCreateErr] = useState<string | null>(null);
240
254
 
241
- // 历史
255
+ // History
242
256
  const [loadingHist, setLoadingHist] = useState(false);
243
257
  const [historyList, setHistoryList] = useState<any[]>([]);
244
258
  const [historyCursor, setHistoryCursor] = useState<string | null>(null);
@@ -247,66 +261,69 @@ export const CliMenuManager: React.FC = () => {
247
261
  const [historyMenuStage, setHistoryMenuStage] = useState<'list'|'window'|'deleteConfirm'>('list');
248
262
  const [selectedHistory, setSelectedHistory] = useState<any|null>(null);
249
263
 
250
- // 历史-消息内容
264
+ // History Messages
251
265
  const [sessionMessages, setSessionMessages] = useState<MessageEntry[]>([]);
252
266
  const [loadingMsgs, setLoadingMsgs] = useState(false);
253
267
  const [msgsErr, setMsgsErr] = useState<string | null>(null);
254
- const [msgsPage, setMsgsPage] = useState(0); // 0=最新页(底部),1=向上翻一页
268
+ const [msgsPage, setMsgsPage] = useState(0);
255
269
 
256
- // 标记持久化
270
+ // Mark Persistence
257
271
  const [markedSessionIds, setMarkedSessionIds] = useState<Set<string>>(new Set());
258
272
 
259
- // 设置页滚动偏移
273
+ // Settings page scroll offset
260
274
  const [settingsOffset, setSettingsOffset] = useState(0);
261
275
  const [settingData, setSettingData] = useState<any>(null);
262
276
  const [loadingSetting, setLoadingSetting] = useState(false);
263
277
  const [setErr, setSetErr] = useState<string | null>(null);
264
278
 
265
- // 顶部工具栏状态
279
+ // Top toolbar state
266
280
  const [animEnabled, setAnimEnabled] = useState(true);
267
281
  const [tipsEnabled, setTipsEnabled] = useState(true);
268
282
 
269
- // 帮助覆盖层
283
+ // Help overlay
270
284
  const [showHelp, setShowHelp] = useState(false);
271
285
 
272
- // 快速恢复标志(R)
286
+ // Keyboard navigation for lists
287
+ const [listOffset, setListOffset] = useState(0);
288
+
289
+ // Quick Resume (R)
273
290
  const [quickResumeRequested, setQuickResumeRequested] = useState(false);
274
291
 
275
- // 计算视口
292
+ // Compute viewport
276
293
  const TOP_H = page === null ? TOP_H_HOME : TOP_H_COMPACT;
277
294
  const MID_H = Math.max(5, VIEW_H - TOP_H - BOTTOM_H);
278
295
  const MSGS_PAGE_SIZE = Math.max(1, MID_H - 2);
279
296
  const [expandMsgs, setExpandMsgs] = useState(false);
280
- // 配置就绪探测(用于避免 Logo 早期读取)
297
+ // Config ready probe (avoid Logo early read)
281
298
  const [configReady, setConfigReady] = useState(false);
282
299
 
283
- // 启动/复用本地唯一服务,并注入 apiUrl(含重试机制)
300
+ // Boot/Reuse singleton local server (with retry)
284
301
  useEffect(() => {
285
302
  let mounted = true;
286
303
  (async () => {
287
304
  if (apiUrl) return;
288
305
  const entry = path.resolve(import.meta.dir, '../server/index.ts');
289
306
  const MAX_RETRIES = 3;
290
- const RETRY_DELAYS = [0, 2000, 5000]; // 首次无延迟,第2次2秒,第3次5秒
307
+ const RETRY_DELAYS = [0, 2000, 5000]; // 0s, 2s, 5s
291
308
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
292
309
  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 || '本地服务启动失败');
310
+ if (attempt > 0) {
311
+ setBootErr(`Attempt ${attempt} failed, retrying in ${RETRY_DELAYS[attempt] / 1000}s...`);
312
+ await new Promise(r => setTimeout(r, RETRY_DELAYS[attempt]));
313
+ }
314
+ if (!mounted) return;
315
+ try {
316
+ const handle = await ensureSingletonLocalServer({ serverEntry: entry });
317
+ if (!mounted) { await handle.stopIfLast(); return; }
318
+ setApiUrl(handle.baseUrl);
319
+ setStopIfLast(() => handle.stopIfLast);
320
+ setBootErr(null);
321
+ return; // Success, exit retry
322
+ } catch (e: any) {
323
+ if (attempt === MAX_RETRIES - 1) {
324
+ setBootErr(e.message || 'Local server failed to start');
325
+ }
308
326
  }
309
- }
310
327
  }
311
328
  })();
312
329
  return () => { mounted = false; if (stopIfLast) stopIfLast(); };
@@ -325,12 +342,12 @@ export const CliMenuManager: React.FC = () => {
325
342
  return () => { cancelled = true; };
326
343
  }, []);
327
344
 
328
- // 初始化标记
345
+ // Init marks
329
346
  useEffect(() => {
330
347
  setMarkedSessionIds(loadMarkedSessionIds());
331
348
  }, []);
332
349
 
333
- // 页面切换复位
350
+ // Page switch reset
334
351
  useEffect(() => {
335
352
  if (page === 'newSession') {
336
353
  setNewSessionId(null);
@@ -340,11 +357,11 @@ export const CliMenuManager: React.FC = () => {
340
357
  if (page !== 'settings') {
341
358
  setSettingsOffset(0);
342
359
  }
343
- // 关闭帮助覆盖层
360
+ // Close help overlay
344
361
  setShowHelp(false);
345
362
  }, [page]);
346
363
 
347
- // 历史页进入复位
364
+ // History page entry reset
348
365
  useEffect(() => {
349
366
  if (page === 'history') {
350
367
  setHistoryMenuStage('list');
@@ -357,14 +374,14 @@ export const CliMenuManager: React.FC = () => {
357
374
  }
358
375
  }, [page]);
359
376
 
360
- // 创建会话
377
+ // Create Session
361
378
  const onCreateSession = async () => {
362
379
  setCreating(true); setCreateErr(null);
363
380
  try {
364
381
  const fsReq = require('fs');
365
382
  const pathReq = require('path');
366
383
  const { spawn } = require('child_process');
367
- // import.meta.dir 定位包根,避免 process.cwd() 指向用户目录
384
+ // Use import.meta.dir for pkg root
368
385
  const pkgPath = pathReq.resolve(import.meta.dir, '../../package.json');
369
386
  const pkgJson = JSON.parse(fsReq.readFileSync(pkgPath, 'utf-8'));
370
387
  const bins = pkgJson.bin || {};
@@ -373,7 +390,7 @@ export const CliMenuManager: React.FC = () => {
373
390
  ? (bins['claude-haha'] ? 'claude-haha' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]))
374
391
  : (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
375
392
  const spawnCmd = isWin ? 'cmd' : 'sh';
376
- // Windows 直接调全局 bingocode 命令,不用 bun 前缀
393
+ // Windows calls global bingocode directly
377
394
  const spawnArgs = isWin ? ['/c', 'start', 'cmd', '/k', 'bingocode'] : ['-c', `${binName}`];
378
395
  const spawnEnv = await buildSpawnEnv();
379
396
  spawn(spawnCmd, spawnArgs, {
@@ -382,15 +399,15 @@ export const CliMenuManager: React.FC = () => {
382
399
  detached: true,
383
400
  stdio: 'ignore'
384
401
  }).unref();
385
- setNewSessionId('CLI已启动: ' + binName);
402
+ setNewSessionId('Started: ' + binName);
386
403
  } catch(e: any) {
387
- setCreateErr(e.message || '新建失败');
404
+ setCreateErr(e.message || 'Failed to create');
388
405
  } finally {
389
406
  setCreating(false);
390
407
  }
391
408
  };
392
409
 
393
- // 历史分页加载
410
+ // Paged loading for history
394
411
  useEffect(() => {
395
412
  if (page === 'history' && historyMenuStage === 'list') {
396
413
  setLoadingHist(true); setHistErr(null);
@@ -406,7 +423,7 @@ export const CliMenuManager: React.FC = () => {
406
423
  }
407
424
  setHistoryHasMore(!!pageData?.has_more);
408
425
  } catch (e: any) {
409
- setHistErr(e.message || '获取历史失败');
426
+ setHistErr(e.message || 'Failed to fetch history');
410
427
  } finally {
411
428
  setLoadingHist(false);
412
429
  }
@@ -447,7 +464,7 @@ export const CliMenuManager: React.FC = () => {
447
464
  setSessionMessages(msgs);
448
465
  }
449
466
  } catch (e: any) {
450
- if (!cancelled) setMsgsErr(e.message || '消息加载失败');
467
+ if (!cancelled) setMsgsErr(e.message || 'Failed to load messages');
451
468
  } finally {
452
469
  if (!cancelled) setLoadingMsgs(false);
453
470
  }
@@ -460,7 +477,7 @@ export const CliMenuManager: React.FC = () => {
460
477
  }
461
478
  }, [page, historyMenuStage, selectedHistory, apiUrl]);
462
479
 
463
- // 设置页数据
480
+ // Settings data
464
481
  useEffect(() => {
465
482
  if (page === 'settings') {
466
483
  setLoadingSetting(true); setSetErr(null);
@@ -469,7 +486,7 @@ export const CliMenuManager: React.FC = () => {
469
486
  const data = (await import('../utils/settings/settings')).default;
470
487
  setSettingData(data);
471
488
  } catch(e: any) {
472
- setSetErr(e.message||'获取设置失败');
489
+ setSetErr(e.message||'Failed to load settings');
473
490
  } finally { setLoadingSetting(false); }
474
491
  })();
475
492
  } else {
@@ -479,15 +496,21 @@ export const CliMenuManager: React.FC = () => {
479
496
  }
480
497
  }, [page]);
481
498
 
482
- // 键盘交互
499
+ // Keyboard interactions
483
500
  useInput((input, key) => {
484
- // 语言切换
501
+ // Language toggle
485
502
  if (input === 'l' || input === 'L') {
486
- setLang(l => (l === 'zh' ? 'en' : 'zh'));
503
+ const nextLang = lang === 'zh' ? 'en' : 'zh';
504
+ setLang(nextLang);
505
+ try {
506
+ const cfg = getGlobalConfig();
507
+ cfg.language = nextLang;
508
+ saveGlobalConfig(cfg);
509
+ } catch {}
487
510
  return;
488
511
  }
489
512
 
490
- // 主题切换(G
513
+ // Theme toggle (G)
491
514
  if ((input === 'g' || input === 'G')) {
492
515
  const order = ['light', 'dark', 'highContrast'] as const;
493
516
  const curr = String(theme || 'light');
@@ -502,27 +525,27 @@ export const CliMenuManager: React.FC = () => {
502
525
  return;
503
526
  }
504
527
 
505
- // 顶部动画开关(O
528
+ // Top animation toggle (O)
506
529
  if (input === 'o' || input === 'O') {
507
530
  setAnimEnabled(v => !v);
508
531
  return;
509
532
  }
510
- // 顶部 Tips 开关(T
533
+ // Top Tips toggle (T)
511
534
  if (input === 't' || input === 'T') {
512
535
  setTipsEnabled(v => !v);
513
536
  return;
514
537
  }
515
538
 
516
- // 帮助覆盖层(?)
539
+ // Help overlay (?)
517
540
  if (input === '?') {
518
541
  setShowHelp(v => !v);
519
542
  return;
520
543
  }
521
544
 
522
- // ESC 返回主页面或关闭帮助
545
+ // ESC to back or close help
523
546
  if (key.escape) {
524
547
  if (showHelp) { setShowHelp(false); return; }
525
- if (page === 'provider') return; // provider 内部处理
548
+ if (page === 'provider') return; // Handled internally
526
549
  setPage(null);
527
550
  setHistoryMenuStage('list');
528
551
  setSelectedHistory(null);
@@ -533,7 +556,7 @@ export const CliMenuManager: React.FC = () => {
533
556
  return;
534
557
  }
535
558
 
536
- // 快速入口:N 新建、R 恢复、P Provider
559
+ // Quick entries: N New, R Resume, P Provider
537
560
  if (input === 'n' || input === 'N') {
538
561
  setPage('newSession');
539
562
  onCreateSession();
@@ -549,7 +572,7 @@ export const CliMenuManager: React.FC = () => {
549
572
  return;
550
573
  }
551
574
 
552
- // 主菜单左右移动
575
+ // Main menu navigation
553
576
  if (!showHelp && key.leftArrow && page === null) {
554
577
  setNavIndex(i => (i - 1 + menuItems.length) % menuItems.length);
555
578
  return;
@@ -566,22 +589,32 @@ export const CliMenuManager: React.FC = () => {
566
589
  return;
567
590
  }
568
591
 
569
- // 历史页快捷键
592
+ // History shortcuts
570
593
  if (!showHelp && page === 'history') {
571
594
  if (historyMenuStage === 'list') {
595
+ if (key.downArrow || input === 'j') {
596
+ // Internal SelectInput handles cursor, we just need to track offset for ScrollBar
597
+ setListOffset(o => o + 1);
598
+ }
599
+ if (key.upArrow || input === 'k') {
600
+ setListOffset(o => Math.max(0, o - 1));
601
+ }
572
602
  if (input === 'q') {
573
603
  setPage(null);
574
604
  setHistoryMenuStage('list');
575
605
  setSelectedHistory(null);
576
606
  setHistoryCursor(null);
607
+ setListOffset(0);
577
608
  return;
578
609
  }
579
610
  if (input === 'j' && historyHasMore) {
580
611
  setHistoryCursor(historyList[historyList.length - 1]?.id || null);
612
+ setListOffset(0);
581
613
  return;
582
614
  }
583
615
  if (input === 'k') {
584
616
  setHistoryCursor(null);
617
+ setListOffset(0);
585
618
  return;
586
619
  }
587
620
  } else if (historyMenuStage === 'window') {
@@ -601,7 +634,7 @@ export const CliMenuManager: React.FC = () => {
601
634
  handleHistoryMenuAction('__back');
602
635
  return;
603
636
  }
604
- // 消息滚动:↑/k 向上翻页,↓/j 向下翻页
637
+ // Message scrolling
605
638
  if (key.upArrow || input === 'k') {
606
639
  setMsgsPage(p => Math.max(0, p - 1));
607
640
  return;
@@ -619,7 +652,7 @@ export const CliMenuManager: React.FC = () => {
619
652
  }
620
653
  }
621
654
 
622
- // 设置页滚动
655
+ // Settings scrolling
623
656
  if (!showHelp && page === 'settings' && settingData && typeof settingData === 'object') {
624
657
  const total = Object.keys(settingData).length;
625
658
  const visible = Math.max(1, MID_H - 1);
@@ -732,14 +765,14 @@ export const CliMenuManager: React.FC = () => {
732
765
  ];
733
766
  }
734
767
  const items = [
735
- ...groupToItems(today, '—— 今天 ——'),
736
- ...groupToItems(week, '—— 本周 ——'),
737
- ...groupToItems(earlier, '—— 更早 ——'),
768
+ ...groupToItems(today, '—— Today ——'),
769
+ ...groupToItems(week, '—— This Week ——'),
770
+ ...groupToItems(earlier, '—— Earlier ——'),
738
771
  ];
739
772
  return items;
740
773
  }, [historyList, markedSessionIds]);
741
774
 
742
- // 标记切换
775
+ // Toggle Mark
743
776
  const toggleMarkSession = (sessionId: string) => {
744
777
  setMarkedSessionIds(prev => {
745
778
  const next = new Set(prev);
@@ -779,7 +812,7 @@ export const CliMenuManager: React.FC = () => {
779
812
  };
780
813
 
781
814
 
782
- // 刷新历史
815
+ // Refresh history
783
816
  const refreshHistoryList = () => {
784
817
  setLoadingHist(true); setHistErr(null);
785
818
  let url = apiUrl + '/api/sessions';
@@ -789,11 +822,11 @@ export const CliMenuManager: React.FC = () => {
789
822
  setHistoryCursor(pageData?.first_id || null);
790
823
  setHistoryHasMore(!!pageData?.has_more);
791
824
  }).catch(e => {
792
- setHistErr(e.message || '获取历史失败');
825
+ setHistErr(e.message || 'Failed to fetch history');
793
826
  }).finally(() => setLoadingHist(false));
794
827
  };
795
828
 
796
- // 删除会话
829
+ // Delete Session
797
830
  const handleDeleteSession = (sessionId: string) => {
798
831
  const url = apiUrl.replace(/\/+$/, '') + '/api/sessions/' + sessionId;
799
832
  axios.delete(url)
@@ -806,18 +839,18 @@ export const CliMenuManager: React.FC = () => {
806
839
  });
807
840
  };
808
841
 
809
- // 二级菜单(底栏右侧)
842
+ // Secondary menu (bottom bar right)
810
843
  const secondaryMenu: SecondaryMenu = useMemo(() => {
811
844
  if (page === 'history' && historyMenuStage === 'window' && selectedHistory) {
812
845
  const isMarked = markedSessionIds.has(selectedHistory.id);
813
846
  const markLabel = isMarked ? i18nMap[lang].unmark : i18nMap[lang].mark;
814
847
  return {
815
- title: lang === 'zh' ? '会话操作' : 'Session Actions',
848
+ title: 'Session Actions',
816
849
  items: [
817
850
  { label: markLabel, value: '__toggle_mark' },
818
- { label: '→ 继续会话聊天', value: '__continue' },
819
- { label: '→ 删除会话聊天', value: '__delete' },
820
- { label: '← 返回历史列表', value: '__back' },
851
+ { label: '→ Continue session', value: '__continue' },
852
+ { label: '→ Delete session', value: '__delete' },
853
+ { label: '← Back to list', value: '__back' },
821
854
  ],
822
855
  onSelect: (item: any) => {
823
856
  if (item.value === '__back') {
@@ -837,10 +870,10 @@ export const CliMenuManager: React.FC = () => {
837
870
 
838
871
  if (page === 'history' && historyMenuStage === 'deleteConfirm' && selectedHistory) {
839
872
  return {
840
- title: lang === 'zh' ? '删除确认' : 'Confirm Delete',
873
+ title: 'Confirm Delete',
841
874
  items: [
842
- { label: lang === 'zh' ? '是,确认删除' : 'Yes, delete', value: '__confirm_delete' },
843
- { label: lang === 'zh' ? '否,返回详情' : 'No, back', value: '__cancel_delete' },
875
+ { label: 'Yes, delete', value: '__confirm_delete' },
876
+ { label: 'No, back', value: '__cancel_delete' },
844
877
  ],
845
878
  onSelect: (item: any) => {
846
879
  if (item.value === '__cancel_delete') {
@@ -854,34 +887,32 @@ export const CliMenuManager: React.FC = () => {
854
887
  return null;
855
888
  }, [page, historyMenuStage, selectedHistory, markedSessionIds, lang]);
856
889
 
857
- // 帮助覆盖层
890
+ // Help Overlay
858
891
  function renderHelpOverlay() {
859
892
  return (
860
893
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
861
894
  <Text color="magenta">{i18nMap[lang].helpTitle}</Text>
862
895
  <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>
896
+ <Text color="cyan">N</Text><Text> New Session</Text>
897
+ <Text color="cyan">R</Text><Text> Quick Resume</Text>
898
+ <Text color="cyan">P</Text><Text> Open Provider Config</Text>
899
+ <Text color="cyan">G</Text><Text> Toggle Theme (light/dark/highContrast)</Text>
900
+ <Text color="cyan">L</Text><Text> Toggle Language (en/zh)</Text>
901
+ <Text color="cyan">O</Text><Text> Toggle Top Animation</Text>
902
+ <Text color="cyan">T</Text><Text> Toggle Top Tips</Text>
903
+ <Text color="cyan">?</Text><Text> Toggle Help</Text>
871
904
  <Text> </Text>
872
- <Hint>{lang==='zh' ? 'ESC 关闭 · 在任意页面可用' : 'ESC to close · Works anywhere'}</Hint>
905
+ <Hint>ESC to close · Works anywhere</Hint>
873
906
  </Box>
874
907
  );
875
908
  }
876
909
 
877
- // 中心内容渲染
910
+ // Center Content
878
911
  function renderCenter() {
879
912
  if (showHelp) return renderHelpOverlay();
880
913
 
881
- // 首页:默认显示欢迎页图像,水平居中(WelcomeV2 固定 58 字符宽)
914
+ // Home: WelcomeV2 (58 cols wide)
882
915
  if (page === null) {
883
- // 首页 Panel 无边框无 padding,内容区宽 = VIEW_W = 96
884
- // WelcomeV2 宽 58,左偏移 = floor((96 - 58) / 2) = 19
885
916
  const WELCOME_W = 58;
886
917
  const leftPad = Math.max(0, Math.floor((VIEW_W - WELCOME_W) / 2));
887
918
  return (
@@ -891,75 +922,75 @@ export const CliMenuManager: React.FC = () => {
891
922
  <WelcomeV2 />
892
923
  </Box>
893
924
  {!apiUrl && !bootErr && (
894
- <Text color="yellow">⏳ 服务启动中...</Text>
925
+ <StateDisplay type="loading" message="Starting server..." />
895
926
  )}
896
927
  {bootErr && (
897
- <Text color="red">服务启动失败: {bootErr}</Text>
928
+ <StateDisplay type="error" message={`Server boot failed: ${bootErr}`} />
898
929
  )}
899
930
  </Box>
900
931
  );
901
932
  }
902
933
 
903
- // 新建
934
+ // New Session
904
935
  if (page === 'newSession') {
905
936
  return (
906
937
  <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>}
938
+ {creating && <StateDisplay type="loading" message="Creating..." />}
939
+ {createErr && <StateDisplay type="error" message={`Failed to create: ${createErr}`} />}
940
+ {newSessionId && <Box alignItems="center" justifyContent="center" flexGrow={1}><Text color="green">New Session: {newSessionId}</Text></Box>}
941
+ {!creating && !createErr && !newSessionId && <StateDisplay type="empty" message="Entered new session page, waiting for result..." />}
911
942
  </Box>
912
943
  );
913
944
  }
914
945
 
915
- // 历史
946
+ // History
916
947
  if (page === 'history') {
917
- if (histErr) return <Box width={VIEW_W} height={MID_H}><Text color="red">{histErr}</Text></Box>;
948
+ if (histErr) return <StateDisplay type="error" message={histErr} onRetry={refreshHistoryList} />;
918
949
  if (historyMenuStage === 'deleteConfirm' && selectedHistory) {
919
950
  const halfH = Math.floor(MID_H / 2);
920
951
  const items = [
921
- { label: lang === 'zh' ? '是,确认删除' : 'Yes, delete', value: '__confirm_delete' },
922
- { label: lang === 'zh' ? '否,返回详情' : 'No, back', value: '__cancel_delete' },
952
+ { label: 'Yes, delete', value: '__confirm_delete' },
953
+ { label: 'No, back', value: '__cancel_delete' },
923
954
  ];
924
955
  return (
925
956
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
926
957
  <Box height={halfH} flexDirection="column">
927
958
  <Text color="red">{i18nMap[lang].deleting}</Text>
928
959
  <Text>id: {selectedHistory.id}</Text>
929
- <Text>标题: {selectedHistory.title}</Text>
930
- <Text>创建时间: {selectedHistory.createdAt}</Text>
960
+ <Text>Title: {selectedHistory.title}</Text>
961
+ <Text>Created At: {selectedHistory.createdAt}</Text>
931
962
  </Box>
932
- <Box height={MID_H - halfH} flexDirection="column" borderStyle="round" borderColor="red" paddingLeft={1} paddingRight={1}>
933
- <Text>{lang === 'zh' ? '删除确认' : 'Confirm Delete'}</Text>
963
+ <Panel height={MID_H - halfH} borderStyle="round" borderColor="red" paddingX={1}>
964
+ <Text>Confirm Delete</Text>
934
965
  <SelectInput
935
966
  items={items}
936
967
  onSelect={(item) => handleHistoryMenuAction(String(item.value))}
937
968
  />
938
- <Hint>↩ 执行 · q 返回</Hint>
939
- </Box>
969
+ <Hint>↩ Enter · q Back</Hint>
970
+ </Panel>
940
971
  </Box>
941
972
  );
942
973
  }
943
974
  if (!historyList.length && loadingHist) {
944
- return <Box width={VIEW_W} height={MID_H}><Text color="yellow">加载中...</Text></Box>;
975
+ return <StateDisplay type="loading" message="Fetching history..." />;
945
976
  }
946
977
  if (!historyList.length) {
947
- return <Box width={VIEW_W} height={MID_H}><Text dimColor>{i18nMap[lang].emptyHistory}</Text></Box>;
978
+ return <StateDisplay type="empty" message={i18nMap[lang].emptyHistory} />;
948
979
  }
949
980
 
950
981
  if (historyMenuStage === 'window' && selectedHistory) {
951
982
  const isMarked = markedSessionIds.has(selectedHistory.id);
952
983
 
953
- // ── 信息栏高度固定 3 行(标题 + 元信息 + 提示) ──
984
+ // INFO_H: Title + Metadata + Hint (3 lines)
954
985
  const INFO_H = 3;
955
986
  const MSGS_H = Math.max(3, MID_H - INFO_H);
956
987
 
957
- // ── 过滤出可展示的消息(user / assistant / system,跳过 tool_use / tool_result) ──
988
+ // Filter messages
958
989
  const displayMsgs = sessionMessages.filter(
959
990
  m => m.type === 'user' || m.type === 'assistant' || m.type === 'system'
960
991
  );
961
992
 
962
- // ── 分页:每页 MSGS_PAGE_SIZE 条消息 ──
993
+ // Pagination
963
994
  const totalPages = Math.max(1, Math.ceil(displayMsgs.length / MSGS_PAGE_SIZE));
964
995
  const safePage = Math.min(msgsPage, totalPages - 1);
965
996
  const pageStart = safePage * MSGS_PAGE_SIZE;
@@ -967,24 +998,24 @@ export const CliMenuManager: React.FC = () => {
967
998
 
968
999
  return (
969
1000
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
970
- {/* ── 顶部信息栏 ── */}
1001
+ {/* Top Info Bar */}
971
1002
  <Box height={INFO_H} flexDirection="column">
972
1003
  <Text color={isMarked ? 'yellow' : 'cyan'}>
973
1004
  {isMarked ? '★ ' : ''}{selectedHistory.title || 'Untitled'}
974
1005
  <Text dimColor> {selectedHistory.createdAt?.slice(0, 16).replace('T', ' ') || ''} · {displayMsgs.length} msgs</Text>
975
1006
  </Text>
976
1007
  <Hint>
977
- j/↓ 下翻 · k/↑ 上翻 · m 标记 · c 继续 · d 删除 · q 返回
1008
+ j/↓ Down · k/↑ Up · m Mark · c Continue · d Delete · q Back
978
1009
  {displayMsgs.length > MSGS_PAGE_SIZE ? ` [${safePage + 1}/${totalPages}]` : ''}
979
1010
  </Hint>
980
1011
  </Box>
981
1012
 
982
- {/* ── 消息区 ── */}
1013
+ {/* Message Area */}
983
1014
  <Box height={MSGS_H} flexDirection="column">
984
- {loadingMsgs && <Text color="yellow">加载消息中...</Text>}
985
- {msgsErr && <Text color="red">错误: {msgsErr}</Text>}
1015
+ {loadingMsgs && <StateDisplay type="loading" message="Loading messages..." />}
1016
+ {msgsErr && <StateDisplay type="error" message={`Error: ${msgsErr}`} />}
986
1017
  {!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
987
- <Text dimColor>无消息记录</Text>
1018
+ <StateDisplay type="empty" message="No message history" />
988
1019
  )}
989
1020
  {pageMsgs.map((msg) => {
990
1021
  const text = extractTextFromContent(msg.content);
@@ -1010,32 +1041,35 @@ export const CliMenuManager: React.FC = () => {
1010
1041
  }
1011
1042
 
1012
1043
 
1013
- // 历史列表
1044
+ // History List
1014
1045
  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>
1046
+ <Box width={VIEW_W} height={MID_H} flexDirection="row">
1047
+ <Box flexDirection="column" flexGrow={1}>
1048
+ <SelectInput
1049
+ key={`${historyCursor ?? 'first'}:${groupedHistoryItems.length}`}
1050
+ items={groupedHistoryItems}
1051
+ onSelect={item => {
1052
+ if (String(item.value).startsWith('__group_')) return; // Ignore group headers
1053
+ const session = historyList.find(h => h.id === item.value);
1054
+ if (session) {
1055
+ setSelectedHistory(session);
1056
+ setHistoryMenuStage('window');
1057
+ }
1058
+ }}
1059
+ itemComponent={({ isSelected, label }) => {
1060
+ const it = groupedHistoryItems.find(i => i.label === label);
1061
+ const isGroup = it?.isGroup;
1062
+ const color = it?.color;
1063
+ return (
1064
+ <Text color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
1065
+ {label}
1066
+ </Text>
1067
+ )
1068
+ }}
1069
+ />
1070
+ <Hint>{i18nMap[lang].historyHint}</Hint>
1071
+ </Box>
1072
+ <ScrollBar total={groupedHistoryItems.length} offset={listOffset} height={MID_H} />
1039
1073
  </Box>
1040
1074
  );
1041
1075
  }
@@ -1045,8 +1079,12 @@ export const CliMenuManager: React.FC = () => {
1045
1079
  if (!apiUrl) {
1046
1080
  return (
1047
1081
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1048
- <Text color="yellow">{bootErr ? `服务启动失败: ${bootErr}` : '⏳ 服务启动中,请稍候...'}</Text>
1049
- <Text dimColor>ESC 返回主菜单</Text>
1082
+ <StateDisplay
1083
+ type={bootErr ? "error" : "loading"}
1084
+ message={bootErr ? `Server boot failed: ${bootErr}` : 'Starting server, please wait...'}
1085
+ onRetry={() => process.exit(1)} // Or another way to trigger reboot
1086
+ />
1087
+ <Text dimColor alignSelf="center">ESC for main menu</Text>
1050
1088
  </Box>
1051
1089
  );
1052
1090
  }
@@ -1057,61 +1095,62 @@ export const CliMenuManager: React.FC = () => {
1057
1095
  );
1058
1096
  }
1059
1097
 
1060
- // 设置
1098
+ // Settings
1061
1099
  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>;
1100
+ if (loadingSetting) return <StateDisplay type="loading" message="Loading settings..." />;
1101
+ if (setErr) return <StateDisplay type="error" message={setErr} />;
1102
+ if (!settingData || typeof settingData !== 'object') return <StateDisplay type="empty" message="No settings found" />;
1065
1103
  const entries = Object.entries(settingData);
1066
1104
  const visible = Math.max(1, MID_H - 1);
1067
1105
  const start = Math.min(settingsOffset, Math.max(0, entries.length - visible));
1068
1106
  const sliced = entries.slice(start, start + visible);
1069
1107
  return (
1070
1108
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1109
+ <ScrollBar total={entries.length} offset={start} height={visible} />
1071
1110
  {sliced.map(([k, v]) => <Text key={k}>{k}: {typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>)}
1072
1111
  <Hint>
1073
- {lang==='zh' ? '↑/k 与 ↓/j 滚动' : '↑/k and ↓/j to scroll'} · {start+1}-{Math.min(start+visible, entries.length)}/{entries.length}
1112
+ ↑/k and ↓/j scroll · {start+1}-{Math.min(start+visible, entries.length)}/{entries.length}
1074
1113
  </Hint>
1075
1114
  </Box>
1076
1115
  );
1077
1116
  }
1078
1117
 
1079
- // 关于
1118
+ // About
1080
1119
  if (page === 'about') {
1081
1120
  return (
1082
1121
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1083
1122
  <Text>{i18nMap[lang].about}</Text>
1084
1123
  <Hint>
1085
- {lang==='zh' ? 'API 地址' : 'API Base'}: {apiUrl}
1124
+ API Base: {apiUrl}
1086
1125
  </Hint>
1087
1126
  </Box>
1088
1127
  );
1089
1128
  }
1090
1129
 
1091
- // 退出
1130
+ // Exit
1092
1131
  if (page === 'exit') {
1093
1132
  exit();
1094
- return <Box width={VIEW_W} height={MID_H}><Text>退出...</Text></Box>;
1133
+ return <Box width={VIEW_W} height={MID_H}><Text>Exiting...</Text></Box>;
1095
1134
  }
1096
1135
 
1097
1136
  return <Box width={VIEW_W} height={MID_H} />;
1098
1137
  }
1099
1138
 
1100
- // 退出逻辑
1139
+ // Exit logic
1101
1140
  if (terminalSize.columns < 60 || terminalSize.rows < 15) {
1102
1141
  return (
1103
1142
  <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>
1143
+ <Text color="red">Terminal too small!</Text>
1144
+ <Text>Current: {terminalSize.columns}x{terminalSize.rows}</Text>
1145
+ <Text>Please resize to continue...</Text>
1107
1146
  </Box>
1108
1147
  );
1109
1148
  }
1110
1149
 
1111
- // 根渲染:上-中-下三段
1150
+ // Root Render
1112
1151
  return (
1113
1152
  <Box flexDirection="column" width={VIEW_W}>
1114
- {/* 顶部欢迎/Logo + 工具栏 */}
1153
+ {/* Top Welcome / Logo Area + Toolbar */}
1115
1154
  <TopBar
1116
1155
  ready={configReady}
1117
1156
  page={page}
@@ -1130,9 +1169,7 @@ export const CliMenuManager: React.FC = () => {
1130
1169
  }
1131
1170
  />
1132
1171
 
1133
- {/* 中心业务区(固定高度,内部处理分页/滚动)
1134
- 首页:无边框无 margin,WelcomeV2 直接撑满,避免 border 额外占行导致超出;
1135
- 其它页面:single border + marginY */}
1172
+ {/* Center Center Area */}
1136
1173
  {page === null ? (
1137
1174
  <Panel width={VIEW_W} height={MID_H} noBorder paddingX={0} paddingY={0} marginY={0}>
1138
1175
  {renderCenter()}
@@ -1143,7 +1180,7 @@ export const CliMenuManager: React.FC = () => {
1143
1180
  </Panel>
1144
1181
  )}
1145
1182
 
1146
- {/* 底部菜单与二级菜单 */}
1183
+ {/* Bottom Menu & Secondary Menu */}
1147
1184
  <BottomBar
1148
1185
  width={VIEW_W}
1149
1186
  height={BOTTOM_H}