@vellumai/cli 0.4.48 → 0.4.50
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/README.md +7 -9
- package/package.json +1 -1
- package/src/adapters/install.sh +6 -6
- package/src/commands/clean.ts +11 -6
- package/src/commands/client.ts +5 -13
- package/src/commands/hatch.ts +8 -20
- package/src/commands/ps.ts +24 -4
- package/src/commands/recover.ts +1 -1
- package/src/commands/retire.ts +2 -5
- package/src/commands/setup.ts +172 -0
- package/src/commands/sleep.ts +1 -1
- package/src/commands/wake.ts +2 -2
- package/src/components/DefaultMainScreen.tsx +270 -22
- package/src/components/TextInput.tsx +159 -12
- package/src/index.ts +101 -22
- package/src/lib/assistant-config.ts +1 -6
- package/src/lib/constants.ts +7 -1
- package/src/lib/docker.ts +103 -9
- package/src/lib/doctor-client.ts +1 -1
- package/src/lib/gcp.ts +1 -1
- package/src/lib/health-check.ts +87 -0
- package/src/lib/http-client.ts +1 -2
- package/src/lib/input-history.ts +44 -0
- package/src/lib/local.ts +160 -161
- package/src/lib/orphan-detection.ts +13 -3
- package/src/lib/process.ts +3 -1
- package/src/lib/terminal-capabilities.ts +133 -0
- package/src/lib/xdg-log.ts +2 -2
|
@@ -17,7 +17,12 @@ import { removeAssistantEntry } from "../lib/assistant-config";
|
|
|
17
17
|
import { SPECIES_CONFIG, type Species } from "../lib/constants";
|
|
18
18
|
import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
|
|
19
19
|
import { checkHealth } from "../lib/health-check";
|
|
20
|
+
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
20
21
|
import { statusEmoji, withStatusEmoji } from "../lib/status-emoji";
|
|
22
|
+
import {
|
|
23
|
+
getTerminalCapabilities,
|
|
24
|
+
unicodeOrFallback,
|
|
25
|
+
} from "../lib/terminal-capabilities";
|
|
21
26
|
import TextInput from "./TextInput";
|
|
22
27
|
import { Tooltip } from "./Tooltip";
|
|
23
28
|
|
|
@@ -55,7 +60,9 @@ const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
|
55
60
|
const DEFAULT_TERMINAL_ROWS = 24;
|
|
56
61
|
const LEFT_PANEL_WIDTH = 36;
|
|
57
62
|
|
|
58
|
-
const
|
|
63
|
+
const COMPACT_THRESHOLD = 60;
|
|
64
|
+
const HEADER_PREFIX_UNICODE = "── Vellum ";
|
|
65
|
+
const HEADER_PREFIX_ASCII = "-- Vellum ";
|
|
59
66
|
|
|
60
67
|
// Left panel structure: HEADER lines + art + FOOTER lines
|
|
61
68
|
const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
|
|
@@ -493,6 +500,9 @@ async function handleScopeSelection(
|
|
|
493
500
|
|
|
494
501
|
export const TYPING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
495
502
|
|
|
503
|
+
/** ASCII-safe spinner frames for the connection screen. */
|
|
504
|
+
const CONNECTION_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
505
|
+
|
|
496
506
|
export interface ToolCallInfo {
|
|
497
507
|
name: string;
|
|
498
508
|
input: Record<string, unknown>;
|
|
@@ -668,6 +678,75 @@ function SpinnerDisplay({ text }: { text: string }): ReactElement {
|
|
|
668
678
|
);
|
|
669
679
|
}
|
|
670
680
|
|
|
681
|
+
type ConnectionState = "connecting" | "connected" | "error";
|
|
682
|
+
|
|
683
|
+
function ConnectionScreen({
|
|
684
|
+
state,
|
|
685
|
+
errorMessage,
|
|
686
|
+
species,
|
|
687
|
+
terminalRows,
|
|
688
|
+
terminalColumns,
|
|
689
|
+
onRetry,
|
|
690
|
+
onExit,
|
|
691
|
+
}: {
|
|
692
|
+
state: ConnectionState;
|
|
693
|
+
errorMessage?: string;
|
|
694
|
+
species: Species;
|
|
695
|
+
terminalRows: number;
|
|
696
|
+
terminalColumns: number;
|
|
697
|
+
onRetry: () => void;
|
|
698
|
+
onExit: () => void;
|
|
699
|
+
}): ReactElement {
|
|
700
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
701
|
+
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (state !== "connecting") return;
|
|
704
|
+
const timer = setInterval(() => {
|
|
705
|
+
setFrameIndex((prev) => (prev + 1) % CONNECTION_SPINNER_FRAMES.length);
|
|
706
|
+
}, 150);
|
|
707
|
+
return () => clearInterval(timer);
|
|
708
|
+
}, [state]);
|
|
709
|
+
|
|
710
|
+
useInput((input, key) => {
|
|
711
|
+
if (key.ctrl && input === "c") {
|
|
712
|
+
onExit();
|
|
713
|
+
}
|
|
714
|
+
if (state === "error" && input === "r") {
|
|
715
|
+
onRetry();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const config = SPECIES_CONFIG[species];
|
|
720
|
+
const title = `Vellum ${config.hatchedEmoji} ${species}`;
|
|
721
|
+
const width = Math.min(terminalColumns, MAX_TOTAL_WIDTH);
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<Box
|
|
725
|
+
flexDirection="column"
|
|
726
|
+
height={terminalRows}
|
|
727
|
+
width={width}
|
|
728
|
+
justifyContent="center"
|
|
729
|
+
alignItems="center"
|
|
730
|
+
>
|
|
731
|
+
<Text dimColor bold>
|
|
732
|
+
{title}
|
|
733
|
+
</Text>
|
|
734
|
+
<Text> </Text>
|
|
735
|
+
{state === "connecting" ? (
|
|
736
|
+
<Text dimColor>
|
|
737
|
+
{CONNECTION_SPINNER_FRAMES[frameIndex]} Connecting to assistant...
|
|
738
|
+
</Text>
|
|
739
|
+
) : (
|
|
740
|
+
<>
|
|
741
|
+
<Text color="red">Failed to connect: {errorMessage}</Text>
|
|
742
|
+
<Text> </Text>
|
|
743
|
+
<Text dimColor>Press r to retry or Ctrl+C to quit</Text>
|
|
744
|
+
</>
|
|
745
|
+
)}
|
|
746
|
+
</Box>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
671
750
|
export function renderErrorMainScreen(error: unknown): number {
|
|
672
751
|
const msg = error instanceof Error ? error.message : String(error);
|
|
673
752
|
console.log(
|
|
@@ -694,6 +773,37 @@ interface StyledLine {
|
|
|
694
773
|
style: "heading" | "dim" | "normal";
|
|
695
774
|
}
|
|
696
775
|
|
|
776
|
+
function CompactHeader({
|
|
777
|
+
species,
|
|
778
|
+
healthStatus,
|
|
779
|
+
totalWidth,
|
|
780
|
+
}: {
|
|
781
|
+
species: Species;
|
|
782
|
+
healthStatus?: string;
|
|
783
|
+
totalWidth: number;
|
|
784
|
+
}): ReactElement {
|
|
785
|
+
const config = SPECIES_CONFIG[species];
|
|
786
|
+
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
787
|
+
const status = healthStatus ?? "checking...";
|
|
788
|
+
const label = ` ${config.hatchedEmoji} ${species} ${statusEmoji(status)} `;
|
|
789
|
+
const prefix = "── Vellum";
|
|
790
|
+
const suffix = "──";
|
|
791
|
+
const fillLen = Math.max(
|
|
792
|
+
0,
|
|
793
|
+
totalWidth - prefix.length - label.length - suffix.length,
|
|
794
|
+
);
|
|
795
|
+
return (
|
|
796
|
+
<Box flexDirection="column" width={totalWidth}>
|
|
797
|
+
<Text dimColor>
|
|
798
|
+
{prefix}
|
|
799
|
+
<Text color={accentColor}>{label}</Text>
|
|
800
|
+
{"─".repeat(fillLen)}
|
|
801
|
+
{suffix}
|
|
802
|
+
</Text>
|
|
803
|
+
</Box>
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
697
807
|
function DefaultMainScreen({
|
|
698
808
|
runtimeUrl,
|
|
699
809
|
assistantId,
|
|
@@ -705,10 +815,27 @@ function DefaultMainScreen({
|
|
|
705
815
|
const config = SPECIES_CONFIG[species];
|
|
706
816
|
const art = config.art;
|
|
707
817
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
818
|
+
const caps = getTerminalCapabilities();
|
|
819
|
+
const headerPrefix = caps.unicodeSupported
|
|
820
|
+
? HEADER_PREFIX_UNICODE
|
|
821
|
+
: HEADER_PREFIX_ASCII;
|
|
822
|
+
const headerSep = caps.unicodeSupported ? "─" : "-";
|
|
708
823
|
|
|
709
824
|
const { stdout } = useStdout();
|
|
710
825
|
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
711
826
|
const totalWidth = Math.min(MAX_TOTAL_WIDTH, terminalColumns);
|
|
827
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
828
|
+
|
|
829
|
+
if (isCompact) {
|
|
830
|
+
return (
|
|
831
|
+
<CompactHeader
|
|
832
|
+
species={species}
|
|
833
|
+
healthStatus={healthStatus}
|
|
834
|
+
totalWidth={totalWidth}
|
|
835
|
+
/>
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
712
839
|
const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
|
|
713
840
|
|
|
714
841
|
const leftLines = [
|
|
@@ -729,7 +856,10 @@ function DefaultMainScreen({
|
|
|
729
856
|
{ text: "Assistant", style: "heading" },
|
|
730
857
|
{ text: assistantId, style: "dim" },
|
|
731
858
|
{ text: "Species", style: "heading" },
|
|
732
|
-
{
|
|
859
|
+
{
|
|
860
|
+
text: `${unicodeOrFallback(config.hatchedEmoji, `[${species}]`)} ${species}`,
|
|
861
|
+
style: "dim",
|
|
862
|
+
},
|
|
733
863
|
{ text: "Status", style: "heading" },
|
|
734
864
|
{ text: withStatusEmoji(healthStatus ?? "checking..."), style: "dim" },
|
|
735
865
|
];
|
|
@@ -739,8 +869,8 @@ function DefaultMainScreen({
|
|
|
739
869
|
return (
|
|
740
870
|
<Box flexDirection="column" width={totalWidth}>
|
|
741
871
|
<Text dimColor>
|
|
742
|
-
{
|
|
743
|
-
|
|
872
|
+
{headerPrefix +
|
|
873
|
+
headerSep.repeat(Math.max(0, totalWidth - headerPrefix.length))}
|
|
744
874
|
</Text>
|
|
745
875
|
<Box flexDirection="row">
|
|
746
876
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
@@ -755,7 +885,7 @@ function DefaultMainScreen({
|
|
|
755
885
|
}
|
|
756
886
|
if (i > 2 && i <= 2 + art.length) {
|
|
757
887
|
return (
|
|
758
|
-
<Text key={i} color={accentColor}>
|
|
888
|
+
<Text key={i} color={caps.isDumb ? undefined : accentColor}>
|
|
759
889
|
{line}
|
|
760
890
|
</Text>
|
|
761
891
|
);
|
|
@@ -792,7 +922,7 @@ function DefaultMainScreen({
|
|
|
792
922
|
})}
|
|
793
923
|
</Box>
|
|
794
924
|
</Box>
|
|
795
|
-
<Text dimColor>{
|
|
925
|
+
<Text dimColor>{headerSep.repeat(totalWidth)}</Text>
|
|
796
926
|
<Text> </Text>
|
|
797
927
|
</Box>
|
|
798
928
|
);
|
|
@@ -838,9 +968,18 @@ function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
|
|
|
838
968
|
function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
839
969
|
if (isRuntimeMessage(item)) {
|
|
840
970
|
const cols = Math.max(1, terminalColumns);
|
|
971
|
+
// Account for "HH:MM AM Label: " prefix on the first line
|
|
972
|
+
const defaultLabel = item.role === "user" ? "You:" : "Assistant:";
|
|
973
|
+
const label = item.label ?? defaultLabel;
|
|
974
|
+
const prefixLen = 10 + label.length + 1; // timestamp + space + label + space
|
|
841
975
|
let lines = 0;
|
|
842
|
-
|
|
843
|
-
|
|
976
|
+
const contentLines = item.content.split("\n");
|
|
977
|
+
for (let idx = 0; idx < contentLines.length; idx++) {
|
|
978
|
+
const lineLen =
|
|
979
|
+
idx === 0
|
|
980
|
+
? contentLines[idx].length + prefixLen
|
|
981
|
+
: contentLines[idx].length;
|
|
982
|
+
lines += Math.max(1, Math.ceil(lineLen / cols));
|
|
844
983
|
}
|
|
845
984
|
if (item.role === "assistant" && item.toolCalls) {
|
|
846
985
|
for (const tc of item.toolCalls) {
|
|
@@ -870,7 +1009,15 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
|
870
1009
|
return 1;
|
|
871
1010
|
}
|
|
872
1011
|
|
|
873
|
-
|
|
1012
|
+
const COMPACT_HEADER_HEIGHT = 1;
|
|
1013
|
+
|
|
1014
|
+
function calculateHeaderHeight(
|
|
1015
|
+
species: Species,
|
|
1016
|
+
terminalColumns?: number,
|
|
1017
|
+
): number {
|
|
1018
|
+
if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
|
|
1019
|
+
return COMPACT_HEADER_HEIGHT;
|
|
1020
|
+
}
|
|
874
1021
|
const config = SPECIES_CONFIG[species];
|
|
875
1022
|
const artLength = config.art.length;
|
|
876
1023
|
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
@@ -885,6 +1032,9 @@ export function render(
|
|
|
885
1032
|
assistantId: string,
|
|
886
1033
|
species: Species,
|
|
887
1034
|
): number {
|
|
1035
|
+
const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1036
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1037
|
+
|
|
888
1038
|
const config = SPECIES_CONFIG[species];
|
|
889
1039
|
const art = config.art;
|
|
890
1040
|
|
|
@@ -901,6 +1051,10 @@ export function render(
|
|
|
901
1051
|
);
|
|
902
1052
|
unmount();
|
|
903
1053
|
|
|
1054
|
+
if (isCompact) {
|
|
1055
|
+
return COMPACT_HEADER_HEIGHT;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
904
1058
|
const statusCanvasLine = RIGHT_PANEL_LINE_COUNT + HEADER_TOP_BORDER_LINES;
|
|
905
1059
|
const statusCol = LEFT_PANEL_WIDTH + 1;
|
|
906
1060
|
checkHealth(runtimeUrl)
|
|
@@ -1128,6 +1282,9 @@ function ChatApp({
|
|
|
1128
1282
|
handleRef,
|
|
1129
1283
|
}: ChatAppProps): ReactElement {
|
|
1130
1284
|
const [inputValue, setInputValue] = useState("");
|
|
1285
|
+
const historyRef = useRef<string[]>(loadHistory());
|
|
1286
|
+
const historyIndexRef = useRef(-1);
|
|
1287
|
+
const savedInputRef = useRef("");
|
|
1131
1288
|
const [feed, setFeed] = useState<FeedItem[]>([]);
|
|
1132
1289
|
const [spinnerText, setSpinnerText] = useState<string | null>(null);
|
|
1133
1290
|
const [selection, setSelection] = useState<SelectionRequest | null>(null);
|
|
@@ -1139,6 +1296,11 @@ function ChatApp({
|
|
|
1139
1296
|
const [healthStatus, setHealthStatus] = useState<string | undefined>(
|
|
1140
1297
|
undefined,
|
|
1141
1298
|
);
|
|
1299
|
+
const [connectionState, setConnectionState] =
|
|
1300
|
+
useState<ConnectionState>("connecting");
|
|
1301
|
+
const [connectionError, setConnectionError] = useState<string | undefined>(
|
|
1302
|
+
undefined,
|
|
1303
|
+
);
|
|
1142
1304
|
const prevFeedLengthRef = useRef(0);
|
|
1143
1305
|
const busyRef = useRef(false);
|
|
1144
1306
|
const connectedRef = useRef(false);
|
|
@@ -1152,15 +1314,20 @@ function ChatApp({
|
|
|
1152
1314
|
const { stdout } = useStdout();
|
|
1153
1315
|
const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
|
|
1154
1316
|
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1155
|
-
const headerHeight = calculateHeaderHeight(species);
|
|
1317
|
+
const headerHeight = calculateHeaderHeight(species, terminalColumns);
|
|
1156
1318
|
|
|
1319
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1320
|
+
const compactInputAreaHeight = 1; // input row only, no separators
|
|
1321
|
+
const inputAreaHeight = isCompact
|
|
1322
|
+
? compactInputAreaHeight
|
|
1323
|
+
: INPUT_AREA_HEIGHT;
|
|
1157
1324
|
const bottomHeight = selection
|
|
1158
1325
|
? selection.options.length + SELECTION_CHROME_LINES + TOOLTIP_HEIGHT
|
|
1159
1326
|
: secretInput
|
|
1160
1327
|
? SECRET_INPUT_HEIGHT + TOOLTIP_HEIGHT
|
|
1161
1328
|
: spinnerText
|
|
1162
|
-
? SPINNER_HEIGHT +
|
|
1163
|
-
:
|
|
1329
|
+
? SPINNER_HEIGHT + inputAreaHeight
|
|
1330
|
+
: inputAreaHeight;
|
|
1164
1331
|
const availableRows = Math.max(
|
|
1165
1332
|
MIN_FEED_ROWS,
|
|
1166
1333
|
terminalRows - headerHeight - bottomHeight,
|
|
@@ -1197,11 +1364,14 @@ function ChatApp({
|
|
|
1197
1364
|
}
|
|
1198
1365
|
|
|
1199
1366
|
if (scrollIndex === null) {
|
|
1367
|
+
// Reserve 1 line for "N more above" indicator when there are hidden messages
|
|
1200
1368
|
let totalHeight = 0;
|
|
1201
1369
|
let start = feed.length;
|
|
1202
1370
|
for (let i = feed.length - 1; i >= 0; i--) {
|
|
1203
1371
|
const h = estimateItemHeight(feed[i], terminalColumns);
|
|
1204
|
-
|
|
1372
|
+
// Reserve space for the "more above" indicator if we'd hide messages
|
|
1373
|
+
const indicatorLine = i > 0 ? 1 : 0;
|
|
1374
|
+
if (totalHeight + h + indicatorLine > availableRows) {
|
|
1205
1375
|
break;
|
|
1206
1376
|
}
|
|
1207
1377
|
totalHeight += h;
|
|
@@ -1220,11 +1390,16 @@ function ChatApp({
|
|
|
1220
1390
|
}
|
|
1221
1391
|
|
|
1222
1392
|
const start = Math.max(0, Math.min(scrollIndex, feed.length - 1));
|
|
1393
|
+
// Reserve lines for "more above/below" indicators
|
|
1394
|
+
const aboveIndicator = start > 0 ? 1 : 0;
|
|
1395
|
+
const budget = availableRows - aboveIndicator;
|
|
1223
1396
|
let totalHeight = 0;
|
|
1224
1397
|
let end = start;
|
|
1225
1398
|
for (let i = start; i < feed.length; i++) {
|
|
1226
1399
|
const h = estimateItemHeight(feed[i], terminalColumns);
|
|
1227
|
-
|
|
1400
|
+
// Reserve space for "more below" indicator if we'd hide messages
|
|
1401
|
+
const belowIndicator = i + 1 < feed.length ? 1 : 0;
|
|
1402
|
+
if (totalHeight + h + belowIndicator > budget) {
|
|
1228
1403
|
break;
|
|
1229
1404
|
}
|
|
1230
1405
|
totalHeight += h;
|
|
@@ -1378,6 +1553,8 @@ function ChatApp({
|
|
|
1378
1553
|
return false;
|
|
1379
1554
|
}
|
|
1380
1555
|
connectingRef.current = true;
|
|
1556
|
+
setConnectionState("connecting");
|
|
1557
|
+
setConnectionError(undefined);
|
|
1381
1558
|
const h = handleRef_.current;
|
|
1382
1559
|
|
|
1383
1560
|
h.showSpinner("Connecting...");
|
|
@@ -1440,12 +1617,15 @@ function ChatApp({
|
|
|
1440
1617
|
|
|
1441
1618
|
connectedRef.current = true;
|
|
1442
1619
|
connectingRef.current = false;
|
|
1620
|
+
setConnectionState("connected");
|
|
1443
1621
|
return true;
|
|
1444
1622
|
} catch (err) {
|
|
1445
1623
|
h.hideSpinner();
|
|
1446
1624
|
connectingRef.current = false;
|
|
1447
1625
|
h.updateHealthStatus("unreachable");
|
|
1448
1626
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1627
|
+
setConnectionState("error");
|
|
1628
|
+
setConnectionError(msg);
|
|
1449
1629
|
h.addStatus(
|
|
1450
1630
|
`${statusEmoji("unreachable")} Failed to connect: ${msg}`,
|
|
1451
1631
|
"red",
|
|
@@ -1967,6 +2147,7 @@ function ChatApp({
|
|
|
1967
2147
|
role: "assistant",
|
|
1968
2148
|
content: msg.content,
|
|
1969
2149
|
});
|
|
2150
|
+
process.stdout.write("\x07");
|
|
1970
2151
|
h.setBusy(false);
|
|
1971
2152
|
h.hideSpinner();
|
|
1972
2153
|
return;
|
|
@@ -2005,12 +2186,44 @@ function ChatApp({
|
|
|
2005
2186
|
|
|
2006
2187
|
const handleSubmit = useCallback(
|
|
2007
2188
|
(value: string) => {
|
|
2189
|
+
const trimmed = value.trim();
|
|
2190
|
+
if (trimmed) {
|
|
2191
|
+
appendHistory(trimmed);
|
|
2192
|
+
historyRef.current = loadHistory();
|
|
2193
|
+
}
|
|
2194
|
+
historyIndexRef.current = -1;
|
|
2195
|
+
savedInputRef.current = "";
|
|
2008
2196
|
setInputValue("");
|
|
2009
2197
|
handleInput(value);
|
|
2010
2198
|
},
|
|
2011
2199
|
[handleInput],
|
|
2012
2200
|
);
|
|
2013
2201
|
|
|
2202
|
+
const handleHistoryUp = useCallback(() => {
|
|
2203
|
+
const history = historyRef.current;
|
|
2204
|
+
if (history.length === 0) return;
|
|
2205
|
+
if (historyIndexRef.current === -1) {
|
|
2206
|
+
savedInputRef.current = inputValue;
|
|
2207
|
+
}
|
|
2208
|
+
const nextIndex = Math.min(historyIndexRef.current + 1, history.length - 1);
|
|
2209
|
+
historyIndexRef.current = nextIndex;
|
|
2210
|
+
const entry = history[history.length - 1 - nextIndex];
|
|
2211
|
+
setInputValue(entry);
|
|
2212
|
+
}, [inputValue]);
|
|
2213
|
+
|
|
2214
|
+
const handleHistoryDown = useCallback(() => {
|
|
2215
|
+
if (historyIndexRef.current === -1) return;
|
|
2216
|
+
if (historyIndexRef.current <= 0) {
|
|
2217
|
+
historyIndexRef.current = -1;
|
|
2218
|
+
setInputValue(savedInputRef.current);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
historyIndexRef.current -= 1;
|
|
2222
|
+
const history = historyRef.current;
|
|
2223
|
+
const entry = history[history.length - 1 - historyIndexRef.current];
|
|
2224
|
+
setInputValue(entry);
|
|
2225
|
+
}, []);
|
|
2226
|
+
|
|
2014
2227
|
useEffect(() => {
|
|
2015
2228
|
const handle: ChatAppHandle = {
|
|
2016
2229
|
addMessage,
|
|
@@ -2044,6 +2257,13 @@ function ChatApp({
|
|
|
2044
2257
|
updateHealthStatus,
|
|
2045
2258
|
]);
|
|
2046
2259
|
|
|
2260
|
+
const retryConnection = useCallback(() => {
|
|
2261
|
+
if (connectingRef.current) return; // already retrying
|
|
2262
|
+
connectedRef.current = false;
|
|
2263
|
+
setConnectionState("connecting");
|
|
2264
|
+
ensureConnected();
|
|
2265
|
+
}, [ensureConnected]);
|
|
2266
|
+
|
|
2047
2267
|
useEffect(() => {
|
|
2048
2268
|
ensureConnected();
|
|
2049
2269
|
}, [ensureConnected]);
|
|
@@ -2132,6 +2352,20 @@ function ChatApp({
|
|
|
2132
2352
|
}
|
|
2133
2353
|
}, [selection]);
|
|
2134
2354
|
|
|
2355
|
+
if (connectionState !== "connected") {
|
|
2356
|
+
return (
|
|
2357
|
+
<ConnectionScreen
|
|
2358
|
+
state={connectionState}
|
|
2359
|
+
errorMessage={connectionError}
|
|
2360
|
+
species={species}
|
|
2361
|
+
terminalRows={terminalRows}
|
|
2362
|
+
terminalColumns={terminalColumns}
|
|
2363
|
+
onRetry={retryConnection}
|
|
2364
|
+
onExit={onExit}
|
|
2365
|
+
/>
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2135
2369
|
return (
|
|
2136
2370
|
<Box flexDirection="column" height={terminalRows}>
|
|
2137
2371
|
<DefaultMainScreen
|
|
@@ -2144,8 +2378,9 @@ function ChatApp({
|
|
|
2144
2378
|
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
2145
2379
|
{visibleWindow.hiddenAbove > 0 ? (
|
|
2146
2380
|
<Text dimColor>
|
|
2147
|
-
{
|
|
2148
|
-
|
|
2381
|
+
{isCompact
|
|
2382
|
+
? `\u2191 ${visibleWindow.hiddenAbove} more above`
|
|
2383
|
+
: `\u2191 ${visibleWindow.hiddenAbove} more above (Shift+\u2191/Cmd+\u2191)`}
|
|
2149
2384
|
</Text>
|
|
2150
2385
|
) : null}
|
|
2151
2386
|
|
|
@@ -2210,22 +2445,35 @@ function ChatApp({
|
|
|
2210
2445
|
) : null}
|
|
2211
2446
|
|
|
2212
2447
|
{!selection && !secretInput ? (
|
|
2213
|
-
<Box flexDirection="column">
|
|
2214
|
-
|
|
2215
|
-
|
|
2448
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
2449
|
+
{isCompact ? null : (
|
|
2450
|
+
<Text dimColor>
|
|
2451
|
+
{unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
|
|
2452
|
+
</Text>
|
|
2453
|
+
)}
|
|
2454
|
+
<Box paddingLeft={isCompact ? 0 : 1} height={1} flexShrink={0}>
|
|
2216
2455
|
<Text color="green" bold>
|
|
2217
|
-
|
|
2456
|
+
{isCompact ? ">" : "you>"}
|
|
2218
2457
|
{" "}
|
|
2219
2458
|
</Text>
|
|
2220
2459
|
<TextInput
|
|
2221
2460
|
value={inputValue}
|
|
2222
2461
|
onChange={setInputValue}
|
|
2223
2462
|
onSubmit={handleSubmit}
|
|
2463
|
+
onHistoryUp={handleHistoryUp}
|
|
2464
|
+
onHistoryDown={handleHistoryDown}
|
|
2465
|
+
completionCommands={SLASH_COMMANDS}
|
|
2224
2466
|
focus={inputFocused}
|
|
2225
2467
|
/>
|
|
2226
2468
|
</Box>
|
|
2227
|
-
|
|
2228
|
-
|
|
2469
|
+
{terminalColumns >= COMPACT_THRESHOLD ? (
|
|
2470
|
+
<>
|
|
2471
|
+
<Text dimColor>
|
|
2472
|
+
{unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
|
|
2473
|
+
</Text>
|
|
2474
|
+
<Text dimColor> ? for shortcuts</Text>
|
|
2475
|
+
</>
|
|
2476
|
+
) : null}
|
|
2229
2477
|
</Box>
|
|
2230
2478
|
) : null}
|
|
2231
2479
|
</Box>
|