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.
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +210 -174
- package/src/components/HelpV2/General.tsx +16 -5
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +11 -2
- package/src/manager/CliMenuManager.tsx +244 -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,45 @@ 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>('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(
|
|
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(
|
|
241
|
+
const id = setInterval(() => setNowStr(new Date().toLocaleString('en-US', { hour12: false })), 1000);
|
|
228
242
|
return () => clearInterval(id);
|
|
229
|
-
}, [
|
|
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);
|
|
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
|
-
//
|
|
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
|
-
//
|
|
297
|
+
// Config ready probe (avoid Logo early read)
|
|
281
298
|
const [configReady, setConfigReady] = useState(false);
|
|
282
299
|
|
|
283
|
-
//
|
|
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]; //
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
//
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
528
|
+
// Top animation toggle (O)
|
|
506
529
|
if (input === 'o' || input === 'O') {
|
|
507
530
|
setAnimEnabled(v => !v);
|
|
508
531
|
return;
|
|
509
532
|
}
|
|
510
|
-
//
|
|
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; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
848
|
+
title: 'Session Actions',
|
|
816
849
|
items: [
|
|
817
850
|
{ label: markLabel, value: '__toggle_mark' },
|
|
818
|
-
{ label: '→
|
|
819
|
-
{ label: '→
|
|
820
|
-
{ label: '←
|
|
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:
|
|
873
|
+
title: 'Confirm Delete',
|
|
841
874
|
items: [
|
|
842
|
-
{ label:
|
|
843
|
-
{ label:
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
-
//
|
|
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
|
-
<
|
|
925
|
+
<StateDisplay type="loading" message="Starting server..." />
|
|
895
926
|
)}
|
|
896
927
|
{bootErr && (
|
|
897
|
-
<
|
|
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 && <
|
|
908
|
-
{createErr && <
|
|
909
|
-
{newSessionId && <Text color="green"
|
|
910
|
-
{!creating && !createErr && !newSessionId && <
|
|
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 <
|
|
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:
|
|
922
|
-
{ label:
|
|
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
|
|
930
|
-
<Text
|
|
960
|
+
<Text>Title: {selectedHistory.title}</Text>
|
|
961
|
+
<Text>Created At: {selectedHistory.createdAt}</Text>
|
|
931
962
|
</Box>
|
|
932
|
-
<
|
|
933
|
-
<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>↩
|
|
939
|
-
</
|
|
969
|
+
<Hint>↩ Enter · q Back</Hint>
|
|
970
|
+
</Panel>
|
|
940
971
|
</Box>
|
|
941
972
|
);
|
|
942
973
|
}
|
|
943
974
|
if (!historyList.length && loadingHist) {
|
|
944
|
-
return <
|
|
975
|
+
return <StateDisplay type="loading" message="Fetching history..." />;
|
|
945
976
|
}
|
|
946
977
|
if (!historyList.length) {
|
|
947
|
-
return <
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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/↓
|
|
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 && <
|
|
985
|
-
{msgsErr && <
|
|
1015
|
+
{loadingMsgs && <StateDisplay type="loading" message="Loading messages..." />}
|
|
1016
|
+
{msgsErr && <StateDisplay type="error" message={`Error: ${msgsErr}`} />}
|
|
986
1017
|
{!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
|
|
987
|
-
<
|
|
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="
|
|
1016
|
-
<
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
{
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
<
|
|
1049
|
-
|
|
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 <
|
|
1063
|
-
if (setErr) return <
|
|
1064
|
-
if (!settingData || typeof settingData !== 'object') return <
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
1105
|
-
<Text
|
|
1106
|
-
<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
|
-
{/*
|
|
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}
|