@vellumai/cli 0.4.46 → 0.4.49
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 +21 -3
- package/src/commands/recover.ts +1 -1
- package/src/commands/retire.ts +41 -2
- 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 +160 -19
- package/src/components/TextInput.tsx +159 -12
- package/src/index.ts +3 -0
- package/src/lib/assistant-config.ts +1 -6
- package/src/lib/constants.ts +7 -1
- package/src/lib/docker.ts +54 -6
- package/src/lib/doctor-client.ts +1 -1
- package/src/lib/gcp.ts +1 -1
- 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 +124 -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
|
|
@@ -694,6 +701,37 @@ interface StyledLine {
|
|
|
694
701
|
style: "heading" | "dim" | "normal";
|
|
695
702
|
}
|
|
696
703
|
|
|
704
|
+
function CompactHeader({
|
|
705
|
+
species,
|
|
706
|
+
healthStatus,
|
|
707
|
+
totalWidth,
|
|
708
|
+
}: {
|
|
709
|
+
species: Species;
|
|
710
|
+
healthStatus?: string;
|
|
711
|
+
totalWidth: number;
|
|
712
|
+
}): ReactElement {
|
|
713
|
+
const config = SPECIES_CONFIG[species];
|
|
714
|
+
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
715
|
+
const status = healthStatus ?? "checking...";
|
|
716
|
+
const label = ` ${config.hatchedEmoji} ${species} ${statusEmoji(status)} `;
|
|
717
|
+
const prefix = "── Vellum";
|
|
718
|
+
const suffix = "──";
|
|
719
|
+
const fillLen = Math.max(
|
|
720
|
+
0,
|
|
721
|
+
totalWidth - prefix.length - label.length - suffix.length,
|
|
722
|
+
);
|
|
723
|
+
return (
|
|
724
|
+
<Box flexDirection="column" width={totalWidth}>
|
|
725
|
+
<Text dimColor>
|
|
726
|
+
{prefix}
|
|
727
|
+
<Text color={accentColor}>{label}</Text>
|
|
728
|
+
{"─".repeat(fillLen)}
|
|
729
|
+
{suffix}
|
|
730
|
+
</Text>
|
|
731
|
+
</Box>
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
697
735
|
function DefaultMainScreen({
|
|
698
736
|
runtimeUrl,
|
|
699
737
|
assistantId,
|
|
@@ -705,10 +743,27 @@ function DefaultMainScreen({
|
|
|
705
743
|
const config = SPECIES_CONFIG[species];
|
|
706
744
|
const art = config.art;
|
|
707
745
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
746
|
+
const caps = getTerminalCapabilities();
|
|
747
|
+
const headerPrefix = caps.unicodeSupported
|
|
748
|
+
? HEADER_PREFIX_UNICODE
|
|
749
|
+
: HEADER_PREFIX_ASCII;
|
|
750
|
+
const headerSep = caps.unicodeSupported ? "─" : "-";
|
|
708
751
|
|
|
709
752
|
const { stdout } = useStdout();
|
|
710
753
|
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
711
754
|
const totalWidth = Math.min(MAX_TOTAL_WIDTH, terminalColumns);
|
|
755
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
756
|
+
|
|
757
|
+
if (isCompact) {
|
|
758
|
+
return (
|
|
759
|
+
<CompactHeader
|
|
760
|
+
species={species}
|
|
761
|
+
healthStatus={healthStatus}
|
|
762
|
+
totalWidth={totalWidth}
|
|
763
|
+
/>
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
712
767
|
const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
|
|
713
768
|
|
|
714
769
|
const leftLines = [
|
|
@@ -729,7 +784,10 @@ function DefaultMainScreen({
|
|
|
729
784
|
{ text: "Assistant", style: "heading" },
|
|
730
785
|
{ text: assistantId, style: "dim" },
|
|
731
786
|
{ text: "Species", style: "heading" },
|
|
732
|
-
{
|
|
787
|
+
{
|
|
788
|
+
text: `${unicodeOrFallback(config.hatchedEmoji, `[${species}]`)} ${species}`,
|
|
789
|
+
style: "dim",
|
|
790
|
+
},
|
|
733
791
|
{ text: "Status", style: "heading" },
|
|
734
792
|
{ text: withStatusEmoji(healthStatus ?? "checking..."), style: "dim" },
|
|
735
793
|
];
|
|
@@ -739,8 +797,8 @@ function DefaultMainScreen({
|
|
|
739
797
|
return (
|
|
740
798
|
<Box flexDirection="column" width={totalWidth}>
|
|
741
799
|
<Text dimColor>
|
|
742
|
-
{
|
|
743
|
-
|
|
800
|
+
{headerPrefix +
|
|
801
|
+
headerSep.repeat(Math.max(0, totalWidth - headerPrefix.length))}
|
|
744
802
|
</Text>
|
|
745
803
|
<Box flexDirection="row">
|
|
746
804
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
@@ -755,7 +813,7 @@ function DefaultMainScreen({
|
|
|
755
813
|
}
|
|
756
814
|
if (i > 2 && i <= 2 + art.length) {
|
|
757
815
|
return (
|
|
758
|
-
<Text key={i} color={accentColor}>
|
|
816
|
+
<Text key={i} color={caps.isDumb ? undefined : accentColor}>
|
|
759
817
|
{line}
|
|
760
818
|
</Text>
|
|
761
819
|
);
|
|
@@ -792,7 +850,7 @@ function DefaultMainScreen({
|
|
|
792
850
|
})}
|
|
793
851
|
</Box>
|
|
794
852
|
</Box>
|
|
795
|
-
<Text dimColor>{
|
|
853
|
+
<Text dimColor>{headerSep.repeat(totalWidth)}</Text>
|
|
796
854
|
<Text> </Text>
|
|
797
855
|
</Box>
|
|
798
856
|
);
|
|
@@ -838,9 +896,18 @@ function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
|
|
|
838
896
|
function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
839
897
|
if (isRuntimeMessage(item)) {
|
|
840
898
|
const cols = Math.max(1, terminalColumns);
|
|
899
|
+
// Account for "HH:MM AM Label: " prefix on the first line
|
|
900
|
+
const defaultLabel = item.role === "user" ? "You:" : "Assistant:";
|
|
901
|
+
const label = item.label ?? defaultLabel;
|
|
902
|
+
const prefixLen = 10 + label.length + 1; // timestamp + space + label + space
|
|
841
903
|
let lines = 0;
|
|
842
|
-
|
|
843
|
-
|
|
904
|
+
const contentLines = item.content.split("\n");
|
|
905
|
+
for (let idx = 0; idx < contentLines.length; idx++) {
|
|
906
|
+
const lineLen =
|
|
907
|
+
idx === 0
|
|
908
|
+
? contentLines[idx].length + prefixLen
|
|
909
|
+
: contentLines[idx].length;
|
|
910
|
+
lines += Math.max(1, Math.ceil(lineLen / cols));
|
|
844
911
|
}
|
|
845
912
|
if (item.role === "assistant" && item.toolCalls) {
|
|
846
913
|
for (const tc of item.toolCalls) {
|
|
@@ -870,7 +937,15 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
|
870
937
|
return 1;
|
|
871
938
|
}
|
|
872
939
|
|
|
873
|
-
|
|
940
|
+
const COMPACT_HEADER_HEIGHT = 1;
|
|
941
|
+
|
|
942
|
+
function calculateHeaderHeight(
|
|
943
|
+
species: Species,
|
|
944
|
+
terminalColumns?: number,
|
|
945
|
+
): number {
|
|
946
|
+
if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
|
|
947
|
+
return COMPACT_HEADER_HEIGHT;
|
|
948
|
+
}
|
|
874
949
|
const config = SPECIES_CONFIG[species];
|
|
875
950
|
const artLength = config.art.length;
|
|
876
951
|
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
@@ -885,6 +960,9 @@ export function render(
|
|
|
885
960
|
assistantId: string,
|
|
886
961
|
species: Species,
|
|
887
962
|
): number {
|
|
963
|
+
const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
964
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
965
|
+
|
|
888
966
|
const config = SPECIES_CONFIG[species];
|
|
889
967
|
const art = config.art;
|
|
890
968
|
|
|
@@ -901,6 +979,10 @@ export function render(
|
|
|
901
979
|
);
|
|
902
980
|
unmount();
|
|
903
981
|
|
|
982
|
+
if (isCompact) {
|
|
983
|
+
return COMPACT_HEADER_HEIGHT;
|
|
984
|
+
}
|
|
985
|
+
|
|
904
986
|
const statusCanvasLine = RIGHT_PANEL_LINE_COUNT + HEADER_TOP_BORDER_LINES;
|
|
905
987
|
const statusCol = LEFT_PANEL_WIDTH + 1;
|
|
906
988
|
checkHealth(runtimeUrl)
|
|
@@ -1128,6 +1210,9 @@ function ChatApp({
|
|
|
1128
1210
|
handleRef,
|
|
1129
1211
|
}: ChatAppProps): ReactElement {
|
|
1130
1212
|
const [inputValue, setInputValue] = useState("");
|
|
1213
|
+
const historyRef = useRef<string[]>(loadHistory());
|
|
1214
|
+
const historyIndexRef = useRef(-1);
|
|
1215
|
+
const savedInputRef = useRef("");
|
|
1131
1216
|
const [feed, setFeed] = useState<FeedItem[]>([]);
|
|
1132
1217
|
const [spinnerText, setSpinnerText] = useState<string | null>(null);
|
|
1133
1218
|
const [selection, setSelection] = useState<SelectionRequest | null>(null);
|
|
@@ -1152,15 +1237,20 @@ function ChatApp({
|
|
|
1152
1237
|
const { stdout } = useStdout();
|
|
1153
1238
|
const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
|
|
1154
1239
|
const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1155
|
-
const headerHeight = calculateHeaderHeight(species);
|
|
1240
|
+
const headerHeight = calculateHeaderHeight(species, terminalColumns);
|
|
1156
1241
|
|
|
1242
|
+
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1243
|
+
const compactInputAreaHeight = 2; // separator + input row only
|
|
1244
|
+
const inputAreaHeight = isCompact
|
|
1245
|
+
? compactInputAreaHeight
|
|
1246
|
+
: INPUT_AREA_HEIGHT;
|
|
1157
1247
|
const bottomHeight = selection
|
|
1158
1248
|
? selection.options.length + SELECTION_CHROME_LINES + TOOLTIP_HEIGHT
|
|
1159
1249
|
: secretInput
|
|
1160
1250
|
? SECRET_INPUT_HEIGHT + TOOLTIP_HEIGHT
|
|
1161
1251
|
: spinnerText
|
|
1162
|
-
? SPINNER_HEIGHT +
|
|
1163
|
-
:
|
|
1252
|
+
? SPINNER_HEIGHT + inputAreaHeight
|
|
1253
|
+
: inputAreaHeight;
|
|
1164
1254
|
const availableRows = Math.max(
|
|
1165
1255
|
MIN_FEED_ROWS,
|
|
1166
1256
|
terminalRows - headerHeight - bottomHeight,
|
|
@@ -1197,11 +1287,14 @@ function ChatApp({
|
|
|
1197
1287
|
}
|
|
1198
1288
|
|
|
1199
1289
|
if (scrollIndex === null) {
|
|
1290
|
+
// Reserve 1 line for "N more above" indicator when there are hidden messages
|
|
1200
1291
|
let totalHeight = 0;
|
|
1201
1292
|
let start = feed.length;
|
|
1202
1293
|
for (let i = feed.length - 1; i >= 0; i--) {
|
|
1203
1294
|
const h = estimateItemHeight(feed[i], terminalColumns);
|
|
1204
|
-
|
|
1295
|
+
// Reserve space for the "more above" indicator if we'd hide messages
|
|
1296
|
+
const indicatorLine = i > 0 ? 1 : 0;
|
|
1297
|
+
if (totalHeight + h + indicatorLine > availableRows) {
|
|
1205
1298
|
break;
|
|
1206
1299
|
}
|
|
1207
1300
|
totalHeight += h;
|
|
@@ -1220,11 +1313,16 @@ function ChatApp({
|
|
|
1220
1313
|
}
|
|
1221
1314
|
|
|
1222
1315
|
const start = Math.max(0, Math.min(scrollIndex, feed.length - 1));
|
|
1316
|
+
// Reserve lines for "more above/below" indicators
|
|
1317
|
+
const aboveIndicator = start > 0 ? 1 : 0;
|
|
1318
|
+
const budget = availableRows - aboveIndicator;
|
|
1223
1319
|
let totalHeight = 0;
|
|
1224
1320
|
let end = start;
|
|
1225
1321
|
for (let i = start; i < feed.length; i++) {
|
|
1226
1322
|
const h = estimateItemHeight(feed[i], terminalColumns);
|
|
1227
|
-
|
|
1323
|
+
// Reserve space for "more below" indicator if we'd hide messages
|
|
1324
|
+
const belowIndicator = i + 1 < feed.length ? 1 : 0;
|
|
1325
|
+
if (totalHeight + h + belowIndicator > budget) {
|
|
1228
1326
|
break;
|
|
1229
1327
|
}
|
|
1230
1328
|
totalHeight += h;
|
|
@@ -2005,12 +2103,44 @@ function ChatApp({
|
|
|
2005
2103
|
|
|
2006
2104
|
const handleSubmit = useCallback(
|
|
2007
2105
|
(value: string) => {
|
|
2106
|
+
const trimmed = value.trim();
|
|
2107
|
+
if (trimmed) {
|
|
2108
|
+
appendHistory(trimmed);
|
|
2109
|
+
historyRef.current = loadHistory();
|
|
2110
|
+
}
|
|
2111
|
+
historyIndexRef.current = -1;
|
|
2112
|
+
savedInputRef.current = "";
|
|
2008
2113
|
setInputValue("");
|
|
2009
2114
|
handleInput(value);
|
|
2010
2115
|
},
|
|
2011
2116
|
[handleInput],
|
|
2012
2117
|
);
|
|
2013
2118
|
|
|
2119
|
+
const handleHistoryUp = useCallback(() => {
|
|
2120
|
+
const history = historyRef.current;
|
|
2121
|
+
if (history.length === 0) return;
|
|
2122
|
+
if (historyIndexRef.current === -1) {
|
|
2123
|
+
savedInputRef.current = inputValue;
|
|
2124
|
+
}
|
|
2125
|
+
const nextIndex = Math.min(historyIndexRef.current + 1, history.length - 1);
|
|
2126
|
+
historyIndexRef.current = nextIndex;
|
|
2127
|
+
const entry = history[history.length - 1 - nextIndex];
|
|
2128
|
+
setInputValue(entry);
|
|
2129
|
+
}, [inputValue]);
|
|
2130
|
+
|
|
2131
|
+
const handleHistoryDown = useCallback(() => {
|
|
2132
|
+
if (historyIndexRef.current === -1) return;
|
|
2133
|
+
if (historyIndexRef.current <= 0) {
|
|
2134
|
+
historyIndexRef.current = -1;
|
|
2135
|
+
setInputValue(savedInputRef.current);
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
historyIndexRef.current -= 1;
|
|
2139
|
+
const history = historyRef.current;
|
|
2140
|
+
const entry = history[history.length - 1 - historyIndexRef.current];
|
|
2141
|
+
setInputValue(entry);
|
|
2142
|
+
}, []);
|
|
2143
|
+
|
|
2014
2144
|
useEffect(() => {
|
|
2015
2145
|
const handle: ChatAppHandle = {
|
|
2016
2146
|
addMessage,
|
|
@@ -2210,9 +2340,11 @@ function ChatApp({
|
|
|
2210
2340
|
) : null}
|
|
2211
2341
|
|
|
2212
2342
|
{!selection && !secretInput ? (
|
|
2213
|
-
<Box flexDirection="column">
|
|
2214
|
-
<Text dimColor>
|
|
2215
|
-
|
|
2343
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
2344
|
+
<Text dimColor>
|
|
2345
|
+
{unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
|
|
2346
|
+
</Text>
|
|
2347
|
+
<Box paddingLeft={1} height={1} flexShrink={0}>
|
|
2216
2348
|
<Text color="green" bold>
|
|
2217
2349
|
you{">"}
|
|
2218
2350
|
{" "}
|
|
@@ -2221,11 +2353,20 @@ function ChatApp({
|
|
|
2221
2353
|
value={inputValue}
|
|
2222
2354
|
onChange={setInputValue}
|
|
2223
2355
|
onSubmit={handleSubmit}
|
|
2356
|
+
onHistoryUp={handleHistoryUp}
|
|
2357
|
+
onHistoryDown={handleHistoryDown}
|
|
2358
|
+
completionCommands={SLASH_COMMANDS}
|
|
2224
2359
|
focus={inputFocused}
|
|
2225
2360
|
/>
|
|
2226
2361
|
</Box>
|
|
2227
|
-
|
|
2228
|
-
|
|
2362
|
+
{terminalColumns >= COMPACT_THRESHOLD ? (
|
|
2363
|
+
<>
|
|
2364
|
+
<Text dimColor>
|
|
2365
|
+
{unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
|
|
2366
|
+
</Text>
|
|
2367
|
+
<Text dimColor> ? for shortcuts</Text>
|
|
2368
|
+
</>
|
|
2369
|
+
) : null}
|
|
2229
2370
|
</Box>
|
|
2230
2371
|
) : null}
|
|
2231
2372
|
</Box>
|
|
@@ -6,6 +6,9 @@ interface TextInputProps {
|
|
|
6
6
|
value: string;
|
|
7
7
|
onChange: (value: string) => void;
|
|
8
8
|
onSubmit?: (value: string) => void;
|
|
9
|
+
onHistoryUp?: () => void;
|
|
10
|
+
onHistoryDown?: () => void;
|
|
11
|
+
completionCommands?: string[];
|
|
9
12
|
focus?: boolean;
|
|
10
13
|
placeholder?: string;
|
|
11
14
|
}
|
|
@@ -14,12 +17,19 @@ function TextInput({
|
|
|
14
17
|
value,
|
|
15
18
|
onChange,
|
|
16
19
|
onSubmit,
|
|
20
|
+
onHistoryUp,
|
|
21
|
+
onHistoryDown,
|
|
22
|
+
completionCommands,
|
|
17
23
|
focus = true,
|
|
18
24
|
placeholder = "",
|
|
19
25
|
}: TextInputProps): ReactElement {
|
|
20
26
|
const cursorOffsetRef = useRef(value.length);
|
|
21
27
|
const valueRef = useRef(value);
|
|
22
28
|
|
|
29
|
+
// Tab completion state
|
|
30
|
+
const [completionIndex, setCompletionIndex] = useState(-1);
|
|
31
|
+
const [completionMatches, setCompletionMatches] = useState<string[]>([]);
|
|
32
|
+
|
|
23
33
|
valueRef.current = value;
|
|
24
34
|
|
|
25
35
|
if (cursorOffsetRef.current > value.length) {
|
|
@@ -28,40 +38,162 @@ function TextInput({
|
|
|
28
38
|
|
|
29
39
|
const [, setRenderTick] = useState(0);
|
|
30
40
|
|
|
41
|
+
const clearCompletion = () => {
|
|
42
|
+
setCompletionIndex(-1);
|
|
43
|
+
setCompletionMatches([]);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getMatches = (text: string): string[] => {
|
|
47
|
+
if (!completionCommands || !text.startsWith("/") || text.includes(" ")) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const prefix = text.toLowerCase();
|
|
51
|
+
return completionCommands.filter((cmd) =>
|
|
52
|
+
cmd.toLowerCase().startsWith(prefix),
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
31
56
|
useInput(
|
|
32
57
|
(input, key) => {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
if (key.upArrow && !key.shift && !key.meta) {
|
|
59
|
+
clearCompletion();
|
|
60
|
+
onHistoryUp?.();
|
|
61
|
+
cursorOffsetRef.current = Infinity;
|
|
62
|
+
setRenderTick((t) => t + 1);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key.downArrow && !key.shift && !key.meta) {
|
|
66
|
+
clearCompletion();
|
|
67
|
+
onHistoryDown?.();
|
|
68
|
+
cursorOffsetRef.current = Infinity;
|
|
69
|
+
setRenderTick((t) => t + 1);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.ctrl && input === "c") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Tab completion handling
|
|
77
|
+
if (key.tab) {
|
|
78
|
+
const currentValue = valueRef.current;
|
|
79
|
+
|
|
80
|
+
if (completionMatches.length > 0) {
|
|
81
|
+
// Already in completion mode — cycle through matches
|
|
82
|
+
const direction = key.shift ? -1 : 1;
|
|
83
|
+
const nextIndex =
|
|
84
|
+
(completionIndex + direction + completionMatches.length) %
|
|
85
|
+
completionMatches.length;
|
|
86
|
+
setCompletionIndex(nextIndex);
|
|
87
|
+
|
|
88
|
+
const completed = completionMatches[nextIndex]!;
|
|
89
|
+
valueRef.current = completed;
|
|
90
|
+
cursorOffsetRef.current = completed.length;
|
|
91
|
+
onChange(completed);
|
|
92
|
+
setRenderTick((t) => t + 1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Start completion mode
|
|
97
|
+
const matches = getMatches(currentValue);
|
|
98
|
+
if (matches.length === 1) {
|
|
99
|
+
// Single match — accept immediately with trailing space
|
|
100
|
+
const completed = matches[0]! + " ";
|
|
101
|
+
valueRef.current = completed;
|
|
102
|
+
cursorOffsetRef.current = completed.length;
|
|
103
|
+
onChange(completed);
|
|
104
|
+
setRenderTick((t) => t + 1);
|
|
105
|
+
} else if (matches.length > 1) {
|
|
106
|
+
setCompletionMatches(matches);
|
|
107
|
+
const idx = key.shift ? matches.length - 1 : 0;
|
|
108
|
+
setCompletionIndex(idx);
|
|
109
|
+
|
|
110
|
+
const completed = matches[idx]!;
|
|
111
|
+
valueRef.current = completed;
|
|
112
|
+
cursorOffsetRef.current = completed.length;
|
|
113
|
+
onChange(completed);
|
|
114
|
+
setRenderTick((t) => t + 1);
|
|
115
|
+
}
|
|
40
116
|
return;
|
|
41
117
|
}
|
|
42
118
|
|
|
119
|
+
// Escape cancels completion mode
|
|
120
|
+
if (key.escape) {
|
|
121
|
+
if (completionMatches.length > 0) {
|
|
122
|
+
clearCompletion();
|
|
123
|
+
setRenderTick((t) => t + 1);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Enter accepts completion and submits
|
|
43
129
|
if (key.return) {
|
|
44
|
-
|
|
130
|
+
if (completionMatches.length > 0) {
|
|
131
|
+
// Append trailing space so the command is recognized by handleInput
|
|
132
|
+
const completed = valueRef.current + " ";
|
|
133
|
+
valueRef.current = completed;
|
|
134
|
+
cursorOffsetRef.current = completed.length;
|
|
135
|
+
onChange(completed);
|
|
136
|
+
clearCompletion();
|
|
137
|
+
onSubmit?.(completed);
|
|
138
|
+
} else {
|
|
139
|
+
clearCompletion();
|
|
140
|
+
onSubmit?.(valueRef.current);
|
|
141
|
+
}
|
|
45
142
|
return;
|
|
46
143
|
}
|
|
47
144
|
|
|
145
|
+
// Space accepts completion, then continues editing
|
|
146
|
+
if (input === " " && completionMatches.length > 0) {
|
|
147
|
+
clearCompletion();
|
|
148
|
+
// Let the space be inserted normally below
|
|
149
|
+
} else if (completionMatches.length > 0) {
|
|
150
|
+
// Any other key exits completion mode
|
|
151
|
+
clearCompletion();
|
|
152
|
+
}
|
|
153
|
+
|
|
48
154
|
const currentValue = valueRef.current;
|
|
49
155
|
const currentOffset = cursorOffsetRef.current;
|
|
50
156
|
let nextValue = currentValue;
|
|
51
157
|
let nextOffset = currentOffset;
|
|
52
158
|
|
|
53
|
-
if (key.
|
|
159
|
+
if (key.ctrl && input === "a") {
|
|
160
|
+
// Ctrl+A — move cursor to start
|
|
161
|
+
nextOffset = 0;
|
|
162
|
+
} else if (key.ctrl && input === "e") {
|
|
163
|
+
// Ctrl+E — move cursor to end
|
|
164
|
+
nextOffset = currentValue.length;
|
|
165
|
+
} else if (key.ctrl && input === "u") {
|
|
166
|
+
// Ctrl+U — clear line before cursor
|
|
167
|
+
nextValue = currentValue.slice(currentOffset);
|
|
168
|
+
nextOffset = 0;
|
|
169
|
+
} else if (key.ctrl && input === "k") {
|
|
170
|
+
// Ctrl+K — kill from cursor to end
|
|
171
|
+
nextValue = currentValue.slice(0, currentOffset);
|
|
172
|
+
} else if (key.ctrl && input === "w") {
|
|
173
|
+
// Ctrl+W — delete word backwards (handles tabs and other whitespace)
|
|
174
|
+
const before = currentValue.slice(0, currentOffset);
|
|
175
|
+
// Skip trailing whitespace, then find previous whitespace boundary
|
|
176
|
+
const match = before.match(/^(.*\s)?\S+\s*$/);
|
|
177
|
+
const wordStart = match?.[1]?.length ?? 0;
|
|
178
|
+
nextValue =
|
|
179
|
+
currentValue.slice(0, wordStart) + currentValue.slice(currentOffset);
|
|
180
|
+
nextOffset = wordStart;
|
|
181
|
+
} else if (key.leftArrow) {
|
|
54
182
|
nextOffset = Math.max(0, currentOffset - 1);
|
|
55
183
|
} else if (key.rightArrow) {
|
|
56
184
|
nextOffset = Math.min(currentValue.length, currentOffset + 1);
|
|
57
185
|
} else if (key.backspace || key.delete) {
|
|
58
186
|
if (currentOffset > 0) {
|
|
59
|
-
nextValue =
|
|
187
|
+
nextValue =
|
|
188
|
+
currentValue.slice(0, currentOffset - 1) +
|
|
189
|
+
currentValue.slice(currentOffset);
|
|
60
190
|
nextOffset = currentOffset - 1;
|
|
61
191
|
}
|
|
62
192
|
} else {
|
|
63
193
|
nextValue =
|
|
64
|
-
currentValue.slice(0, currentOffset) +
|
|
194
|
+
currentValue.slice(0, currentOffset) +
|
|
195
|
+
input +
|
|
196
|
+
currentValue.slice(currentOffset);
|
|
65
197
|
nextOffset = currentOffset + input.length;
|
|
66
198
|
}
|
|
67
199
|
|
|
@@ -78,6 +210,14 @@ function TextInput({
|
|
|
78
210
|
);
|
|
79
211
|
|
|
80
212
|
const cursorOffset = cursorOffsetRef.current;
|
|
213
|
+
const isCompleting = completionMatches.length > 0;
|
|
214
|
+
|
|
215
|
+
// Build completion hint text
|
|
216
|
+
let completionHint = "";
|
|
217
|
+
if (isCompleting && completionMatches.length > 1) {
|
|
218
|
+
completionHint = ` [${completionIndex + 1}/${completionMatches.length}]`;
|
|
219
|
+
}
|
|
220
|
+
|
|
81
221
|
let renderedValue: string;
|
|
82
222
|
let renderedPlaceholder: string | undefined;
|
|
83
223
|
|
|
@@ -97,6 +237,9 @@ function TextInput({
|
|
|
97
237
|
if (cursorOffset === value.length) {
|
|
98
238
|
renderedValue += chalk.inverse(" ");
|
|
99
239
|
}
|
|
240
|
+
if (completionHint) {
|
|
241
|
+
renderedValue += chalk.grey(completionHint);
|
|
242
|
+
}
|
|
100
243
|
} else {
|
|
101
244
|
renderedValue = chalk.inverse(" ");
|
|
102
245
|
}
|
|
@@ -107,7 +250,11 @@ function TextInput({
|
|
|
107
250
|
|
|
108
251
|
return (
|
|
109
252
|
<Text>
|
|
110
|
-
{placeholder
|
|
253
|
+
{placeholder
|
|
254
|
+
? value.length > 0
|
|
255
|
+
? renderedValue
|
|
256
|
+
: renderedPlaceholder
|
|
257
|
+
: renderedValue}
|
|
111
258
|
</Text>
|
|
112
259
|
);
|
|
113
260
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { pair } from "./commands/pair";
|
|
|
9
9
|
import { ps } from "./commands/ps";
|
|
10
10
|
import { recover } from "./commands/recover";
|
|
11
11
|
import { retire } from "./commands/retire";
|
|
12
|
+
import { setup } from "./commands/setup";
|
|
12
13
|
import { sleep } from "./commands/sleep";
|
|
13
14
|
import { ssh } from "./commands/ssh";
|
|
14
15
|
import { tunnel } from "./commands/tunnel";
|
|
@@ -25,6 +26,7 @@ const commands = {
|
|
|
25
26
|
ps,
|
|
26
27
|
recover,
|
|
27
28
|
retire,
|
|
29
|
+
setup,
|
|
28
30
|
sleep,
|
|
29
31
|
ssh,
|
|
30
32
|
tunnel,
|
|
@@ -59,6 +61,7 @@ async function main() {
|
|
|
59
61
|
);
|
|
60
62
|
console.log(" recover Restore a previously retired local assistant");
|
|
61
63
|
console.log(" retire Delete an assistant instance");
|
|
64
|
+
console.log(" setup Configure API keys interactively");
|
|
62
65
|
console.log(" sleep Stop the assistant process");
|
|
63
66
|
console.log(" ssh SSH into a remote assistant instance");
|
|
64
67
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
@@ -190,11 +190,7 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
190
190
|
mutated = true;
|
|
191
191
|
}
|
|
192
192
|
if (typeof res.pidFile !== "string") {
|
|
193
|
-
res.pidFile = join(
|
|
194
|
-
res.instanceDir as string,
|
|
195
|
-
".vellum",
|
|
196
|
-
"vellum.pid",
|
|
197
|
-
);
|
|
193
|
+
res.pidFile = join(res.instanceDir as string, ".vellum", "vellum.pid");
|
|
198
194
|
mutated = true;
|
|
199
195
|
}
|
|
200
196
|
}
|
|
@@ -363,7 +359,6 @@ export async function allocateLocalResources(
|
|
|
363
359
|
if (existingLocals.length === 0) {
|
|
364
360
|
const home = homedir();
|
|
365
361
|
const vellumDir = join(home, ".vellum");
|
|
366
|
-
mkdirSync(vellumDir, { recursive: true });
|
|
367
362
|
return {
|
|
368
363
|
instanceDir: home,
|
|
369
364
|
daemonPort: DEFAULT_DAEMON_PORT,
|
package/src/lib/constants.ts
CHANGED
|
@@ -8,7 +8,13 @@ export const DEFAULT_DAEMON_PORT = 7821;
|
|
|
8
8
|
export const DEFAULT_GATEWAY_PORT = 7830;
|
|
9
9
|
export const DEFAULT_QDRANT_PORT = 6333;
|
|
10
10
|
|
|
11
|
-
export const VALID_REMOTE_HOSTS = [
|
|
11
|
+
export const VALID_REMOTE_HOSTS = [
|
|
12
|
+
"local",
|
|
13
|
+
"gcp",
|
|
14
|
+
"aws",
|
|
15
|
+
"docker",
|
|
16
|
+
"custom",
|
|
17
|
+
] as const;
|
|
12
18
|
export type RemoteHost = (typeof VALID_REMOTE_HOSTS)[number];
|
|
13
19
|
export const VALID_SPECIES = ["openclaw", "vellum"] as const;
|
|
14
20
|
export type Species = (typeof VALID_SPECIES)[number];
|