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.
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +193 -174
- package/src/components/HelpV2/General.tsx +16 -5
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +11 -2
- package/src/manager/CliMenuManager.tsx +230 -207
- package/src/manager/CliMenuUi.tsx +87 -27
- package/src/manager/TopToolbar.tsx +6 -6
- package/src/server/cli/providersMenu.tsx +46 -46
- package/src/server/config/providers.yaml +18 -18
- package/src/server/ensureSingletonLocalServer.ts +2 -2
- package/src/utils/config.ts +3 -0
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
18
|
+
// Theme switching (Hook)
|
|
19
19
|
import { useTheme } from '../components/design-system/ThemeProvider.js';
|
|
20
|
-
// Markdown
|
|
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
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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; //
|
|
45
|
+
return true; // Cannot read settings.json -> Treat as official mode
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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]
|
|
147
|
+
console.error('[saveMarkedSessionIds] Save failed:', err);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
217
|
+
// Theme (Global Hook)
|
|
218
218
|
const [theme, setTheme] = useTheme();
|
|
219
219
|
|
|
220
|
-
//
|
|
221
|
-
const [lang, setLang] = useState<Lang>('
|
|
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(
|
|
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(
|
|
227
|
+
const id = setInterval(() => setNowStr(new Date().toLocaleString('en-US', { hour12: false })), 1000);
|
|
228
228
|
return () => clearInterval(id);
|
|
229
|
-
}, [
|
|
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);
|
|
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
|
-
//
|
|
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
|
-
//
|
|
283
|
+
// Config ready probe (avoid Logo early read)
|
|
281
284
|
const [configReady, setConfigReady] = useState(false);
|
|
282
285
|
|
|
283
|
-
//
|
|
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]; //
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
//
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
514
|
+
// Top animation toggle (O)
|
|
506
515
|
if (input === 'o' || input === 'O') {
|
|
507
516
|
setAnimEnabled(v => !v);
|
|
508
517
|
return;
|
|
509
518
|
}
|
|
510
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
834
|
+
title: 'Session Actions',
|
|
816
835
|
items: [
|
|
817
836
|
{ label: markLabel, value: '__toggle_mark' },
|
|
818
|
-
{ label: '→
|
|
819
|
-
{ label: '→
|
|
820
|
-
{ label: '←
|
|
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:
|
|
859
|
+
title: 'Confirm Delete',
|
|
841
860
|
items: [
|
|
842
|
-
{ label:
|
|
843
|
-
{ label:
|
|
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>
|
|
864
|
-
<Text color="cyan">R</Text><Text>
|
|
865
|
-
<Text color="cyan">P</Text><Text>
|
|
866
|
-
<Text color="cyan">G</Text><Text>
|
|
867
|
-
<Text color="cyan">L</Text><Text>
|
|
868
|
-
<Text color="cyan">O</Text><Text>
|
|
869
|
-
<Text color="cyan">T</Text><Text>
|
|
870
|
-
<Text color="cyan">?</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>
|
|
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
|
-
//
|
|
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
|
-
<
|
|
911
|
+
<StateDisplay type="loading" message="Starting server..." />
|
|
895
912
|
)}
|
|
896
913
|
{bootErr && (
|
|
897
|
-
<
|
|
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 && <
|
|
908
|
-
{createErr && <
|
|
909
|
-
{newSessionId && <Text color="green"
|
|
910
|
-
{!creating && !createErr && !newSessionId && <
|
|
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 <
|
|
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:
|
|
922
|
-
{ label:
|
|
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
|
|
930
|
-
<Text
|
|
946
|
+
<Text>Title: {selectedHistory.title}</Text>
|
|
947
|
+
<Text>Created At: {selectedHistory.createdAt}</Text>
|
|
931
948
|
</Box>
|
|
932
|
-
<
|
|
933
|
-
<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>↩
|
|
939
|
-
</
|
|
955
|
+
<Hint>↩ Enter · q Back</Hint>
|
|
956
|
+
</Panel>
|
|
940
957
|
</Box>
|
|
941
958
|
);
|
|
942
959
|
}
|
|
943
960
|
if (!historyList.length && loadingHist) {
|
|
944
|
-
return <
|
|
961
|
+
return <StateDisplay type="loading" message="Fetching history..." />;
|
|
945
962
|
}
|
|
946
963
|
if (!historyList.length) {
|
|
947
|
-
return <
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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/↓
|
|
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 && <
|
|
985
|
-
{msgsErr && <
|
|
1001
|
+
{loadingMsgs && <StateDisplay type="loading" message="Loading messages..." />}
|
|
1002
|
+
{msgsErr && <StateDisplay type="error" message={`Error: ${msgsErr}`} />}
|
|
986
1003
|
{!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
|
|
987
|
-
<
|
|
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="
|
|
1016
|
-
<
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
{
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
<
|
|
1049
|
-
|
|
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 <
|
|
1063
|
-
if (setErr) return <
|
|
1064
|
-
if (!settingData || typeof settingData !== 'object') return <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
1105
|
-
<Text
|
|
1106
|
-
<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
|
-
{/*
|
|
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}
|