kob-cli 1.0.23 → 1.0.25
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/ui/code-tui.tsx +189 -103
- package/src/ui/colors.ts +1 -0
package/package.json
CHANGED
package/src/ui/code-tui.tsx
CHANGED
|
@@ -62,6 +62,58 @@ export function kobImagesDir(): string {
|
|
|
62
62
|
return dir;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export function kobHistoryDir(): string {
|
|
66
|
+
const dir = join(homedir(), '.kob-cli', 'history');
|
|
67
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getHistoryFilePath(): string {
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const slug = cwd.replace(/[\\/:\s]+/g, '_').slice(0, 120);
|
|
74
|
+
return join(kobHistoryDir(), `${slug}.json`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function loadChatHistory(): {
|
|
78
|
+
exchanges: Exchange[];
|
|
79
|
+
messages: { role: string; content: string }[];
|
|
80
|
+
} {
|
|
81
|
+
const path = getHistoryFilePath();
|
|
82
|
+
if (!existsSync(path)) return { exchanges: [], messages: [] };
|
|
83
|
+
try {
|
|
84
|
+
const raw = readFileSync(path, 'utf-8');
|
|
85
|
+
const data = JSON.parse(raw);
|
|
86
|
+
if (data.exchanges && Array.isArray(data.exchanges)) {
|
|
87
|
+
return {
|
|
88
|
+
exchanges: data.exchanges,
|
|
89
|
+
messages: data.messages || [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
} catch { /* ignore corrupted history */ }
|
|
93
|
+
return { exchanges: [], messages: [] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function saveChatHistory(
|
|
97
|
+
exchanges: Exchange[],
|
|
98
|
+
messages: { role: string; content: string }[]
|
|
99
|
+
): void {
|
|
100
|
+
const path = getHistoryFilePath();
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(
|
|
103
|
+
path,
|
|
104
|
+
JSON.stringify({ exchanges, messages, savedAt: Date.now() }, null, 2),
|
|
105
|
+
'utf-8'
|
|
106
|
+
);
|
|
107
|
+
} catch { /* ignore write errors */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearChatHistory(): void {
|
|
111
|
+
const path = getHistoryFilePath();
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(path)) writeFileSync(path, '{}', 'utf-8');
|
|
114
|
+
} catch { /* ignore write errors */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
65
117
|
// Save a local file path into our images dir and return the absolute path.
|
|
66
118
|
export function attachImagePath(srcPath: string): string {
|
|
67
119
|
const abs = resolve(srcPath);
|
|
@@ -455,92 +507,80 @@ function BrandHeader({
|
|
|
455
507
|
}) {
|
|
456
508
|
const { frame } = useAnimation({ interval: 600 });
|
|
457
509
|
const isThinking = phase === 'generating';
|
|
458
|
-
const dotColor =
|
|
459
|
-
const statusText =
|
|
460
|
-
const statusIcon =
|
|
510
|
+
const dotColor = c.green;
|
|
511
|
+
const statusText = 'ready';
|
|
512
|
+
const statusIcon = '●';
|
|
461
513
|
|
|
462
514
|
const K = c.brand; // cyan for KOB
|
|
463
|
-
const C = c.
|
|
515
|
+
const C = c.green; // green for CLI
|
|
464
516
|
|
|
465
517
|
return (
|
|
466
|
-
<Box flexDirection="column">
|
|
518
|
+
<Box flexDirection="column" marginTop={2}>
|
|
467
519
|
{/* Big ASCII KOB CLI logo */}
|
|
468
520
|
<Box flexDirection="column" alignItems="center">
|
|
469
521
|
<Box>
|
|
470
|
-
<Text color={
|
|
522
|
+
<Text color={c.blue}>██╗ ██╗ ██████╗ ██████╗ </Text>
|
|
471
523
|
<Text color={C}>██████╗██╗ ██╗</Text>
|
|
472
524
|
</Box>
|
|
473
525
|
<Box>
|
|
474
|
-
<Text color={
|
|
526
|
+
<Text color={c.blue}>██║ ██╔╝██╔═══██╗██╔══██╗ </Text>
|
|
475
527
|
<Text color={C}>██╔════╝██║ ██║</Text>
|
|
476
528
|
</Box>
|
|
477
529
|
<Box>
|
|
478
|
-
<Text color=
|
|
530
|
+
<Text color="white">█████╔╝ ██║ ██║██████╔╝ </Text>
|
|
479
531
|
<Text color={C}>██║ ██║ ██║</Text>
|
|
480
532
|
</Box>
|
|
481
533
|
<Box>
|
|
482
|
-
<Text color=
|
|
534
|
+
<Text color="white">██╔═██╗ ██║ ██║██╔══██╗ </Text>
|
|
483
535
|
<Text color={C}>██║ ██║ ██║</Text>
|
|
484
536
|
</Box>
|
|
485
537
|
<Box>
|
|
486
|
-
<Text color={
|
|
538
|
+
<Text color={c.red}>██║ ██╗╚██████╔╝██████╔╝ </Text>
|
|
487
539
|
<Text color={C}>╚██████╗███████╗██║</Text>
|
|
488
540
|
</Box>
|
|
489
541
|
<Box>
|
|
490
|
-
<Text color={
|
|
542
|
+
<Text color={c.red}>╚═╝ ╚═╝ ╚═════╝ ╚═════╝ </Text>
|
|
491
543
|
<Text color={C}>╚═════╝╚══════╝╚═╝</Text>
|
|
492
544
|
</Box>
|
|
493
545
|
</Box>
|
|
494
546
|
|
|
547
|
+
{/* Tagline */}
|
|
548
|
+
<Box justifyContent="center" marginTop={1}>
|
|
549
|
+
<Text color={c.textMuted} italic>Made in Thailand</Text>
|
|
550
|
+
</Box>
|
|
551
|
+
|
|
495
552
|
{/* Info bar with brand identity + live status */}
|
|
496
553
|
<Box
|
|
497
|
-
borderStyle="
|
|
554
|
+
borderStyle="single"
|
|
555
|
+
borderTop={false}
|
|
556
|
+
borderLeft={false}
|
|
557
|
+
borderRight={false}
|
|
498
558
|
borderColor={c.brand}
|
|
499
559
|
paddingX={1}
|
|
500
560
|
flexDirection="column"
|
|
501
561
|
marginTop={1}
|
|
502
562
|
>
|
|
503
|
-
<Box>
|
|
504
|
-
<Text color={c.text} bold>KOB CLI</Text>
|
|
505
|
-
<Text color={c.textDim}> </Text>
|
|
506
|
-
<Text color={c.pink} italic>Thailand</Text>
|
|
507
|
-
<Text> </Text>
|
|
508
|
-
<Text backgroundColor="red" color="red">▰▰</Text>
|
|
509
|
-
<Text> </Text>
|
|
510
|
-
<Text color="white" bold>▰</Text>
|
|
511
|
-
<Text> </Text>
|
|
512
|
-
<Text backgroundColor="blue" color="blue">▰▰</Text>
|
|
513
|
-
<Text color={c.textDim}> · </Text>
|
|
514
|
-
<Text color={c.brand} bold>powered by</Text>
|
|
515
|
-
<Text> </Text>
|
|
516
|
-
<Text color={c.green} bold>Tavon Seesenpila</Text>
|
|
517
|
-
<Text color={c.textDim}> </Text>
|
|
518
|
-
<Text color={c.textMuted} italic>Founder of Kob AI</Text>
|
|
519
|
-
</Box>
|
|
520
|
-
|
|
521
|
-
{/* Row 2: version + model info */}
|
|
522
563
|
<Box>
|
|
523
564
|
<Box>
|
|
524
|
-
<Text color={c.
|
|
565
|
+
<Text color={c.text} bold>KOB CLI</Text>
|
|
566
|
+
<Text color={c.textDim}> · </Text>
|
|
567
|
+
<Text color={c.green} bold>Tavon Seesenpila</Text>
|
|
568
|
+
<Text color={c.textDim}> </Text>
|
|
569
|
+
<Text color={c.textMuted} italic>Founder of Kob AI</Text>
|
|
525
570
|
</Box>
|
|
526
571
|
<Box flexGrow={1} />
|
|
572
|
+
<Box>
|
|
573
|
+
<Text color={c.textDim}>v{version}</Text>
|
|
574
|
+
</Box>
|
|
575
|
+
</Box>
|
|
576
|
+
|
|
577
|
+
{/* Row 2: model info */}
|
|
578
|
+
<Box>
|
|
527
579
|
<Box>
|
|
528
580
|
<Text color={c.textDim}>model </Text>
|
|
529
581
|
<Text color={c.pink} bold>{modelName}</Text>
|
|
530
582
|
<Text color={c.textDim}> · </Text>
|
|
531
583
|
<Text color={c.text}>{provider}</Text>
|
|
532
|
-
<Text color={c.textDim}> · </Text>
|
|
533
|
-
<Text color={c.textMuted}>/v2/chat</Text>
|
|
534
|
-
<Text color={c.textDim}> · </Text>
|
|
535
|
-
<Text color={c.green}>bearer</Text>
|
|
536
|
-
<Text color={c.textDim}> · </Text>
|
|
537
|
-
{modelSupportsVision(modelName) ? (
|
|
538
|
-
<Text color={c.accent} bold>👁 vision</Text>
|
|
539
|
-
) : (
|
|
540
|
-
<Text color={c.borderDim}>◌ text-only</Text>
|
|
541
|
-
)}
|
|
542
|
-
<Text color={c.textDim}> · </Text>
|
|
543
|
-
<Text color={c.green}>stream ✓</Text>
|
|
544
584
|
</Box>
|
|
545
585
|
</Box>
|
|
546
586
|
|
|
@@ -566,7 +606,7 @@ function BrandHeader({
|
|
|
566
606
|
<Text color={c.textDim}> </Text>
|
|
567
607
|
<Text color={c.pink}>↑ {formatNum(session.totalOut)}</Text>
|
|
568
608
|
<Text color={c.textDim}> · </Text>
|
|
569
|
-
<Text color={dotColor}>{statusIcon}
|
|
609
|
+
<Text color={dotColor}>{statusIcon}</Text>
|
|
570
610
|
</Box>
|
|
571
611
|
</Box>
|
|
572
612
|
</Box>
|
|
@@ -608,6 +648,7 @@ type LineItem =
|
|
|
608
648
|
| { kind: 'response-line'; text: string; modeColor: string; isFirst: boolean }
|
|
609
649
|
| { kind: 'response-trunc'; totalChars: number; totalLines: number }
|
|
610
650
|
| { kind: 'no-response' }
|
|
651
|
+
| { kind: 'code-block'; lang: string; lines: string[] }
|
|
611
652
|
| { kind: 'file-line'; filename: string; add: number; del: number; created: boolean }
|
|
612
653
|
| { kind: 'cmd-header'; cmd: string; ok: boolean; statusColor: string; exitCode: number; durationMs: number }
|
|
613
654
|
| { kind: 'cmd-output'; text: string; statusColor: string }
|
|
@@ -641,14 +682,46 @@ function buildLineBuffer(exchanges: Exchange[]): LineItem[] {
|
|
|
641
682
|
const total = respLines.length;
|
|
642
683
|
const trunc = total > MAX_RESP_LINES;
|
|
643
684
|
const shown = trunc ? respLines.slice(0, MAX_RESP_LINES) : respLines;
|
|
685
|
+
|
|
686
|
+
let inCode = false;
|
|
687
|
+
let codeBuf: string[] = [];
|
|
688
|
+
let codeLang = '';
|
|
689
|
+
|
|
690
|
+
const flushCode = () => {
|
|
691
|
+
if (codeBuf.length > 0) {
|
|
692
|
+
buf.push({ kind: 'code-block', lang: codeLang, lines: codeBuf });
|
|
693
|
+
codeBuf = [];
|
|
694
|
+
codeLang = '';
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
644
698
|
shown.forEach((line, j) => {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
699
|
+
const trimmed = line.trim();
|
|
700
|
+
if (trimmed.startsWith('```')) {
|
|
701
|
+
if (inCode) {
|
|
702
|
+
flushCode();
|
|
703
|
+
inCode = false;
|
|
704
|
+
} else {
|
|
705
|
+
flushCode(); // flush any prose before code block
|
|
706
|
+
inCode = true;
|
|
707
|
+
codeLang = trimmed.slice(3).trim();
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (inCode) {
|
|
712
|
+
codeBuf.push(line);
|
|
713
|
+
} else {
|
|
714
|
+
buf.push({
|
|
715
|
+
kind: 'response-line',
|
|
716
|
+
text: line,
|
|
717
|
+
modeColor: m.color,
|
|
718
|
+
isFirst: j === 0,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
651
721
|
});
|
|
722
|
+
|
|
723
|
+
if (inCode) flushCode(); // response ended inside code block
|
|
724
|
+
|
|
652
725
|
if (trunc) {
|
|
653
726
|
buf.push({ kind: 'response-trunc', totalChars: exc.output.length, totalLines: total });
|
|
654
727
|
}
|
|
@@ -745,6 +818,28 @@ function renderLine(item: LineItem, idx: number): React.ReactNode {
|
|
|
745
818
|
<Text color={c.red}>✗ (no response received)</Text>
|
|
746
819
|
</Box>
|
|
747
820
|
);
|
|
821
|
+
case 'code-block':
|
|
822
|
+
return (
|
|
823
|
+
<Box key={key} marginLeft={2} marginTop={1} marginBottom={1}>
|
|
824
|
+
<Box
|
|
825
|
+
borderStyle="round"
|
|
826
|
+
borderColor={c.borderDim}
|
|
827
|
+
backgroundColor={c.panel}
|
|
828
|
+
paddingX={1}
|
|
829
|
+
paddingY={1}
|
|
830
|
+
flexDirection="column"
|
|
831
|
+
>
|
|
832
|
+
<Box>
|
|
833
|
+
<Text color={c.accent}>◆ {item.lang || 'code'}</Text>
|
|
834
|
+
</Box>
|
|
835
|
+
{item.lines.map((line, i) => (
|
|
836
|
+
<Box key={i}>
|
|
837
|
+
<Text color={c.green}>{line.length === 0 ? ' ' : line}</Text>
|
|
838
|
+
</Box>
|
|
839
|
+
))}
|
|
840
|
+
</Box>
|
|
841
|
+
</Box>
|
|
842
|
+
);
|
|
748
843
|
case 'file-line':
|
|
749
844
|
return (
|
|
750
845
|
<Box key={key} marginLeft={2}>
|
|
@@ -788,9 +883,13 @@ function renderLine(item: LineItem, idx: number): React.ReactNode {
|
|
|
788
883
|
return (
|
|
789
884
|
<Box key={key} marginLeft={2} marginTop={1}>
|
|
790
885
|
<Text color={c.textDim}>↳ </Text>
|
|
791
|
-
<Text color={c.textMuted}>{item.exc.output.length}
|
|
886
|
+
<Text color={c.textMuted}>{item.exc.output.length} </Text>
|
|
887
|
+
<Text color={c.yellow}>chars</Text>
|
|
792
888
|
<Text color={c.textDim}> · </Text>
|
|
793
|
-
<Text color={c.
|
|
889
|
+
<Text color={c.blue}>↓ {formatNum(item.exc.inTokens)}</Text>
|
|
890
|
+
<Text color={c.textDim}> </Text>
|
|
891
|
+
<Text color={c.pink}>↑ {formatNum(item.exc.outTokens)} </Text>
|
|
892
|
+
<Text color={c.yellow}>tok</Text>
|
|
794
893
|
<Text color={c.textDim}> · </Text>
|
|
795
894
|
<Text color={c.brand}>{item.exc.model}</Text>
|
|
796
895
|
</Box>
|
|
@@ -1067,25 +1166,10 @@ function InputBox({ onSubmit, mode, onModeChange, visionSupported, placeholder,
|
|
|
1067
1166
|
setTimeout(() => setPasteHint(null), 2500);
|
|
1068
1167
|
return;
|
|
1069
1168
|
}
|
|
1070
|
-
if
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
setAttachments([]);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
// Tab: cycle modes UNLESS the slash popup is open (then it fills the highlighted command)
|
|
1080
|
-
if (key.tab) {
|
|
1081
|
-
if (slashOpen) {
|
|
1082
|
-
const cmd = slashMatches[slashIdx];
|
|
1083
|
-
if (cmd) setValue('/' + cmd.name + ' ');
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
const i = MODES.findIndex(m => m.key === mode);
|
|
1087
|
-
const next = MODES[(i + 1) % MODES.length]!;
|
|
1088
|
-
onModeChange(next.key);
|
|
1169
|
+
// Tab: fill highlighted slash command if popup is open
|
|
1170
|
+
if (key.tab && slashOpen) {
|
|
1171
|
+
const cmd = slashMatches[slashIdx];
|
|
1172
|
+
if (cmd) setValue('/' + cmd.name + ' ');
|
|
1089
1173
|
return;
|
|
1090
1174
|
}
|
|
1091
1175
|
if (!key.shift && (char === '1' || char === '2' || char === '3') && value.length === 0) {
|
|
@@ -1158,25 +1242,9 @@ function InputBox({ onSubmit, mode, onModeChange, visionSupported, placeholder,
|
|
|
1158
1242
|
|
|
1159
1243
|
return (
|
|
1160
1244
|
<Box flexDirection="column" borderStyle="round" borderColor={borderColor} marginTop={1}>
|
|
1161
|
-
<Box paddingX={1} borderStyle="single" borderColor={c.borderDim} borderTop={false} borderLeft={false} borderRight={false} justifyContent="space-between">
|
|
1162
|
-
<Box>
|
|
1163
|
-
<Text color={modeInfo.color} bold>▶ </Text>
|
|
1164
|
-
<Text color={c.text} bold>Input</Text>
|
|
1165
|
-
<Text color={c.textDim}> · </Text>
|
|
1166
|
-
<ModeSelector mode={mode} />
|
|
1167
|
-
</Box>
|
|
1168
|
-
<Box>
|
|
1169
|
-
<Text color={c.textDim}>{visionSupported ? '⌘V ' : ''}</Text>
|
|
1170
|
-
<Text color={c.textMuted}>{visionSupported ? 'paste image' : 'text only'}</Text>
|
|
1171
|
-
<Text color={c.borderDim}> · </Text>
|
|
1172
|
-
<Text color={c.textDim}>Tab </Text>
|
|
1173
|
-
<Text color={c.textMuted}>switch</Text>
|
|
1174
|
-
</Box>
|
|
1175
|
-
</Box>
|
|
1176
|
-
|
|
1177
1245
|
{/* Attachment chips */}
|
|
1178
1246
|
{attachments.length > 0 && (
|
|
1179
|
-
<Box paddingX={1}
|
|
1247
|
+
<Box paddingX={1} flexWrap="wrap">
|
|
1180
1248
|
{attachments.map((a, i) => (
|
|
1181
1249
|
<Box key={i} marginRight={1}>
|
|
1182
1250
|
<Text color={c.accent}>📎 </Text>
|
|
@@ -1194,11 +1262,11 @@ function InputBox({ onSubmit, mode, onModeChange, visionSupported, placeholder,
|
|
|
1194
1262
|
</Box>
|
|
1195
1263
|
)}
|
|
1196
1264
|
|
|
1197
|
-
<Box paddingX={1}
|
|
1265
|
+
<Box paddingX={1}>
|
|
1266
|
+
{visionSupported && <Text color={c.green}>🖼️ </Text>}
|
|
1198
1267
|
<Text color={modeInfo.color}>❯ </Text>
|
|
1199
1268
|
{isEmpty ? (
|
|
1200
1269
|
<>
|
|
1201
|
-
<Text color={c.borderDim}>{ph}</Text>
|
|
1202
1270
|
{showCursor && <Text color={modeInfo.color}>{'▌'}</Text>}
|
|
1203
1271
|
</>
|
|
1204
1272
|
) : (
|
|
@@ -1393,7 +1461,6 @@ function BottomBar({ phase, mode, isFirstStart }: { phase: Phase; mode: Mode; is
|
|
|
1393
1461
|
const modeInfo = getMode(mode);
|
|
1394
1462
|
return (
|
|
1395
1463
|
<Box
|
|
1396
|
-
marginTop={1}
|
|
1397
1464
|
borderStyle="round"
|
|
1398
1465
|
borderColor={c.borderDim}
|
|
1399
1466
|
paddingX={1}
|
|
@@ -1403,12 +1470,6 @@ function BottomBar({ phase, mode, isFirstStart }: { phase: Phase; mode: Mode; is
|
|
|
1403
1470
|
<Text color={c.green}>⏎ </Text>
|
|
1404
1471
|
<Text color={c.textDim}>submit</Text>
|
|
1405
1472
|
<Text color={c.borderDim}> · </Text>
|
|
1406
|
-
<Text color={c.yellow}>Esc</Text>
|
|
1407
|
-
<Text color={c.textDim}> clear</Text>
|
|
1408
|
-
<Text color={c.borderDim}> · </Text>
|
|
1409
|
-
<Text color={c.pink}>Tab</Text>
|
|
1410
|
-
<Text color={c.textDim}> mode</Text>
|
|
1411
|
-
<Text color={c.borderDim}> · </Text>
|
|
1412
1473
|
<Text color={c.brand}>/models</Text>
|
|
1413
1474
|
<Text color={c.textDim}> switch</Text>
|
|
1414
1475
|
<Text color={c.borderDim}> · </Text>
|
|
@@ -1503,6 +1564,23 @@ function CodeEngine() {
|
|
|
1503
1564
|
return () => clearInterval(t);
|
|
1504
1565
|
}, []);
|
|
1505
1566
|
|
|
1567
|
+
// Load chat history on mount
|
|
1568
|
+
useEffect(() => {
|
|
1569
|
+
const { exchanges: savedExchanges, messages: savedMessages } = loadChatHistory();
|
|
1570
|
+
if (savedExchanges.length > 0) {
|
|
1571
|
+
setExchanges(savedExchanges);
|
|
1572
|
+
messagesRef.current = savedMessages;
|
|
1573
|
+
exchangesLenRef.current = savedExchanges.length;
|
|
1574
|
+
}
|
|
1575
|
+
}, []);
|
|
1576
|
+
|
|
1577
|
+
// Save chat history whenever exchanges change
|
|
1578
|
+
useEffect(() => {
|
|
1579
|
+
if (exchanges.length > 0) {
|
|
1580
|
+
saveChatHistory(exchanges, messagesRef.current);
|
|
1581
|
+
}
|
|
1582
|
+
}, [exchanges]);
|
|
1583
|
+
|
|
1506
1584
|
// Auto-scroll to bottom on each new round
|
|
1507
1585
|
useEffect(() => {
|
|
1508
1586
|
if (exchanges.length !== exchangesLenRef.current) {
|
|
@@ -1519,9 +1597,9 @@ function CodeEngine() {
|
|
|
1519
1597
|
? now - startMs
|
|
1520
1598
|
: (exchanges.length > 0 ? exchanges.reduce((s, e) => s + e.durationMs, 0) : 0);
|
|
1521
1599
|
|
|
1522
|
-
// Reserve rows for: brand header (~10), input area (~
|
|
1600
|
+
// Reserve rows for: brand header (~10), input area (~2), bottom bar (~1),
|
|
1523
1601
|
// welcome footer when present (~5), and margins/padding (~3).
|
|
1524
|
-
const RESERVED_ROWS =
|
|
1602
|
+
const RESERVED_ROWS = 21;
|
|
1525
1603
|
const convMaxHeight = Math.max(8, viewportRows - RESERVED_ROWS);
|
|
1526
1604
|
|
|
1527
1605
|
// Dispatch a /slash command. Returns true if the input was a slash command
|
|
@@ -1553,6 +1631,7 @@ function CodeEngine() {
|
|
|
1553
1631
|
messagesRef.current = [];
|
|
1554
1632
|
exchangesLenRef.current = 0;
|
|
1555
1633
|
setScrollOffset(0);
|
|
1634
|
+
clearChatHistory();
|
|
1556
1635
|
showBanner('◆ session cleared');
|
|
1557
1636
|
return true;
|
|
1558
1637
|
case 'reset': {
|
|
@@ -1567,6 +1646,7 @@ function CodeEngine() {
|
|
|
1567
1646
|
setExchanges([]);
|
|
1568
1647
|
messagesRef.current = [];
|
|
1569
1648
|
exchangesLenRef.current = 0;
|
|
1649
|
+
clearChatHistory();
|
|
1570
1650
|
showBanner(`◆ reset → model ${newModel}`);
|
|
1571
1651
|
return true;
|
|
1572
1652
|
}
|
|
@@ -1578,7 +1658,7 @@ function CodeEngine() {
|
|
|
1578
1658
|
return true;
|
|
1579
1659
|
case 'help':
|
|
1580
1660
|
case '?':
|
|
1581
|
-
|
|
1661
|
+
setHelpOpen(true);
|
|
1582
1662
|
return true;
|
|
1583
1663
|
case 'exit':
|
|
1584
1664
|
case 'quit':
|
|
@@ -1736,6 +1816,8 @@ function CodeEngine() {
|
|
|
1736
1816
|
}} />
|
|
1737
1817
|
)}
|
|
1738
1818
|
|
|
1819
|
+
{helpOpen && <HelpScreen onClose={() => setHelpOpen(false)} />}
|
|
1820
|
+
|
|
1739
1821
|
{phase === 'generating' ? (
|
|
1740
1822
|
<GeneratingPanel messages={getMode(mode).statusMessages} elapsed={now - startMs} />
|
|
1741
1823
|
) : (
|
|
@@ -1744,11 +1826,15 @@ function CodeEngine() {
|
|
|
1744
1826
|
mode={mode}
|
|
1745
1827
|
onModeChange={setMode}
|
|
1746
1828
|
visionSupported={modelSupportsVision(model)}
|
|
1747
|
-
isActive={palette === null && !configOpen && phase === 'input'}
|
|
1829
|
+
isActive={palette === null && !configOpen && !helpOpen && phase === 'input'}
|
|
1748
1830
|
/>
|
|
1749
1831
|
)}
|
|
1750
1832
|
|
|
1751
|
-
<BottomBar phase={phase} mode={mode} />
|
|
1833
|
+
<BottomBar phase={phase} mode={mode} isFirstStart={isFirstStart} />
|
|
1834
|
+
|
|
1835
|
+
<Box justifyContent="center">
|
|
1836
|
+
<Text color={c.textDim}>📁 {process.cwd()}</Text>
|
|
1837
|
+
</Box>
|
|
1752
1838
|
</Box>
|
|
1753
1839
|
);
|
|
1754
1840
|
}
|