bingocode 1.1.200-beta.2 → 1.1.200-beta.21
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 +813 -813
- package/src/commands/brain/brain.ts +4 -9
- package/src/components/PromptInput/PromptInput.tsx +1 -1
- package/src/constants/prompts.ts +31 -19
- package/src/hooks/useMergedTools.ts +3 -1
- package/src/main.tsx +4693 -4691
- package/src/manager/CliMenuManager.tsx +1589 -1583
- package/src/screens/REPL.tsx +5010 -5008
- package/src/screens/ResumeConversation.tsx +7 -1
- package/src/server/api/providers.ts +24 -0
- package/src/server/config/providers.yaml +21 -0
- package/src/server/proxy/handler.ts +16 -3
- package/src/server/services/providerService.ts +189 -50
- package/src/tools/AgentTool/UI.tsx +73 -8
- package/src/tools/AgentTool/agentToolUtils.ts +2 -2
- package/src/tools.ts +397 -390
- package/src/types/logs.ts +1 -0
- package/src/utils/attachments.ts +3 -2
- package/src/utils/conversationRecovery.ts +1 -0
- package/src/utils/sessionRestore.ts +2 -0
- package/src/utils/sessionStorage.ts +5180 -5105
- package/src/utils/tokens.ts +23 -0
|
@@ -1,1584 +1,1590 @@
|
|
|
1
|
-
//@C:M ID=M.CM.CliMenuManager;K=M;V=1.5;P=module;D=CLI;M=cli;S=main
|
|
2
|
-
import React, { useState, useEffect, useMemo } from 'react';
|
|
3
|
-
import axios from 'axios';
|
|
4
|
-
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
|
-
import SelectInput from 'ink-select-input';
|
|
6
|
-
import ProviderPanel from '../cli/ProviderPanel.tsx';
|
|
7
|
-
import { LogoV2 } from '../components/LogoV2/LogoV2.tsx';
|
|
8
|
-
import { CondensedLogo } from '../components/LogoV2/CondensedLogo.tsx';
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import os from 'os';
|
|
12
|
-
import { ensureSingletonLocalServer } from '../server/ensureSingletonLocalServer.ts';
|
|
13
|
-
// New: Common UI elements and top toolbar
|
|
14
|
-
import { TopBar, BottomBar, Panel, Hint, Kbd, SecondaryMenu, StateDisplay, ScrollBar, truncate, safePadEnd } from '../manager/CliMenuUi.tsx';
|
|
15
|
-
import { WelcomeV2 } from '../components/LogoV2/WelcomeV2.tsx';
|
|
16
|
-
import { TopToolbar } from '../manager/TopToolbar.tsx';
|
|
17
|
-
|
|
18
|
-
// Theme switching (Hook)
|
|
19
|
-
import { useTheme } from '../components/design-system/ThemeProvider.js';
|
|
20
|
-
// Markdown rendering (Pure function, no AppStateProvider context dependency)
|
|
21
|
-
import { applyMarkdown } from '../utils/markdown.js';
|
|
22
|
-
import { Ansi } from '../ink/Ansi.js';
|
|
23
|
-
|
|
24
|
-
// Config related (using available interfaces)
|
|
25
|
-
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
fs.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
fs.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fs.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{ label: '
|
|
190
|
-
{ label: '
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'
|
|
207
|
-
'
|
|
208
|
-
'
|
|
209
|
-
'
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'
|
|
249
|
-
'
|
|
250
|
-
'
|
|
251
|
-
'
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'
|
|
291
|
-
'
|
|
292
|
-
'
|
|
293
|
-
'
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
type
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (block.type === '
|
|
371
|
-
if (block.type === '
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
.
|
|
377
|
-
.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const [
|
|
414
|
-
const [
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (typeof cfg.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
const [
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const [
|
|
474
|
-
const [
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const [
|
|
479
|
-
const [
|
|
480
|
-
const [
|
|
481
|
-
const [
|
|
482
|
-
const [
|
|
483
|
-
const [
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const [
|
|
488
|
-
const [
|
|
489
|
-
const [
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const [
|
|
497
|
-
const [
|
|
498
|
-
const [
|
|
499
|
-
const [
|
|
500
|
-
const [
|
|
501
|
-
const [
|
|
502
|
-
const [
|
|
503
|
-
const [
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const [
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
const [
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
});
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
//
|
|
767
|
-
if (input === '
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (input === '
|
|
802
|
-
setPage('
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
setListOffset(0);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
setHistoryCursor(null);
|
|
849
|
-
setListOffset(0);
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (input === '
|
|
881
|
-
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if (
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
? ['
|
|
1013
|
-
: ['-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
return
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
<Text color="
|
|
1199
|
-
<Text
|
|
1200
|
-
<Text color="cyan">
|
|
1201
|
-
<Text color="cyan">
|
|
1202
|
-
<Text color="cyan"
|
|
1203
|
-
<Text> </Text>
|
|
1204
|
-
<
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
<
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
<
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
]
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
key:
|
|
1454
|
-
label:
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1
|
+
//@C:M ID=M.CM.CliMenuManager;K=M;V=1.5;P=module;D=CLI;M=cli;S=main
|
|
2
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
5
|
+
import SelectInput from 'ink-select-input';
|
|
6
|
+
import ProviderPanel from '../cli/ProviderPanel.tsx';
|
|
7
|
+
import { LogoV2 } from '../components/LogoV2/LogoV2.tsx';
|
|
8
|
+
import { CondensedLogo } from '../components/LogoV2/CondensedLogo.tsx';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import { ensureSingletonLocalServer } from '../server/ensureSingletonLocalServer.ts';
|
|
13
|
+
// New: Common UI elements and top toolbar
|
|
14
|
+
import { TopBar, BottomBar, Panel, Hint, Kbd, SecondaryMenu, StateDisplay, ScrollBar, truncate, safePadEnd } from '../manager/CliMenuUi.tsx';
|
|
15
|
+
import { WelcomeV2 } from '../components/LogoV2/WelcomeV2.tsx';
|
|
16
|
+
import { TopToolbar } from '../manager/TopToolbar.tsx';
|
|
17
|
+
|
|
18
|
+
// Theme switching (Hook)
|
|
19
|
+
import { useTheme } from '../components/design-system/ThemeProvider.js';
|
|
20
|
+
// Markdown rendering (Pure function, no AppStateProvider context dependency)
|
|
21
|
+
import { applyMarkdown } from '../utils/markdown.js';
|
|
22
|
+
import { Ansi } from '../ink/Ansi.js';
|
|
23
|
+
|
|
24
|
+
// Config related (using available interfaces)
|
|
25
|
+
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
|
|
26
|
+
import { logError } from '../utils/log.js';
|
|
27
|
+
|
|
28
|
+
// markedSessions stored in ~/.claude-cli/ fixed directory, regardless of cwd
|
|
29
|
+
const MARKED_FILE = path.join(os.homedir(), '.claude-cli', 'markedSessions.json');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the path to ~/.claude/bingo/settings.json (offline persistence for language
|
|
33
|
+
* and auto-mode settings, same file used by provider service). This ensures
|
|
34
|
+
* these settings survive across full restarts.
|
|
35
|
+
*/
|
|
36
|
+
function getBingoSettingsPath(): string {
|
|
37
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
38
|
+
return path.join(configDir, 'bingo', 'settings.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readBingoSettings(): Record<string, unknown> {
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(getBingoSettingsPath(), 'utf-8');
|
|
44
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeBingoSettings(updates: Record<string, unknown>): void {
|
|
51
|
+
const p = getBingoSettingsPath();
|
|
52
|
+
const dir = path.dirname(p);
|
|
53
|
+
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
|
|
54
|
+
|
|
55
|
+
let current: Record<string, unknown> = {};
|
|
56
|
+
try {
|
|
57
|
+
if (fs.existsSync(p)) {
|
|
58
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
59
|
+
current = JSON.parse(raw) as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
const merged = { ...current, ...updates };
|
|
64
|
+
|
|
65
|
+
// atomic write via temp + rename
|
|
66
|
+
const tmp = `${p}.tmp.${Date.now()}`;
|
|
67
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
68
|
+
fs.renameSync(tmp, p);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// write yml
|
|
72
|
+
function readGlobalClaudeConfig(): Record<string, unknown> {
|
|
73
|
+
const configPath = path.join(os.homedir(), '.claude.json');
|
|
74
|
+
try {
|
|
75
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
76
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// write merge config directly to ~/.claude.json (atomic write)
|
|
83
|
+
function writeGlobalClaudeConfig(updates: Record<string, unknown>): void {
|
|
84
|
+
const configPath = path.join(os.homedir(), '.claude.json');
|
|
85
|
+
const dir = path.dirname(configPath);
|
|
86
|
+
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
|
|
87
|
+
|
|
88
|
+
let current: Record<string, unknown> = {};
|
|
89
|
+
try {
|
|
90
|
+
if (fs.existsSync(configPath)) {
|
|
91
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
92
|
+
current = JSON.parse(raw) as Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
const merged = { ...current, ...updates };
|
|
97
|
+
|
|
98
|
+
// atomic write via temp + rename
|
|
99
|
+
const tmp = `${configPath}.tmp.${Date.now()}`;
|
|
100
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
101
|
+
fs.renameSync(tmp, configPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readClaudeSettings(): Record<string, unknown> {
|
|
105
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
106
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
107
|
+
try {
|
|
108
|
+
const raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
109
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
110
|
+
} catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeClaudeSettings(updates: Record<string, unknown>): void {
|
|
116
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
117
|
+
const settingsPath = path.join(configDir, 'settings.json');
|
|
118
|
+
const dir = path.dirname(settingsPath);
|
|
119
|
+
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
|
|
120
|
+
|
|
121
|
+
let current: Record<string, unknown> = {};
|
|
122
|
+
try {
|
|
123
|
+
if (fs.existsSync(settingsPath)) {
|
|
124
|
+
const raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
125
|
+
current = JSON.parse(raw) as Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
const merged = { ...current, ...updates };
|
|
130
|
+
|
|
131
|
+
// atomic write via temp + rename
|
|
132
|
+
const tmp = `${settingsPath}.tmp.${Date.now()}`;
|
|
133
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
134
|
+
fs.renameSync(tmp, settingsPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Determine if in "official" mode (no custom provider active).
|
|
139
|
+
* Logic matches ConversationService.shouldMarkManagedOAuth().
|
|
140
|
+
*/
|
|
141
|
+
function isOfficialMode(): boolean {
|
|
142
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
143
|
+
const settingsPath = path.join(configDir, 'bingo', 'settings.json');
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
146
|
+
const parsed = JSON.parse(raw) as { env?: Record<string, string> };
|
|
147
|
+
const env = parsed.env ?? {};
|
|
148
|
+
const hasProviderEnv = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL']
|
|
149
|
+
.some(key => typeof env[key] === 'string' && env[key]!.trim().length > 0);
|
|
150
|
+
return !hasProviderEnv;
|
|
151
|
+
} catch {
|
|
152
|
+
return true; // Cannot read settings.json -> Treat as official mode
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build spawn env for child process.
|
|
158
|
+
* In official mode, inject CLAUDE_CODE_ENTRYPOINT=claude-desktop + CLAUDE_CODE_OAUTH_TOKEN,
|
|
159
|
+
* so new/resumed bingocode windows can use OAuth directly.
|
|
160
|
+
*/
|
|
161
|
+
async function buildSpawnEnv(): Promise<NodeJS.ProcessEnv> {
|
|
162
|
+
const base = { ...process.env };
|
|
163
|
+
if (!isOfficialMode()) return base;
|
|
164
|
+
|
|
165
|
+
// Official mode: mark as managed-OAuth and inject OAuth token
|
|
166
|
+
base.CLAUDE_CODE_ENTRYPOINT = 'claude-desktop';
|
|
167
|
+
try {
|
|
168
|
+
const { hahaOAuthService } = await import('../server/services/hahaOAuthService.js');
|
|
169
|
+
const token = await hahaOAuthService.ensureFreshAccessToken();
|
|
170
|
+
if (token) {
|
|
171
|
+
base.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
172
|
+
} else {
|
|
173
|
+
// No valid token -> don't inject, use normal login flow
|
|
174
|
+
delete base.CLAUDE_CODE_OAUTH_TOKEN;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
delete base.CLAUDE_CODE_OAUTH_TOKEN;
|
|
178
|
+
}
|
|
179
|
+
return base;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Top height: Home = Clawd(3 rows) + border(2) = 5; Compact = 1 row + border(2) = 3
|
|
183
|
+
const TOP_H_HOME = Number(process.env.CLI_TOP_H_HOME || 5);
|
|
184
|
+
const TOP_H_COMPACT = Number(process.env.CLI_TOP_H_COMPACT || 3);
|
|
185
|
+
// Bottom bar height
|
|
186
|
+
const BOTTOM_H = Number(process.env.CLI_BOTTOM_H || 3);
|
|
187
|
+
|
|
188
|
+
const LANG_OPTIONS = [
|
|
189
|
+
{ label: 'English', value: 'en' as const },
|
|
190
|
+
{ label: '中文', value: 'zh' as const },
|
|
191
|
+
{ label: '日本語', value: 'ja' as const },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const i18nMap = {
|
|
195
|
+
zh: {
|
|
196
|
+
menu: {
|
|
197
|
+
newSession: '新建会话',
|
|
198
|
+
history: '会话历史',
|
|
199
|
+
provider: 'API 配置',
|
|
200
|
+
settings: '设置',
|
|
201
|
+
about: '关于',
|
|
202
|
+
exit: '退出',
|
|
203
|
+
},
|
|
204
|
+
about: 'Bingo CLI 终端 - 版本信息与关于',
|
|
205
|
+
aboutContent: [
|
|
206
|
+
'Bingo 是一款 AI 助手终端客户端。',
|
|
207
|
+
'1. API 配置:按 "P" 或选择「API 配置」来设置你的密钥。',
|
|
208
|
+
'2. 模型槽:在 Provider 面板中配置各模型。',
|
|
209
|
+
'3. 后台服务:Bingo 会运行一个本地服务器来管理会话。',
|
|
210
|
+
'4. 开始聊天:在任意终端中运行 `bingocode` 或 `claude`。',
|
|
211
|
+
].join('\n'),
|
|
212
|
+
aboutFooter: '作者: leanchy (leanchy07@outlook.com) · github.com/leanchy/claude-code-bingo',
|
|
213
|
+
mark: '→ 标记会话',
|
|
214
|
+
unmark: '→ 取消标记',
|
|
215
|
+
tipsSimple: 'L 语言 | ESC 返回 | ←→ 菜单 | ↩ 确认 | ? 帮助',
|
|
216
|
+
noData: '暂无数据',
|
|
217
|
+
emptyHistory: '还没有会话,要新建一个吗?',
|
|
218
|
+
deleting: '确定删除此会话?(不可恢复)',
|
|
219
|
+
historyHint: '↩ 打开 · j 下一页 · k 首页 · q 返回',
|
|
220
|
+
helpTitle: '快捷键',
|
|
221
|
+
// Settings page
|
|
222
|
+
settingsTitle: '设置',
|
|
223
|
+
langLabel: '语言',
|
|
224
|
+
langPickerTitle: '选择语言',
|
|
225
|
+
settingsHint: '↑/k ↓/j 滚动 · ↩ 切换 · ESC 返回',
|
|
226
|
+
langOptions: LANG_OPTIONS,
|
|
227
|
+
autoModeLabel: 'Auto Mode',
|
|
228
|
+
autoModeOn: '已开启',
|
|
229
|
+
autoModeOff: '已关闭',
|
|
230
|
+
bypassPermsLabel: 'Bypass',
|
|
231
|
+
bypassPermsOn: '已开启',
|
|
232
|
+
bypassPermsOff: '已关闭',
|
|
233
|
+
vscodeLabel: 'Connect to VS Code',
|
|
234
|
+
vscodeOn: '已连接',
|
|
235
|
+
vscodeOff: '未连接',
|
|
236
|
+
},
|
|
237
|
+
en: {
|
|
238
|
+
menu: {
|
|
239
|
+
newSession: 'New Session',
|
|
240
|
+
history: 'Session History',
|
|
241
|
+
provider: 'API Config',
|
|
242
|
+
settings: 'Settings',
|
|
243
|
+
about: 'About',
|
|
244
|
+
exit: 'Exit',
|
|
245
|
+
},
|
|
246
|
+
about: 'Bingo CLI Terminal - Version Info & About',
|
|
247
|
+
aboutContent: [
|
|
248
|
+
'Bingo is an AI assistant terminal client.',
|
|
249
|
+
'1. API Config: Press "P" or select "API Config" to set up your keys.',
|
|
250
|
+
'2. Model Slots: Configure specific models in the Provider panel.',
|
|
251
|
+
'3. Background Service: Bingo runs a local server to manage sessions.',
|
|
252
|
+
'4. Start Chat: Run `bingocode` or `claude` in any terminal to start.',
|
|
253
|
+
].join('\n'),
|
|
254
|
+
aboutFooter: 'Author: leanchy (leanchy07@outlook.com) · github.com/leanchy/claude-code-bingo',
|
|
255
|
+
mark: '→ Mark Session',
|
|
256
|
+
unmark: '→ Unmark Session',
|
|
257
|
+
tipsSimple: 'L Lang | ESC Back | ←→ Menu | ↩ Enter | ? Help',
|
|
258
|
+
noData: 'No data',
|
|
259
|
+
emptyHistory: 'Nothing here yet. Start a new session?',
|
|
260
|
+
deleting: 'Delete this session? (Irreversible)',
|
|
261
|
+
historyHint: 'Enter to open · j next · k first · q back',
|
|
262
|
+
helpTitle: 'Shortcuts',
|
|
263
|
+
// Settings page
|
|
264
|
+
settingsTitle: 'Settings',
|
|
265
|
+
langLabel: 'Language',
|
|
266
|
+
langPickerTitle: 'Select Language',
|
|
267
|
+
settingsHint: '↑/k ↓/j scroll · ↩ toggle · ESC back',
|
|
268
|
+
langOptions: LANG_OPTIONS,
|
|
269
|
+
autoModeLabel: 'Auto Mode',
|
|
270
|
+
autoModeOn: 'Enabled',
|
|
271
|
+
autoModeOff: 'Disabled',
|
|
272
|
+
bypassPermsLabel: 'Bypass',
|
|
273
|
+
bypassPermsOn: 'Enabled',
|
|
274
|
+
bypassPermsOff: 'Disabled',
|
|
275
|
+
vscodeLabel: 'Connect to VS Code',
|
|
276
|
+
vscodeOn: 'Connected',
|
|
277
|
+
vscodeOff: 'Disconnected',
|
|
278
|
+
},
|
|
279
|
+
ja: {
|
|
280
|
+
menu: {
|
|
281
|
+
newSession: '新規セッション',
|
|
282
|
+
history: 'セッション履歴',
|
|
283
|
+
provider: 'API設定',
|
|
284
|
+
settings: '設定',
|
|
285
|
+
about: 'について',
|
|
286
|
+
exit: '終了',
|
|
287
|
+
},
|
|
288
|
+
about: 'Bingo CLI ターミナル - バージョン情報',
|
|
289
|
+
aboutContent: [
|
|
290
|
+
'BingoはAIアシスタントのターミナルクライアントです。',
|
|
291
|
+
'1. API設定: "P"キーまたは「API設定」を選択してキーを設定。',
|
|
292
|
+
'2. モデルスロット: Providerパネルで各モデルを設定。',
|
|
293
|
+
'3. バックグラウンドサービス: セッション管理用ローカルサーバーを起動。',
|
|
294
|
+
'4. チャット開始: 任意のターミナルで `bingocode` または `claude` を実行。',
|
|
295
|
+
].join('\n'),
|
|
296
|
+
aboutFooter: '作者: leanchy (leanchy07@outlook.com) · github.com/leanchy/claude-code-bingo',
|
|
297
|
+
mark: '→ セッションをマーク',
|
|
298
|
+
unmark: '→ マークを解除',
|
|
299
|
+
tipsSimple: 'L 言語 | ESC 戻る | ←→ メニュー | ↩ 決定 | ? ヘルプ',
|
|
300
|
+
noData: 'データなし',
|
|
301
|
+
emptyHistory: 'まだセッションがありません。新規作成しますか?',
|
|
302
|
+
deleting: 'このセッションを削除しますか?(元に戻せません)',
|
|
303
|
+
historyHint: '↩ 開く · j 次へ · k 最初へ · q 戻る',
|
|
304
|
+
helpTitle: 'ショートカット',
|
|
305
|
+
// Settings page
|
|
306
|
+
settingsTitle: '設定',
|
|
307
|
+
langLabel: '言語',
|
|
308
|
+
langPickerTitle: '言語を選択',
|
|
309
|
+
settingsHint: '↑/k ↓/j スクロール · ↩ 切替 · ESC 戻る',
|
|
310
|
+
langOptions: LANG_OPTIONS,
|
|
311
|
+
autoModeLabel: 'Auto Mode',
|
|
312
|
+
autoModeOn: '有効',
|
|
313
|
+
autoModeOff: '無効',
|
|
314
|
+
bypassPermsLabel: 'Bypass',
|
|
315
|
+
bypassPermsOn: '有効',
|
|
316
|
+
bypassPermsOff: '無効',
|
|
317
|
+
vscodeLabel: 'Connect to VS Code',
|
|
318
|
+
vscodeOn: '接続済',
|
|
319
|
+
vscodeOff: '未接続',
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const menuKeys = [
|
|
324
|
+
'newSession', 'history', 'provider', 'settings', 'about', 'exit'
|
|
325
|
+
] as const;
|
|
326
|
+
type MenuKey = typeof menuKeys[number];
|
|
327
|
+
type Lang = keyof typeof i18nMap;
|
|
328
|
+
|
|
329
|
+
//@C:F ID=F.CM.loadMarkedSessionIds;K=F;V=1.0;P=load marked ids;D=CLI;M=cli;S=init;In=;Out=Set<string>
|
|
330
|
+
function loadMarkedSessionIds(): Set<string> {
|
|
331
|
+
try {
|
|
332
|
+
const arr = JSON.parse(fs.readFileSync(MARKED_FILE, 'utf-8'));
|
|
333
|
+
return new Set(typeof arr === 'object' && Array.isArray(arr) ? arr : []);
|
|
334
|
+
} catch {
|
|
335
|
+
return new Set();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//@C:F ID=F.CM.saveMarkedSessionIds;K=F;V=1.1;P=save marked ids;D=CLI;M=cli;S=persist;In=Set<string>;Out=void
|
|
340
|
+
function saveMarkedSessionIds(set: Set<string>) {
|
|
341
|
+
try {
|
|
342
|
+
const dir = path.dirname(MARKED_FILE);
|
|
343
|
+
if (!fs.existsSync(dir)) {
|
|
344
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
fs.writeFileSync(MARKED_FILE, JSON.stringify([...set]), 'utf-8');
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error('[saveMarkedSessionIds] Save failed:', err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Message Entry (Aligned with backend MessageEntry)
|
|
353
|
+
type MessageEntry = {
|
|
354
|
+
id: string;
|
|
355
|
+
type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result';
|
|
356
|
+
content: unknown; // string 或 ContentBlock[]
|
|
357
|
+
timestamp: string;
|
|
358
|
+
model?: string;
|
|
359
|
+
parentUuid?: string;
|
|
360
|
+
parentToolUseId?: string;
|
|
361
|
+
isSidechain?: boolean;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/** Extract plain text from MessageEntry.content */
|
|
365
|
+
function extractTextFromContent(content: unknown): string {
|
|
366
|
+
if (typeof content === 'string') return content;
|
|
367
|
+
if (Array.isArray(content)) {
|
|
368
|
+
return content
|
|
369
|
+
.map((block: any) => {
|
|
370
|
+
if (block.type === 'text' && typeof block.text === 'string') return block.text;
|
|
371
|
+
if (block.type === 'tool_use') return `[Tool: ${block.name || 'unknown'}]`;
|
|
372
|
+
if (block.type === 'tool_result') {
|
|
373
|
+
if (typeof block.content === 'string') return block.content;
|
|
374
|
+
if (Array.isArray(block.content)) {
|
|
375
|
+
return block.content
|
|
376
|
+
.filter((b: any) => b.type === 'text')
|
|
377
|
+
.map((b: any) => b.text)
|
|
378
|
+
.join('\n');
|
|
379
|
+
}
|
|
380
|
+
return '[Tool Result]';
|
|
381
|
+
}
|
|
382
|
+
return '';
|
|
383
|
+
})
|
|
384
|
+
.filter(Boolean)
|
|
385
|
+
.join('\n');
|
|
386
|
+
}
|
|
387
|
+
return String(content ?? '');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
//@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
|
|
391
|
+
export const CliMenuManager: React.FC = () => {
|
|
392
|
+
const { stdout } = useStdout();
|
|
393
|
+
const [terminalSize, setTerminalSize] = useState({
|
|
394
|
+
columns: stdout?.columns || 80,
|
|
395
|
+
rows: stdout?.rows || 24
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
const onResize = () => {
|
|
400
|
+
setTerminalSize({
|
|
401
|
+
columns: stdout?.columns || 80,
|
|
402
|
+
rows: stdout?.rows || 24
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
stdout?.on('resize', onResize);
|
|
406
|
+
return () => { stdout?.off('resize', onResize); };
|
|
407
|
+
}, [stdout]);
|
|
408
|
+
|
|
409
|
+
// Dynamic viewport
|
|
410
|
+
const VIEW_W = Number(process.env.CLI_VIEW_W || Math.min(terminalSize.columns, 96));
|
|
411
|
+
const VIEW_H = Number(process.env.CLI_VIEW_H || terminalSize.rows);
|
|
412
|
+
|
|
413
|
+
const [apiUrl, setApiUrl] = useState<string | null>(process.env.BASE_API_URL || null);
|
|
414
|
+
const [stopIfLast, setStopIfLast] = useState<null | (() => Promise<void>)>(null);
|
|
415
|
+
const [bootErr, setBootErr] = useState<string | null>(null);
|
|
416
|
+
const { exit } = useApp();
|
|
417
|
+
|
|
418
|
+
// Theme (Global Hook)
|
|
419
|
+
const [theme, setTheme] = useTheme();
|
|
420
|
+
|
|
421
|
+
// Language
|
|
422
|
+
const [lang, setLang] = useState<Lang>('en');
|
|
423
|
+
|
|
424
|
+
// Config ready probe (avoid Logo early read)
|
|
425
|
+
const [configReady, setConfigReady] = useState(false);
|
|
426
|
+
|
|
427
|
+
// Load settings from bingo/settings.json at startup
|
|
428
|
+
// (bypasses configReady to avoid stale lock issues)
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
try {
|
|
431
|
+
const bSettings = readBingoSettings();
|
|
432
|
+
const bingoLang = bSettings.language as string | undefined;
|
|
433
|
+
if (bingoLang && (bingoLang === 'en' || bingoLang === 'zh' || bingoLang === 'ja')) {
|
|
434
|
+
setLang(bingoLang as Lang);
|
|
435
|
+
}
|
|
436
|
+
if (typeof bSettings.autoModeEnabled === 'boolean') {
|
|
437
|
+
setAutoModeEnabled(bSettings.autoModeEnabled);
|
|
438
|
+
}
|
|
439
|
+
if (typeof bSettings.bypassPermsEnabled === 'boolean') {
|
|
440
|
+
setBypassPermsEnabled(bSettings.bypassPermsEnabled);
|
|
441
|
+
}
|
|
442
|
+
if (typeof bSettings.vscodeLinked === 'boolean') {
|
|
443
|
+
setVscodeLinked(bSettings.vscodeLinked);
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
}, []);
|
|
447
|
+
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (configReady) {
|
|
450
|
+
try {
|
|
451
|
+
const cfg = getGlobalConfig();
|
|
452
|
+
if (typeof cfg.uiAnimEnabled === 'boolean') setAnimEnabled(cfg.uiAnimEnabled);
|
|
453
|
+
if (typeof cfg.uiTipsEnabled === 'boolean') setTipsEnabled(cfg.uiTipsEnabled);
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
}, [configReady]);
|
|
457
|
+
|
|
458
|
+
const t = i18nMap[lang].menu;
|
|
459
|
+
|
|
460
|
+
// Top time
|
|
461
|
+
const [nowStr, setNowStr] = useState<string>(new Date().toLocaleString('en-US', { hour12: false }));
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
const id = setInterval(() => setNowStr(new Date().toLocaleString('en-US', { hour12: false })), 1000);
|
|
464
|
+
return () => clearInterval(id);
|
|
465
|
+
}, []);
|
|
466
|
+
|
|
467
|
+
// Main Menu
|
|
468
|
+
const [page, setPage] = useState<MenuKey | null>(null);
|
|
469
|
+
const menuItems = useMemo(() => menuKeys.map(key => ({ label: t[key], value: key })), [t]);
|
|
470
|
+
const [navIndex, setNavIndex] = useState(0);
|
|
471
|
+
|
|
472
|
+
// New Session
|
|
473
|
+
const [newSessionId, setNewSessionId] = useState<string | null>(null);
|
|
474
|
+
const [creating, setCreating] = useState(false);
|
|
475
|
+
const [createErr, setCreateErr] = useState<string | null>(null);
|
|
476
|
+
|
|
477
|
+
// History
|
|
478
|
+
const [loadingHist, setLoadingHist] = useState(false);
|
|
479
|
+
const [historyList, setHistoryList] = useState<any[]>([]);
|
|
480
|
+
const [historyCursor, setHistoryCursor] = useState<string | null>(null);
|
|
481
|
+
const [historyHasMore, setHistoryHasMore] = useState<boolean>(false);
|
|
482
|
+
const [histErr, setHistErr] = useState<string | null>(null);
|
|
483
|
+
const [historyMenuStage, setHistoryMenuStage] = useState<'list'|'window'|'deleteConfirm'>('list');
|
|
484
|
+
const [selectedHistory, setSelectedHistory] = useState<any|null>(null);
|
|
485
|
+
|
|
486
|
+
// History Messages
|
|
487
|
+
const [sessionMessages, setSessionMessages] = useState<MessageEntry[]>([]);
|
|
488
|
+
const [loadingMsgs, setLoadingMsgs] = useState(false);
|
|
489
|
+
const [msgsErr, setMsgsErr] = useState<string | null>(null);
|
|
490
|
+
const [msgsPage, setMsgsPage] = useState(0);
|
|
491
|
+
|
|
492
|
+
// Mark Persistence
|
|
493
|
+
const [markedSessionIds, setMarkedSessionIds] = useState<Set<string>>(new Set());
|
|
494
|
+
|
|
495
|
+
// Settings page scroll offset
|
|
496
|
+
const [settingsOffset, setSettingsOffset] = useState(0);
|
|
497
|
+
const [settingData, setSettingData] = useState<any>(null);
|
|
498
|
+
const [loadingSetting, setLoadingSetting] = useState(false);
|
|
499
|
+
const [setErr, setSetErr] = useState<string | null>(null);
|
|
500
|
+
const [settingsStage, setSettingsStage] = useState<'list' | 'langPicker'>('list');
|
|
501
|
+
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
502
|
+
const [autoModeEnabled, setAutoModeEnabled] = useState(false);
|
|
503
|
+
const [bypassPermsEnabled, setBypassPermsEnabled] = useState(false);
|
|
504
|
+
const [vscodeLinked, setVscodeLinked] = useState(false);
|
|
505
|
+
|
|
506
|
+
const [vscodeLinkErr, setVscodeLinkErr] = useState<string | null>(null);
|
|
507
|
+
|
|
508
|
+
// Auto-clear VSCode link error after 5 seconds
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (vscodeLinkErr) {
|
|
511
|
+
const timer = setTimeout(() => setVscodeLinkErr(null), 5000);
|
|
512
|
+
return () => clearTimeout(timer);
|
|
513
|
+
}
|
|
514
|
+
}, [vscodeLinkErr]);
|
|
515
|
+
// Top toolbar state
|
|
516
|
+
const [animEnabled, setAnimEnabled] = useState(true);
|
|
517
|
+
const [tipsEnabled, setTipsEnabled] = useState(true);
|
|
518
|
+
|
|
519
|
+
// Help overlay
|
|
520
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
521
|
+
|
|
522
|
+
// Keyboard navigation for lists
|
|
523
|
+
const [listOffset, setListOffset] = useState(0);
|
|
524
|
+
|
|
525
|
+
// Quick Resume (R)
|
|
526
|
+
const [quickResumeRequested, setQuickResumeRequested] = useState(false);
|
|
527
|
+
|
|
528
|
+
// Compute viewport
|
|
529
|
+
const TOP_H = page === null ? TOP_H_HOME : TOP_H_COMPACT;
|
|
530
|
+
const MID_H = Math.max(5, VIEW_H - TOP_H - BOTTOM_H - (page === null ? 0 : 2));
|
|
531
|
+
const MSGS_PAGE_SIZE = Math.max(1, MID_H - 2);
|
|
532
|
+
const [expandMsgs, setExpandMsgs] = useState(false);
|
|
533
|
+
|
|
534
|
+
// Boot/Reuse singleton local server (with retry)
|
|
535
|
+
useEffect(() => {
|
|
536
|
+
let mounted = true;
|
|
537
|
+
(async () => {
|
|
538
|
+
if (apiUrl) return;
|
|
539
|
+
const entry = path.resolve(import.meta.dir, '../server/index.ts');
|
|
540
|
+
const MAX_RETRIES = 3;
|
|
541
|
+
const RETRY_DELAYS = [0, 2000, 5000]; // 0s, 2s, 5s
|
|
542
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
543
|
+
if (!mounted) return;
|
|
544
|
+
if (attempt > 0) {
|
|
545
|
+
setBootErr(`Attempt ${attempt} failed, retrying in ${RETRY_DELAYS[attempt] / 1000}s...`);
|
|
546
|
+
await new Promise(r => setTimeout(r, RETRY_DELAYS[attempt]));
|
|
547
|
+
}
|
|
548
|
+
if (!mounted) return;
|
|
549
|
+
try {
|
|
550
|
+
const handle = await ensureSingletonLocalServer({ serverEntry: entry });
|
|
551
|
+
if (!mounted) { await handle.stopIfLast(); return; }
|
|
552
|
+
setApiUrl(handle.baseUrl);
|
|
553
|
+
setStopIfLast(() => handle.stopIfLast);
|
|
554
|
+
setBootErr(null);
|
|
555
|
+
return; // Success, exit retry
|
|
556
|
+
} catch (e: any) {
|
|
557
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
558
|
+
setBootErr(e.message || 'Local server failed to start');
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
})();
|
|
563
|
+
return () => { mounted = false; if (stopIfLast) stopIfLast(); };
|
|
564
|
+
}, []);
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
let cancelled = false;
|
|
567
|
+
const probe = () => {
|
|
568
|
+
try {
|
|
569
|
+
getGlobalConfig();
|
|
570
|
+
if (!cancelled) setConfigReady(true);
|
|
571
|
+
} catch {
|
|
572
|
+
if (!cancelled) setTimeout(probe, 60);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
probe();
|
|
576
|
+
return () => { cancelled = true; };
|
|
577
|
+
}, []);
|
|
578
|
+
|
|
579
|
+
// Init marks
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
setMarkedSessionIds(loadMarkedSessionIds());
|
|
582
|
+
}, []);
|
|
583
|
+
|
|
584
|
+
// Page switch reset
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
if (page === 'newSession') {
|
|
587
|
+
setNewSessionId(null);
|
|
588
|
+
setCreating(false);
|
|
589
|
+
setCreateErr(null);
|
|
590
|
+
}
|
|
591
|
+
if (page !== 'settings') {
|
|
592
|
+
setSettingsOffset(0);
|
|
593
|
+
setSettingsStage('list');
|
|
594
|
+
setSettingsCursor(0);
|
|
595
|
+
}
|
|
596
|
+
// Close help overlay
|
|
597
|
+
setShowHelp(false);
|
|
598
|
+
}, [page]);
|
|
599
|
+
|
|
600
|
+
// History page entry reset
|
|
601
|
+
useEffect(() => {
|
|
602
|
+
if (page === 'history') {
|
|
603
|
+
setHistoryMenuStage('list');
|
|
604
|
+
setSelectedHistory(null);
|
|
605
|
+
setHistoryCursor(null);
|
|
606
|
+
setSessionMessages([]);
|
|
607
|
+
setMsgsErr(null);
|
|
608
|
+
setMsgsPage(0);
|
|
609
|
+
setExpandMsgs(false);
|
|
610
|
+
}
|
|
611
|
+
}, [page]);
|
|
612
|
+
|
|
613
|
+
// Create Session
|
|
614
|
+
const onCreateSession = async () => {
|
|
615
|
+
setCreating(true); setCreateErr(null);
|
|
616
|
+
try {
|
|
617
|
+
const fsReq = require('fs');
|
|
618
|
+
const pathReq = require('path');
|
|
619
|
+
const { spawn } = require('child_process');
|
|
620
|
+
// Use import.meta.dir for pkg root
|
|
621
|
+
const pkgPath = pathReq.resolve(import.meta.dir, '../../package.json');
|
|
622
|
+
const pkgJson = JSON.parse(fsReq.readFileSync(pkgPath, 'utf-8'));
|
|
623
|
+
const bins = pkgJson.bin || {};
|
|
624
|
+
const isWin = process.platform === 'win32';
|
|
625
|
+
const binName = isWin
|
|
626
|
+
? (bins['claude-haha'] ? 'claude-haha' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]))
|
|
627
|
+
: (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
|
|
628
|
+
const spawnCmd = isWin ? 'cmd' : 'sh';
|
|
629
|
+
// Windows calls global bingocode directly
|
|
630
|
+
const spawnArgs = isWin ? ['/c', 'start', 'cmd', '/k', 'bingocode'] : ['-c', `${binName}`];
|
|
631
|
+
const spawnEnv = await buildSpawnEnv();
|
|
632
|
+
spawn(spawnCmd, spawnArgs, {
|
|
633
|
+
cwd: process.env.CALLER_DIR || process.cwd(),
|
|
634
|
+
env: spawnEnv,
|
|
635
|
+
detached: true,
|
|
636
|
+
stdio: 'ignore'
|
|
637
|
+
}).unref();
|
|
638
|
+
setNewSessionId('Started: ' + binName);
|
|
639
|
+
} catch(e: any) {
|
|
640
|
+
setCreateErr(e.message || 'Failed to create');
|
|
641
|
+
} finally {
|
|
642
|
+
setCreating(false);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// Paged loading for history
|
|
647
|
+
useEffect(() => {
|
|
648
|
+
if (page === 'history' && historyMenuStage === 'list') {
|
|
649
|
+
setLoadingHist(true); setHistErr(null);
|
|
650
|
+
(async () => {
|
|
651
|
+
try {
|
|
652
|
+
let url = apiUrl + '/api/sessions';
|
|
653
|
+
if (historyCursor) url += `?cursor=${historyCursor}`;
|
|
654
|
+
const res = await axios.get(url);
|
|
655
|
+
const pageData = res.data;
|
|
656
|
+
setHistoryList(pageData?.sessions || []);
|
|
657
|
+
if (historyCursor === null) {
|
|
658
|
+
setHistoryCursor(pageData?.first_id || null);
|
|
659
|
+
}
|
|
660
|
+
setHistoryHasMore(!!pageData?.has_more);
|
|
661
|
+
} catch (e: any) {
|
|
662
|
+
setHistErr(e.message || 'Failed to fetch history');
|
|
663
|
+
} finally {
|
|
664
|
+
setLoadingHist(false);
|
|
665
|
+
}
|
|
666
|
+
})();
|
|
667
|
+
}
|
|
668
|
+
if (page !== 'history') {
|
|
669
|
+
setLoadingHist(false);
|
|
670
|
+
setHistErr(null);
|
|
671
|
+
setHistoryList([]);
|
|
672
|
+
}
|
|
673
|
+
}, [page, historyCursor, historyMenuStage, apiUrl]);
|
|
674
|
+
|
|
675
|
+
// 快速恢复:当按下 R 并已加载历史列表后,自动进入第一个会话窗口
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
if (page === 'history' && historyMenuStage === 'list' && quickResumeRequested && historyList.length) {
|
|
678
|
+
const session = historyList[0];
|
|
679
|
+
if (session) {
|
|
680
|
+
setSelectedHistory(session);
|
|
681
|
+
setHistoryMenuStage('window');
|
|
682
|
+
setQuickResumeRequested(false);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}, [page, historyMenuStage, quickResumeRequested, historyList]);
|
|
686
|
+
|
|
687
|
+
// 会话消息获取
|
|
688
|
+
useEffect(() => {
|
|
689
|
+
if (page === 'history' && historyMenuStage === 'window' && selectedHistory && apiUrl) {
|
|
690
|
+
let cancelled = false;
|
|
691
|
+
setLoadingMsgs(true);
|
|
692
|
+
setMsgsErr(null);
|
|
693
|
+
setSessionMessages([]);
|
|
694
|
+
setMsgsPage(0);
|
|
695
|
+
(async () => {
|
|
696
|
+
try {
|
|
697
|
+
const resp = await axios.get(`${apiUrl}/api/sessions/${selectedHistory.id}/messages`);
|
|
698
|
+
if (!cancelled) {
|
|
699
|
+
const msgs: MessageEntry[] = resp.data?.messages ?? [];
|
|
700
|
+
setSessionMessages(msgs);
|
|
701
|
+
}
|
|
702
|
+
} catch (e: any) {
|
|
703
|
+
if (!cancelled) setMsgsErr(e.message || 'Failed to load messages');
|
|
704
|
+
} finally {
|
|
705
|
+
if (!cancelled) setLoadingMsgs(false);
|
|
706
|
+
}
|
|
707
|
+
})();
|
|
708
|
+
return () => { cancelled = true; };
|
|
709
|
+
} else {
|
|
710
|
+
setSessionMessages([]);
|
|
711
|
+
setLoadingMsgs(false);
|
|
712
|
+
setMsgsErr(null);
|
|
713
|
+
}
|
|
714
|
+
}, [page, historyMenuStage, selectedHistory, apiUrl]);
|
|
715
|
+
|
|
716
|
+
// Settings data
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
if (page === 'settings') {
|
|
719
|
+
setLoadingSetting(true); setSetErr(null);
|
|
720
|
+
(async () => {
|
|
721
|
+
try {
|
|
722
|
+
const data = (await import('../utils/settings/settings')).default;
|
|
723
|
+
setSettingData(data);
|
|
724
|
+
} catch(e: any) {
|
|
725
|
+
setSetErr(e.message||'Failed to load settings');
|
|
726
|
+
} finally { setLoadingSetting(false); }
|
|
727
|
+
})();
|
|
728
|
+
} else {
|
|
729
|
+
setSettingData(null);
|
|
730
|
+
setLoadingSetting(false);
|
|
731
|
+
setSetErr(null);
|
|
732
|
+
}
|
|
733
|
+
}, [page]);
|
|
734
|
+
|
|
735
|
+
// Keyboard interactions
|
|
736
|
+
useInput((input, key) => {
|
|
737
|
+
// Language toggle (en → zh → ja → en)
|
|
738
|
+
if (input === 'l' || input === 'L') {
|
|
739
|
+
const langOrder: Lang[] = ['en', 'zh', 'ja'];
|
|
740
|
+
const nextLang = langOrder[(langOrder.indexOf(lang) + 1) % langOrder.length];
|
|
741
|
+
setLang(nextLang);
|
|
742
|
+
try { writeBingoSettings({ language: nextLang }); } catch {}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Theme toggle (G)
|
|
747
|
+
if ((input === 'g' || input === 'G')) {
|
|
748
|
+
const order = ['light', 'dark', 'highContrast'] as const;
|
|
749
|
+
const curr = String(theme || 'light');
|
|
750
|
+
const idx = Math.max(0, order.indexOf(curr as any));
|
|
751
|
+
const next = order[(idx + 1) % order.length];
|
|
752
|
+
setTheme(next as any);
|
|
753
|
+
try { saveGlobalConfig(current => ({ ...current, theme: next as any })); } catch {}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Top animation toggle (O)
|
|
758
|
+
if (input === 'o' || input === 'O') {
|
|
759
|
+
setAnimEnabled(v => {
|
|
760
|
+
const next = !v;
|
|
761
|
+
try { saveGlobalConfig(current => ({ ...current, uiAnimEnabled: next })); } catch {}
|
|
762
|
+
return next;
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Top Tips toggle (T)
|
|
767
|
+
if (input === 't' || input === 'T') {
|
|
768
|
+
setTipsEnabled(v => {
|
|
769
|
+
const next = !v;
|
|
770
|
+
try { saveGlobalConfig(current => ({ ...current, uiTipsEnabled: next })); } catch {}
|
|
771
|
+
return next;
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Help overlay (?)
|
|
777
|
+
if (input === '?') {
|
|
778
|
+
setShowHelp(v => !v);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ESC to back or close help
|
|
783
|
+
if (key.escape) {
|
|
784
|
+
if (showHelp) { setShowHelp(false); return; }
|
|
785
|
+
if (page === 'provider') return; // Handled internally
|
|
786
|
+
// Settings: langPicker → back to list; list → back to main menu
|
|
787
|
+
if (page === 'settings') {
|
|
788
|
+
if (settingsStage === 'langPicker') { setSettingsStage('list'); return; }
|
|
789
|
+
}
|
|
790
|
+
setPage(null);
|
|
791
|
+
setHistoryMenuStage('list');
|
|
792
|
+
setSelectedHistory(null);
|
|
793
|
+
setHistoryCursor(null);
|
|
794
|
+
setSessionMessages([]);
|
|
795
|
+
setMsgsPage(0);
|
|
796
|
+
setSettingsOffset(0);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Quick entries: N New, R Resume, P Provider
|
|
801
|
+
if (input === 'n' || input === 'N') {
|
|
802
|
+
setPage('newSession');
|
|
803
|
+
onCreateSession();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (input === 'r' || input === 'R') {
|
|
807
|
+
setPage('history');
|
|
808
|
+
setQuickResumeRequested(true);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (input === 'p' || input === 'P') {
|
|
812
|
+
setPage('provider');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Main menu navigation
|
|
817
|
+
if (!showHelp && key.leftArrow && page === null) {
|
|
818
|
+
setNavIndex(i => (i - 1 + menuItems.length) % menuItems.length);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!showHelp && key.rightArrow && page === null) {
|
|
822
|
+
setNavIndex(i => (i + 1) % menuItems.length);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!showHelp && key.return && page === null) {
|
|
826
|
+
const keyVal = menuItems[navIndex].value as MenuKey;
|
|
827
|
+
setPage(keyVal);
|
|
828
|
+
if (keyVal === 'newSession') onCreateSession();
|
|
829
|
+
if (keyVal === 'exit') exit();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// History shortcuts
|
|
834
|
+
if (!showHelp && page === 'history') {
|
|
835
|
+
if (historyMenuStage === 'list') {
|
|
836
|
+
const HIST_VISIBLE = MID_H - 2;
|
|
837
|
+
if (key.downArrow || input === 'j' || input === '\u001b[B') {
|
|
838
|
+
// Internal SelectInput handles cursor, we just need to track offset for ScrollBar
|
|
839
|
+
setListOffset(o => Math.min(o + 1, Math.max(0, groupedHistoryItems.length - HIST_VISIBLE)));
|
|
840
|
+
}
|
|
841
|
+
if (key.upArrow || input === 'k' || input === '\u001b[A') {
|
|
842
|
+
setListOffset(o => Math.max(0, o - 1));
|
|
843
|
+
}
|
|
844
|
+
if (input === 'q') {
|
|
845
|
+
setPage(null);
|
|
846
|
+
setHistoryMenuStage('list');
|
|
847
|
+
setSelectedHistory(null);
|
|
848
|
+
setHistoryCursor(null);
|
|
849
|
+
setListOffset(0);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (input === 'j' && historyHasMore) {
|
|
853
|
+
setHistoryCursor(historyList[historyList.length - 1]?.id || null);
|
|
854
|
+
setListOffset(0);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (input === 'k') {
|
|
858
|
+
setHistoryCursor(null);
|
|
859
|
+
setListOffset(0);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
} else if (historyMenuStage === 'window') {
|
|
863
|
+
if ((input === 'm' || input === 'M') && selectedHistory) {
|
|
864
|
+
handleHistoryMenuAction('__toggle_mark');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if ((input === 'c' || input === 'C') && selectedHistory) {
|
|
868
|
+
handleHistoryMenuAction('__continue');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if ((input === 'd' || input === 'D') && selectedHistory) {
|
|
872
|
+
handleHistoryMenuAction('__delete');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (input === 'q') {
|
|
876
|
+
handleHistoryMenuAction('__back');
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
// Message scrolling
|
|
880
|
+
if (key.upArrow || input === 'k') {
|
|
881
|
+
setMsgsPage(p => Math.max(0, p - 1));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (key.downArrow || input === 'j') {
|
|
885
|
+
setMsgsPage(p => p + 1);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
} else if (historyMenuStage === 'deleteConfirm') {
|
|
890
|
+
if (input === 'q') {
|
|
891
|
+
handleHistoryMenuAction('__cancel_delete');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Settings interactions
|
|
898
|
+
if (!showHelp && page === 'settings') {
|
|
899
|
+
if (settingsStage === 'list') {
|
|
900
|
+
// +1 for the fixed Language row prepended before settingData entries
|
|
901
|
+
const totalRows = 4 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
|
|
902
|
+
const visible = Math.max(1, MID_H - 2);
|
|
903
|
+
if (key.downArrow || input === 'j') {
|
|
904
|
+
setSettingsCursor(c => Math.min(totalRows - 1, c + 1));
|
|
905
|
+
setSettingsOffset(o => Math.min(Math.max(0, totalRows - visible), o + 1));
|
|
906
|
+
}
|
|
907
|
+
if (key.upArrow || input === 'k') {
|
|
908
|
+
setSettingsCursor(c => Math.max(0, c - 1));
|
|
909
|
+
setSettingsOffset(o => Math.max(0, o - 1));
|
|
910
|
+
}
|
|
911
|
+
if (key.return) {
|
|
912
|
+
// Row 0 is the interactive Language row
|
|
913
|
+
if (settingsCursor === 0) {
|
|
914
|
+
setSettingsStage('langPicker');
|
|
915
|
+
} else if (settingsCursor === 1) {
|
|
916
|
+
// Row 1: toggle Auto Mode
|
|
917
|
+
setAutoModeEnabled(prev => {
|
|
918
|
+
const next = !prev;
|
|
919
|
+
try {
|
|
920
|
+
writeBingoSettings({ autoModeEnabled: next });
|
|
921
|
+
const gcfg = readGlobalClaudeConfig();
|
|
922
|
+
gcfg.cachedGrowthBookFeatures = {
|
|
923
|
+
...(gcfg.cachedGrowthBookFeatures as Record<string, unknown>),
|
|
924
|
+
tengu_auto_mode_config: next
|
|
925
|
+
? { enabled: 'enabled', allowModels: ['*'] }
|
|
926
|
+
: { enabled: 'disabled' },
|
|
927
|
+
};
|
|
928
|
+
writeGlobalClaudeConfig(gcfg);
|
|
929
|
+
} catch {
|
|
930
|
+
return prev; // write failed — keep old state
|
|
931
|
+
}
|
|
932
|
+
return next;
|
|
933
|
+
});
|
|
934
|
+
} else if (settingsCursor === 2) {
|
|
935
|
+
// Row 2: toggle Bypass Permissions
|
|
936
|
+
setBypassPermsEnabled(prev => {
|
|
937
|
+
const next = !prev;
|
|
938
|
+
try {
|
|
939
|
+
writeBingoSettings({ bypassPermsEnabled: next });
|
|
940
|
+
const safeSettings = next
|
|
941
|
+
? { permissions: { defaultMode: 'bypassPermissions', skipDangerousModePermissionPrompt: true } }
|
|
942
|
+
: { permissions: { defaultMode: 'default' } };
|
|
943
|
+
writeClaudeSettings(safeSettings);
|
|
944
|
+
} catch {
|
|
945
|
+
return prev; // write failed — keep old state
|
|
946
|
+
}
|
|
947
|
+
return next;
|
|
948
|
+
});
|
|
949
|
+
} else if (settingsCursor === 3) {
|
|
950
|
+
// Row 3: toggle VS Code link -- HTTP first, then state+settings on success
|
|
951
|
+
const port = (process.env.BINGO_PORT ? parseInt(process.env.BINGO_PORT) : 3456) || 3456;
|
|
952
|
+
const apiBase = process.env.BASE_API_URL || `http://127.0.0.1:${port}`;
|
|
953
|
+
const next = !vscodeLinked;
|
|
954
|
+
const method = next ? 'POST' : 'DELETE';
|
|
955
|
+
setVscodeLinkErr(null);
|
|
956
|
+
axios({ method, url: `${apiBase}/api/providers/link-vscode`, timeout: 5000 })
|
|
957
|
+
.then(() => {
|
|
958
|
+
try { writeBingoSettings({ vscodeLinked: next }); } catch { /* ignore write errors */ }
|
|
959
|
+
setVscodeLinked(next);
|
|
960
|
+
})
|
|
961
|
+
.catch((error) => {
|
|
962
|
+
logError(error);
|
|
963
|
+
setVscodeLinkErr(error.message || 'Failed to update VS Code link');
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// langPicker stage: ESC handled above; selection via SelectInput onSelect
|
|
969
|
+
}
|
|
970
|
+
}, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor, autoModeEnabled, bypassPermsEnabled, vscodeLinked, vscodeLinkErr]);
|
|
971
|
+
|
|
972
|
+
function cleanText(text: string): string {
|
|
973
|
+
return String(text ?? '').replace(/[\n\r]+/g, ' ').replace(/\u001b\[[0-9;]*m/g, '').trim();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function clampTextLines(text: string, maxWidth: number, maxLines: number) {
|
|
977
|
+
const cleaned = cleanText(text);
|
|
978
|
+
const out: string[] = [];
|
|
979
|
+
if (cleaned.length <= maxWidth) {
|
|
980
|
+
out.push(cleaned);
|
|
981
|
+
} else {
|
|
982
|
+
out.push(cleaned.slice(0, maxWidth - 1) + '…');
|
|
983
|
+
}
|
|
984
|
+
return out.join('\n');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function makeHistoryLabel(item: any, width: number, isMarked: boolean) {
|
|
988
|
+
const star = isMarked ? '★ ' : '';
|
|
989
|
+
const ts = String(item.createdAt || '').slice(0, 16).replace('T', ' ');
|
|
990
|
+
const cnt = String(item.messageCount ?? 0).padStart(3, ' ');
|
|
991
|
+
// Reserved width for: prefix(star+time) + spacer(2) + suffix(1+cnt)
|
|
992
|
+
// Star is width 2, ts is width 16, spacer is 2, cnt is 3, padding is 1. Total = 24
|
|
993
|
+
const reserved = 24;
|
|
994
|
+
const titleMax = Math.max(8, width - reserved);
|
|
995
|
+
const title = safePadEnd(truncate(String(item.title || ''), titleMax), titleMax);
|
|
996
|
+
return `${star}${ts} ${title} ${cnt}`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// 新增:会话恢复(供快捷键和右侧菜单复用)
|
|
1000
|
+
// workDir: 会话原始工作目录,用于跨文件夹恢复(确保新进程能找到 session 文件)
|
|
1001
|
+
async function resumeSession(sessionId: string, workDir?: string | null) {
|
|
1002
|
+
try {
|
|
1003
|
+
const fsReq = require('fs');
|
|
1004
|
+
const pathReq = require('path');
|
|
1005
|
+
const { spawn } = require('child_process');
|
|
1006
|
+
// 用 import.meta.dir 定位包根,避免 process.cwd() 指向用户目录
|
|
1007
|
+
const pkgPath = pathReq.resolve(import.meta.dir, '../../package.json');
|
|
1008
|
+
const pkgJson = JSON.parse(fsReq.readFileSync(pkgPath, 'utf-8'));
|
|
1009
|
+
const bins = pkgJson.bin || {};
|
|
1010
|
+
const isWin = process.platform === 'win32';
|
|
1011
|
+
const binName = isWin
|
|
1012
|
+
? (bins['claude-haha'] ? 'claude-haha' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]))
|
|
1013
|
+
: (bins['claude-linux'] ? 'claude-linux' : (bins['claude'] ? 'claude' : Object.keys(bins)[0]));
|
|
1014
|
+
const spawnCmd = isWin ? 'cmd' : 'sh';
|
|
1015
|
+
// Windows 直接调全局 bingocode 命令,不用 bun 前缀
|
|
1016
|
+
const spawnArgs = isWin
|
|
1017
|
+
? ['/c', 'start', 'cmd', '/k', `bingocode --resume ${sessionId}`]
|
|
1018
|
+
: ['-c', `${binName} --resume ${sessionId}`];
|
|
1019
|
+
const spawnEnv = await buildSpawnEnv();
|
|
1020
|
+
spawn(spawnCmd, spawnArgs, {
|
|
1021
|
+
cwd: workDir || process.env.CALLER_DIR || process.cwd(),
|
|
1022
|
+
env: spawnEnv,
|
|
1023
|
+
detached: true,
|
|
1024
|
+
stdio: 'ignore'
|
|
1025
|
+
}).unref();
|
|
1026
|
+
} catch {}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
// 历史分组展示
|
|
1031
|
+
const groupedHistoryItems = useMemo(() => {
|
|
1032
|
+
if (!historyList || !Array.isArray(historyList)) return [];
|
|
1033
|
+
const now = new Date();
|
|
1034
|
+
const today: any[] = [];
|
|
1035
|
+
const week: any[] = [];
|
|
1036
|
+
const earlier: any[] = [];
|
|
1037
|
+
const marked: any[] = [];
|
|
1038
|
+
|
|
1039
|
+
for (const item of historyList) {
|
|
1040
|
+
if (markedSessionIds.has(item.id)) {
|
|
1041
|
+
marked.push(item);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const dt = new Date(item.createdAt);
|
|
1045
|
+
const isToday =
|
|
1046
|
+
dt.getFullYear() === now.getFullYear() &&
|
|
1047
|
+
dt.getMonth() === now.getMonth() &&
|
|
1048
|
+
dt.getDate() === now.getDate();
|
|
1049
|
+
const weekStart = new Date(now);
|
|
1050
|
+
weekStart.setDate(now.getDate() - ((now.getDay() + 6) % 7));
|
|
1051
|
+
weekStart.setHours(0, 0, 0, 0);
|
|
1052
|
+
if (isToday) today.push(item);
|
|
1053
|
+
else if (dt >= weekStart) week.push(item);
|
|
1054
|
+
else earlier.push(item);
|
|
1055
|
+
}
|
|
1056
|
+
function groupToItems(group: any[], groupTitle: string) {
|
|
1057
|
+
if (group.length === 0) return [];
|
|
1058
|
+
return [
|
|
1059
|
+
{ label: groupTitle, value: `__group_${groupTitle}`, isGroup: true },
|
|
1060
|
+
...group.map(item => {
|
|
1061
|
+
const isMarked = markedSessionIds.has(item.id);
|
|
1062
|
+
return {
|
|
1063
|
+
label: makeHistoryLabel(item, Math.max(20, VIEW_W - 8), isMarked),
|
|
1064
|
+
value: item.id,
|
|
1065
|
+
color: isMarked ? 'yellow' : undefined,
|
|
1066
|
+
};
|
|
1067
|
+
})
|
|
1068
|
+
];
|
|
1069
|
+
}
|
|
1070
|
+
const items = [
|
|
1071
|
+
...groupToItems(marked, '—— Marked ——'),
|
|
1072
|
+
...groupToItems(today, '—— Today ——'),
|
|
1073
|
+
...groupToItems(week, '—— This Week ——'),
|
|
1074
|
+
...groupToItems(earlier, '—— Earlier ——'),
|
|
1075
|
+
];
|
|
1076
|
+
return items;
|
|
1077
|
+
}, [historyList, markedSessionIds]);
|
|
1078
|
+
|
|
1079
|
+
// Toggle Mark
|
|
1080
|
+
const toggleMarkSession = (sessionId: string) => {
|
|
1081
|
+
setMarkedSessionIds(prev => {
|
|
1082
|
+
const next = new Set(prev);
|
|
1083
|
+
if (next.has(sessionId)) next.delete(sessionId);
|
|
1084
|
+
else next.add(sessionId);
|
|
1085
|
+
saveMarkedSessionIds(next);
|
|
1086
|
+
return next;
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const handleHistoryMenuAction = (action: string) => {
|
|
1091
|
+
if (action === '__back') {
|
|
1092
|
+
setHistoryMenuStage('list');
|
|
1093
|
+
setSelectedHistory(null);
|
|
1094
|
+
setMsgsPage(0);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!selectedHistory) return;
|
|
1098
|
+
|
|
1099
|
+
switch (action) {
|
|
1100
|
+
case '__toggle_mark':
|
|
1101
|
+
toggleMarkSession(selectedHistory.id);
|
|
1102
|
+
break;
|
|
1103
|
+
case '__continue':
|
|
1104
|
+
resumeSession(selectedHistory.id, selectedHistory.workDir);
|
|
1105
|
+
break;
|
|
1106
|
+
case '__delete':
|
|
1107
|
+
setHistoryMenuStage('deleteConfirm');
|
|
1108
|
+
break;
|
|
1109
|
+
case '__confirm_delete':
|
|
1110
|
+
handleDeleteSession(selectedHistory.id);
|
|
1111
|
+
break;
|
|
1112
|
+
case '__cancel_delete':
|
|
1113
|
+
setHistoryMenuStage('window');
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
// Refresh history
|
|
1120
|
+
const refreshHistoryList = () => {
|
|
1121
|
+
setLoadingHist(true); setHistErr(null);
|
|
1122
|
+
let url = apiUrl + '/api/sessions';
|
|
1123
|
+
axios.get(url).then(res => {
|
|
1124
|
+
const pageData = res.data;
|
|
1125
|
+
setHistoryList(pageData?.sessions || []);
|
|
1126
|
+
setHistoryCursor(pageData?.first_id || null);
|
|
1127
|
+
setHistoryHasMore(!!pageData?.has_more);
|
|
1128
|
+
}).catch(e => {
|
|
1129
|
+
setHistErr(e.message || 'Failed to fetch history');
|
|
1130
|
+
}).finally(() => setLoadingHist(false));
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// Delete Session
|
|
1134
|
+
const handleDeleteSession = (sessionId: string) => {
|
|
1135
|
+
const url = apiUrl.replace(/\/+$/, '') + '/api/sessions/' + sessionId;
|
|
1136
|
+
axios.delete(url)
|
|
1137
|
+
.catch(e => {})
|
|
1138
|
+
.finally(() => {
|
|
1139
|
+
setHistoryMenuStage('list');
|
|
1140
|
+
setSelectedHistory(null);
|
|
1141
|
+
setHistoryCursor(null);
|
|
1142
|
+
refreshHistoryList();
|
|
1143
|
+
});
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// Secondary menu (bottom bar right)
|
|
1147
|
+
const secondaryMenu: SecondaryMenu = useMemo(() => {
|
|
1148
|
+
if (page === 'history' && historyMenuStage === 'window' && selectedHistory) {
|
|
1149
|
+
const isMarked = markedSessionIds.has(selectedHistory.id);
|
|
1150
|
+
const markLabel = isMarked ? i18nMap[lang].unmark : i18nMap[lang].mark;
|
|
1151
|
+
return {
|
|
1152
|
+
title: 'Session Actions',
|
|
1153
|
+
items: [
|
|
1154
|
+
{ label: markLabel, value: '__toggle_mark' },
|
|
1155
|
+
{ label: '→ Continue session', value: '__continue' },
|
|
1156
|
+
{ label: '→ Delete session', value: '__delete' },
|
|
1157
|
+
{ label: '← Back to list', value: '__back' },
|
|
1158
|
+
],
|
|
1159
|
+
onSelect: (item: any) => {
|
|
1160
|
+
if (item.value === '__back') {
|
|
1161
|
+
setHistoryMenuStage('list');
|
|
1162
|
+
setSelectedHistory(null);
|
|
1163
|
+
setMsgsPage(0);
|
|
1164
|
+
} else if (item.value === '__continue') {
|
|
1165
|
+
resumeSession(selectedHistory.id, selectedHistory.workDir);
|
|
1166
|
+
} else if (item.value === '__delete') {
|
|
1167
|
+
setHistoryMenuStage('deleteConfirm');
|
|
1168
|
+
} else if (item.value === '__toggle_mark') {
|
|
1169
|
+
toggleMarkSession(selectedHistory.id);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (page === 'history' && historyMenuStage === 'deleteConfirm' && selectedHistory) {
|
|
1176
|
+
return {
|
|
1177
|
+
title: 'Confirm Delete',
|
|
1178
|
+
items: [
|
|
1179
|
+
{ label: 'Yes, delete', value: '__confirm_delete' },
|
|
1180
|
+
{ label: 'No, back', value: '__cancel_delete' },
|
|
1181
|
+
],
|
|
1182
|
+
onSelect: (item: any) => {
|
|
1183
|
+
if (item.value === '__cancel_delete') {
|
|
1184
|
+
setHistoryMenuStage('window');
|
|
1185
|
+
} else if (item.value === '__confirm_delete') {
|
|
1186
|
+
handleDeleteSession(selectedHistory.id);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}, [page, historyMenuStage, selectedHistory, markedSessionIds, lang]);
|
|
1193
|
+
|
|
1194
|
+
// Help Overlay
|
|
1195
|
+
function renderHelpOverlay() {
|
|
1196
|
+
return (
|
|
1197
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1198
|
+
<Text color="magenta">{i18nMap[lang].helpTitle}</Text>
|
|
1199
|
+
<Text> </Text>
|
|
1200
|
+
<Text color="cyan">N</Text><Text> New Session</Text>
|
|
1201
|
+
<Text color="cyan">R</Text><Text> Quick Resume</Text>
|
|
1202
|
+
<Text color="cyan">P</Text><Text> Open Provider Config</Text>
|
|
1203
|
+
<Text color="cyan">G</Text><Text> Toggle Theme (light/dark/highContrast)</Text>
|
|
1204
|
+
<Text color="cyan">L</Text><Text> Toggle Language (en → zh → ja)</Text>
|
|
1205
|
+
<Text color="cyan">O</Text><Text> Toggle Top Animation</Text>
|
|
1206
|
+
<Text color="cyan">T</Text><Text> Toggle Top Tips</Text>
|
|
1207
|
+
<Text color="cyan">?</Text><Text> Toggle Help</Text>
|
|
1208
|
+
<Text> </Text>
|
|
1209
|
+
<Hint>ESC to close · Works anywhere</Hint>
|
|
1210
|
+
</Box>
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Center Content
|
|
1215
|
+
function renderCenter() {
|
|
1216
|
+
if (showHelp) return renderHelpOverlay();
|
|
1217
|
+
|
|
1218
|
+
// Home: WelcomeV2 (58 cols wide)
|
|
1219
|
+
if (page === null) {
|
|
1220
|
+
const WELCOME_W = 58;
|
|
1221
|
+
const leftPad = Math.max(0, Math.floor((VIEW_W - WELCOME_W) / 2));
|
|
1222
|
+
return (
|
|
1223
|
+
<Box flexDirection="column" width={VIEW_W} height={MID_H}>
|
|
1224
|
+
<Box flexDirection="row" width={VIEW_W} flexGrow={1}>
|
|
1225
|
+
<Box width={leftPad} flexShrink={0} />
|
|
1226
|
+
<WelcomeV2 />
|
|
1227
|
+
</Box>
|
|
1228
|
+
{!apiUrl && !bootErr && (
|
|
1229
|
+
<StateDisplay type="loading" message="Starting server..." />
|
|
1230
|
+
)}
|
|
1231
|
+
{bootErr && (
|
|
1232
|
+
<StateDisplay type="error" message={`Server boot failed: ${bootErr}`} />
|
|
1233
|
+
)}
|
|
1234
|
+
</Box>
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// New Session
|
|
1239
|
+
if (page === 'newSession') {
|
|
1240
|
+
return (
|
|
1241
|
+
<Box flexDirection="column" width={VIEW_W} height={MID_H}>
|
|
1242
|
+
{creating && <StateDisplay type="loading" message="Creating..." />}
|
|
1243
|
+
{createErr && <StateDisplay type="error" message={`Failed to create: ${createErr}`} />}
|
|
1244
|
+
{newSessionId && <Box alignItems="center" justifyContent="center" flexGrow={1}><Text color="green">New Session: {newSessionId}</Text></Box>}
|
|
1245
|
+
{!creating && !createErr && !newSessionId && <StateDisplay type="empty" message="Entered new session page, waiting for result..." />}
|
|
1246
|
+
</Box>
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// History
|
|
1251
|
+
if (page === 'history') {
|
|
1252
|
+
if (histErr) return <StateDisplay type="error" message={histErr} onRetry={refreshHistoryList} />;
|
|
1253
|
+
if (historyMenuStage === 'deleteConfirm' && selectedHistory) {
|
|
1254
|
+
const halfH = Math.floor(MID_H / 2);
|
|
1255
|
+
const items = [
|
|
1256
|
+
{ label: 'Yes, Delete', value: '__confirm_delete' },
|
|
1257
|
+
{ label: 'No, Back', value: '__cancel_delete' },
|
|
1258
|
+
];
|
|
1259
|
+
return (
|
|
1260
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1261
|
+
<Box height={halfH} flexDirection="column" paddingX={1} paddingTop={1}>
|
|
1262
|
+
<Text color="red" bold>Confirm Delete?</Text>
|
|
1263
|
+
<Text>Title: {selectedHistory.title || 'Untitled'}</Text>
|
|
1264
|
+
<Text dimColor>Time: {selectedHistory.createdAt?.replace('T',' ')}</Text>
|
|
1265
|
+
<Text dimColor>ID: {selectedHistory.id}</Text>
|
|
1266
|
+
</Box>
|
|
1267
|
+
<Panel height={MID_H - halfH} borderStyle="round" borderColor="red" paddingX={1}>
|
|
1268
|
+
<SelectInput
|
|
1269
|
+
items={items}
|
|
1270
|
+
onSelect={(item) => handleHistoryMenuAction(String(item.value))}
|
|
1271
|
+
/>
|
|
1272
|
+
<Hint>Enter Confirm · q Cancel</Hint>
|
|
1273
|
+
</Panel>
|
|
1274
|
+
</Box>
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (!historyList.length && loadingHist) {
|
|
1278
|
+
return <StateDisplay type="loading" message="Loading..." />;
|
|
1279
|
+
}
|
|
1280
|
+
if (!historyList.length) {
|
|
1281
|
+
return <StateDisplay type="empty" message={i18nMap[lang].emptyHistory} />;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const ACTIONS_H = 7; // Actions title(1) + 4 items + hint(1) + padding(1)
|
|
1285
|
+
const LIST_H = Math.max(2, MID_H - ACTIONS_H - 1);
|
|
1286
|
+
|
|
1287
|
+
if (historyMenuStage === 'window' && selectedHistory) {
|
|
1288
|
+
// Detailed View with Split
|
|
1289
|
+
const isMarked = markedSessionIds.has(selectedHistory.id);
|
|
1290
|
+
const displayMsgs = sessionMessages.filter(
|
|
1291
|
+
m => m.type === 'user' || m.type === 'assistant' || m.type === 'system'
|
|
1292
|
+
);
|
|
1293
|
+
const totalPages = Math.max(1, Math.ceil(displayMsgs.length / MSGS_PAGE_SIZE));
|
|
1294
|
+
const safePage = Math.min(msgsPage, totalPages - 1);
|
|
1295
|
+
const pageStart = safePage * MSGS_PAGE_SIZE;
|
|
1296
|
+
const pageMsgs = displayMsgs.slice(pageStart, pageStart + MSGS_PAGE_SIZE);
|
|
1297
|
+
|
|
1298
|
+
return (
|
|
1299
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1300
|
+
{/* Upper Pane: Preview */}
|
|
1301
|
+
<Box height={LIST_H} flexDirection="column" paddingX={1} overflow="hidden">
|
|
1302
|
+
<Box justifyContent="space-between" marginBottom={0}>
|
|
1303
|
+
<Text color={isMarked ? 'yellow' : 'cyan'} bold>
|
|
1304
|
+
{isMarked ? '★ ' : ''}{truncate(selectedHistory.title || 'Untitled', VIEW_W - 24)}
|
|
1305
|
+
</Text>
|
|
1306
|
+
<Text dimColor>{selectedHistory.createdAt?.slice(0,16).replace('T',' ')}</Text>
|
|
1307
|
+
</Box>
|
|
1308
|
+
|
|
1309
|
+
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
1310
|
+
{loadingMsgs && <StateDisplay type="loading" message="Loading messages..." />}
|
|
1311
|
+
{msgsErr && <StateDisplay type="error" message={msgsErr} />}
|
|
1312
|
+
{!loadingMsgs && pageMsgs.length === 0 && <StateDisplay type="empty" message="No messages" />}
|
|
1313
|
+
{pageMsgs.map((msg) => {
|
|
1314
|
+
const text = extractTextFromContent(msg.content);
|
|
1315
|
+
const roleLabel = msg.type === 'user' ? 'You' : 'Bot';
|
|
1316
|
+
const roleColor = msg.type === 'user' ? 'green' : 'cyan';
|
|
1317
|
+
return (
|
|
1318
|
+
<Box key={msg.id} marginBottom={0} flexDirection="column" height={1} overflow="hidden">
|
|
1319
|
+
<Text color={roleColor} bold>{roleLabel}: <Text color="white" bold={false}>{clampTextLines(text, VIEW_W - 10, 1)}</Text></Text>
|
|
1320
|
+
</Box>
|
|
1321
|
+
);
|
|
1322
|
+
})}
|
|
1323
|
+
</Box>
|
|
1324
|
+
{totalPages > 1 && (
|
|
1325
|
+
<Box justifyContent="center" height={1}>
|
|
1326
|
+
<Hint>Page {safePage + 1}/{totalPages} (↑↓ to scroll)</Hint>
|
|
1327
|
+
</Box>
|
|
1328
|
+
)}
|
|
1329
|
+
</Box>
|
|
1330
|
+
|
|
1331
|
+
<Box height={1} marginBottom={0}><Text dimColor>{'─'.repeat(VIEW_W - 4)}</Text></Box>
|
|
1332
|
+
|
|
1333
|
+
{/* Lower Pane: Actions */}
|
|
1334
|
+
<Box height={ACTIONS_H} paddingX={1} flexDirection="column" overflow="hidden">
|
|
1335
|
+
<Text color="magenta" bold>Actions</Text>
|
|
1336
|
+
<Box marginTop={0} height={ACTIONS_H - 2} overflow="hidden">
|
|
1337
|
+
<SelectInput
|
|
1338
|
+
items={secondaryMenu?.items || []}
|
|
1339
|
+
onSelect={secondaryMenu?.onSelect}
|
|
1340
|
+
/>
|
|
1341
|
+
</Box>
|
|
1342
|
+
<Hint>ESC Back · ↑↓ Select Action · Q/M/C Shortcut</Hint>
|
|
1343
|
+
</Box>
|
|
1344
|
+
</Box>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// History List View (Default)
|
|
1349
|
+
// MID_H - 1 (hint bar at top) - 1 (scrollbar safety) = MID_H - 2 visible items
|
|
1350
|
+
const HIST_VISIBLE = MID_H - 2;
|
|
1351
|
+
const start = Math.min(listOffset, Math.max(0, groupedHistoryItems.length - HIST_VISIBLE));
|
|
1352
|
+
const slicedItems = groupedHistoryItems.slice(start, start + HIST_VISIBLE);
|
|
1353
|
+
|
|
1354
|
+
return (
|
|
1355
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1356
|
+
{/* Hint bar — fixed 1 row at top, never overlaps list */}
|
|
1357
|
+
<Box height={1} paddingX={1}>
|
|
1358
|
+
<Hint>{i18nMap[lang].historyHint}</Hint>
|
|
1359
|
+
</Box>
|
|
1360
|
+
{/* List area — takes the rest of the height */}
|
|
1361
|
+
<Box flexDirection="row" flexGrow={1} position="relative">
|
|
1362
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
1363
|
+
<SelectInput
|
|
1364
|
+
key={`${historyCursor ?? 'first'}:${slicedItems.length}:${start}`}
|
|
1365
|
+
items={slicedItems}
|
|
1366
|
+
onSelect={item => {
|
|
1367
|
+
if (String(item.value).startsWith('__group_')) return;
|
|
1368
|
+
const session = historyList.find(h => h.id === item.value);
|
|
1369
|
+
if (session) {
|
|
1370
|
+
setSelectedHistory(session);
|
|
1371
|
+
setHistoryMenuStage('window');
|
|
1372
|
+
}
|
|
1373
|
+
}}
|
|
1374
|
+
itemComponent={({ isSelected, label }) => {
|
|
1375
|
+
const it = groupedHistoryItems.find(i => i.label === label);
|
|
1376
|
+
const isGroup = it?.isGroup;
|
|
1377
|
+
const color = it?.color;
|
|
1378
|
+
return (
|
|
1379
|
+
<Box height={1} overflow="hidden">
|
|
1380
|
+
<Text wrap="truncate" color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
|
|
1381
|
+
{isSelected ? '> ' : ' '}{label}
|
|
1382
|
+
</Text>
|
|
1383
|
+
</Box>
|
|
1384
|
+
)
|
|
1385
|
+
}}
|
|
1386
|
+
/>
|
|
1387
|
+
</Box>
|
|
1388
|
+
<ScrollBar total={groupedHistoryItems.length} offset={start} height={MID_H - 3} />
|
|
1389
|
+
</Box>
|
|
1390
|
+
</Box>
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Provider
|
|
1395
|
+
if (page === 'provider') {
|
|
1396
|
+
if (!apiUrl) {
|
|
1397
|
+
return (
|
|
1398
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1399
|
+
<StateDisplay
|
|
1400
|
+
type={bootErr ? "error" : "loading"}
|
|
1401
|
+
message={bootErr ? `Server boot failed: ${bootErr}` : 'Starting server, please wait...'}
|
|
1402
|
+
onRetry={() => process.exit(1)} // Or another way to trigger reboot
|
|
1403
|
+
/>
|
|
1404
|
+
<Text dimColor alignSelf="center">ESC for main menu</Text>
|
|
1405
|
+
</Box>
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
return (
|
|
1409
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1410
|
+
<ProviderPanel apiUrl={apiUrl} height={MID_H} onBack={() => setPage(null)} />
|
|
1411
|
+
</Box>
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Settings
|
|
1416
|
+
if (page === 'settings') {
|
|
1417
|
+
if (loadingSetting) return <StateDisplay type="loading" message="Loading settings..." />;
|
|
1418
|
+
if (setErr) return <StateDisplay type="error" message={setErr} />;
|
|
1419
|
+
|
|
1420
|
+
const tS = i18nMap[lang];
|
|
1421
|
+
const currentLangLabel = LANG_OPTIONS.find(o => o.value === lang)?.label ?? lang;
|
|
1422
|
+
|
|
1423
|
+
// --- langPicker sub-menu ---
|
|
1424
|
+
if (settingsStage === 'langPicker') {
|
|
1425
|
+
return (
|
|
1426
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1427
|
+
<Box paddingX={1} marginBottom={1}>
|
|
1428
|
+
<Text color="magenta" bold>{tS.langPickerTitle}</Text>
|
|
1429
|
+
</Box>
|
|
1430
|
+
<Box paddingX={2} flexGrow={1} flexDirection="column">
|
|
1431
|
+
<SelectInput
|
|
1432
|
+
items={tS.langOptions}
|
|
1433
|
+
initialIndex={tS.langOptions.findIndex(o => o.value === lang)}
|
|
1434
|
+
onSelect={(item: { label: string; value: Lang }) => {
|
|
1435
|
+
setLang(item.value);
|
|
1436
|
+
try { writeBingoSettings({ language: item.value }); } catch {}
|
|
1437
|
+
setSettingsStage('list');
|
|
1438
|
+
}}
|
|
1439
|
+
/>
|
|
1440
|
+
</Box>
|
|
1441
|
+
<Box paddingX={1}>
|
|
1442
|
+
<Hint>↩ confirm · ESC back</Hint>
|
|
1443
|
+
</Box>
|
|
1444
|
+
</Box>
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// --- settings list ---
|
|
1449
|
+
type SettingRow = { key: string; label: string; value: string; interactive: boolean };
|
|
1450
|
+
const fixedRows: SettingRow[] = [
|
|
1451
|
+
{ key: '__lang', label: tS.langLabel, value: currentLangLabel, interactive: true },
|
|
1452
|
+
{ key: '__autoMode', label: tS.autoModeLabel, value: autoModeEnabled ? tS.autoModeOn : tS.autoModeOff, interactive: true },
|
|
1453
|
+
{ key: '__bypassPerms', label: tS.bypassPermsLabel, value: bypassPermsEnabled ? tS.bypassPermsOn : tS.bypassPermsOff, interactive: true },
|
|
1454
|
+
{ key: '__vscode', label: tS.vscodeLabel, value: vscodeLinked ? tS.vscodeOn : tS.vscodeOff, interactive: true },
|
|
1455
|
+
];
|
|
1456
|
+
const dataEntries = settingData && typeof settingData === 'object' ? Object.entries(settingData) : [];
|
|
1457
|
+
const dataRows: SettingRow[] = dataEntries.map(([k, v]) => ({
|
|
1458
|
+
key: k,
|
|
1459
|
+
label: k,
|
|
1460
|
+
value: typeof v === 'object' ? JSON.stringify(v) : String(v),
|
|
1461
|
+
interactive: false,
|
|
1462
|
+
}));
|
|
1463
|
+
const allRows: SettingRow[] = [...fixedRows, ...dataRows];
|
|
1464
|
+
const visible = Math.max(1, MID_H - 2);
|
|
1465
|
+
const start = Math.min(settingsOffset, Math.max(0, allRows.length - visible));
|
|
1466
|
+
const sliced = allRows.slice(start, start + visible);
|
|
1467
|
+
|
|
1468
|
+
return (
|
|
1469
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1470
|
+
<Box flexDirection="row" position="relative" flexGrow={1}>
|
|
1471
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
1472
|
+
{sliced.map((row, idx) => {
|
|
1473
|
+
const absIdx = start + idx;
|
|
1474
|
+
const isCursor = absIdx === settingsCursor;
|
|
1475
|
+
const prefix = isCursor ? '>' : ' ';
|
|
1476
|
+
const labelColor = isCursor ? 'cyan' : (row.interactive ? 'white' : 'gray');
|
|
1477
|
+
const valueColor = row.interactive ? 'green' : undefined;
|
|
1478
|
+
return (
|
|
1479
|
+
<Box key={row.key} height={1}>
|
|
1480
|
+
<Text color={labelColor}>
|
|
1481
|
+
{prefix} {row.label}:{' '}
|
|
1482
|
+
<Text color={valueColor ?? (isCursor ? 'white' : 'gray')}>
|
|
1483
|
+
{row.value}
|
|
1484
|
+
{row.interactive ? ' ↩' : ''}
|
|
1485
|
+
</Text>
|
|
1486
|
+
</Text>
|
|
1487
|
+
</Box>
|
|
1488
|
+
);
|
|
1489
|
+
})}
|
|
1490
|
+
</Box>
|
|
1491
|
+
<ScrollBar total={allRows.length} offset={start} height={visible - 1} />
|
|
1492
|
+
</Box>
|
|
1493
|
+
<Box paddingX={1}>
|
|
1494
|
+
{vscodeLinkErr && <Text color="red">{vscodeLinkErr}</Text>}
|
|
1495
|
+
{!vscodeLinkErr && <Hint>{tS.settingsHint} · {start + 1}-{Math.min(start + visible, allRows.length)}/{allRows.length}</Hint>}
|
|
1496
|
+
</Box>
|
|
1497
|
+
</Box>
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// About
|
|
1502
|
+
if (page === 'about') {
|
|
1503
|
+
return (
|
|
1504
|
+
<Box width={VIEW_W} height={MID_H} flexDirection="column">
|
|
1505
|
+
<Text color="cyan" bold>{i18nMap[lang].about}</Text>
|
|
1506
|
+
<Box marginTop={1} flexDirection="column">
|
|
1507
|
+
<Text>{(i18nMap[lang] as any).aboutContent}</Text>
|
|
1508
|
+
</Box>
|
|
1509
|
+
<Box marginTop={1}>
|
|
1510
|
+
<Hint>
|
|
1511
|
+
API Base: {apiUrl}
|
|
1512
|
+
</Hint>
|
|
1513
|
+
</Box>
|
|
1514
|
+
<Box marginTop={1}>
|
|
1515
|
+
<Text color="gray">{(i18nMap[lang] as any).aboutFooter}</Text>
|
|
1516
|
+
</Box>
|
|
1517
|
+
</Box>
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Exit
|
|
1522
|
+
if (page === 'exit') {
|
|
1523
|
+
exit();
|
|
1524
|
+
return <Box width={VIEW_W} height={MID_H}><Text>Exiting...</Text></Box>;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return <Box width={VIEW_W} height={MID_H} />;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Exit logic
|
|
1531
|
+
if (terminalSize.columns < 60 || terminalSize.rows < 15) {
|
|
1532
|
+
return (
|
|
1533
|
+
<Box flexDirection="column" padding={2}>
|
|
1534
|
+
<Text color="red">Terminal too small!</Text>
|
|
1535
|
+
<Text>Current: {terminalSize.columns}x{terminalSize.rows}</Text>
|
|
1536
|
+
<Text>Please resize to continue...</Text>
|
|
1537
|
+
</Box>
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Root Render
|
|
1542
|
+
return (
|
|
1543
|
+
<Box flexDirection="column" width={VIEW_W}>
|
|
1544
|
+
{/* Top Welcome / Logo Area + Toolbar */}
|
|
1545
|
+
<TopBar
|
|
1546
|
+
ready={configReady}
|
|
1547
|
+
page={page}
|
|
1548
|
+
width={VIEW_W}
|
|
1549
|
+
height={TOP_H}
|
|
1550
|
+
toolbar={
|
|
1551
|
+
<TopToolbar
|
|
1552
|
+
ready={configReady}
|
|
1553
|
+
page={page}
|
|
1554
|
+
animEnabled={animEnabled}
|
|
1555
|
+
tipsEnabled={tipsEnabled}
|
|
1556
|
+
ip={apiUrl ? apiUrl.replace(/^https?:\/\//, '') : undefined}
|
|
1557
|
+
/>
|
|
1558
|
+
}
|
|
1559
|
+
/>
|
|
1560
|
+
|
|
1561
|
+
{/* Center Center Area */}
|
|
1562
|
+
{page === null ? (
|
|
1563
|
+
<Panel width={VIEW_W} height={MID_H} noBorder paddingX={0} paddingY={0} marginY={0}>
|
|
1564
|
+
{renderCenter()}
|
|
1565
|
+
</Panel>
|
|
1566
|
+
) : (
|
|
1567
|
+
<Panel width={VIEW_W} height={MID_H} borderStyle="single" paddingX={1} paddingY={0} marginY={1}>
|
|
1568
|
+
{renderCenter()}
|
|
1569
|
+
</Panel>
|
|
1570
|
+
)}
|
|
1571
|
+
|
|
1572
|
+
{/* Bottom Menu & Secondary Menu */}
|
|
1573
|
+
<BottomBar
|
|
1574
|
+
width={VIEW_W}
|
|
1575
|
+
height={BOTTOM_H}
|
|
1576
|
+
menuItems={menuItems}
|
|
1577
|
+
page={page}
|
|
1578
|
+
navIndex={navIndex}
|
|
1579
|
+
tips={i18nMap[lang].tipsSimple}
|
|
1580
|
+
secondaryMenu={
|
|
1581
|
+
page === 'history' && (historyMenuStage === 'window' || historyMenuStage === 'deleteConfirm')
|
|
1582
|
+
? null
|
|
1583
|
+
: secondaryMenu
|
|
1584
|
+
}
|
|
1585
|
+
/>
|
|
1586
|
+
</Box>
|
|
1587
|
+
);
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1584
1590
|
export default CliMenuManager;
|