@vellumai/cli 0.4.25 → 0.4.29
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 +24 -24
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +17 -5
- package/src/__tests__/retire-archive.test.ts +6 -2
- package/src/adapters/openclaw-http-server.ts +22 -7
- package/src/commands/autonomy.ts +10 -11
- package/src/commands/client.ts +25 -9
- package/src/commands/config.ts +2 -6
- package/src/commands/contacts.ts +1 -4
- package/src/commands/hatch.ts +131 -36
- package/src/commands/login.ts +6 -2
- package/src/commands/pair.ts +26 -9
- package/src/commands/ps.ts +55 -23
- package/src/commands/recover.ts +4 -2
- package/src/commands/retire.ts +42 -14
- package/src/commands/sleep.ts +15 -3
- package/src/commands/ssh.ts +20 -13
- package/src/commands/tunnel.ts +6 -7
- package/src/commands/wake.ts +13 -4
- package/src/components/DefaultMainScreen.tsx +309 -99
- package/src/index.ts +2 -2
- package/src/lib/assistant-config.ts +9 -3
- package/src/lib/aws.ts +36 -11
- package/src/lib/constants.ts +3 -1
- package/src/lib/doctor-client.ts +23 -7
- package/src/lib/gcp.ts +74 -24
- package/src/lib/health-check.ts +14 -4
- package/src/lib/local.ts +249 -33
- package/src/lib/ngrok.ts +1 -3
- package/src/lib/openclaw-runtime-server.ts +7 -2
- package/src/lib/platform-client.ts +16 -3
- package/src/lib/xdg-log.ts +25 -5
|
@@ -3,7 +3,14 @@ import { createHash, randomBytes, randomUUID } from "crypto";
|
|
|
3
3
|
import { hostname, platform, userInfo } from "os";
|
|
4
4
|
import { basename } from "path";
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
} from "react";
|
|
7
14
|
import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
|
|
8
15
|
|
|
9
16
|
import { removeAssistantEntry } from "../lib/assistant-config";
|
|
@@ -26,7 +33,16 @@ export const ANSI = {
|
|
|
26
33
|
gray: "\x1b[90m",
|
|
27
34
|
} as const;
|
|
28
35
|
|
|
29
|
-
export const SLASH_COMMANDS = [
|
|
36
|
+
export const SLASH_COMMANDS = [
|
|
37
|
+
"/clear",
|
|
38
|
+
"/doctor",
|
|
39
|
+
"/exit",
|
|
40
|
+
"/help",
|
|
41
|
+
"/pair",
|
|
42
|
+
"/q",
|
|
43
|
+
"/quit",
|
|
44
|
+
"/retire",
|
|
45
|
+
];
|
|
30
46
|
|
|
31
47
|
const POLL_INTERVAL_MS = 3000;
|
|
32
48
|
const SEND_TIMEOUT_MS = 5000;
|
|
@@ -45,17 +61,24 @@ const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
|
|
|
45
61
|
const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
|
|
46
62
|
|
|
47
63
|
// Right panel structure
|
|
48
|
-
const TIPS = [
|
|
64
|
+
const TIPS = [
|
|
65
|
+
"Send a message to start chatting",
|
|
66
|
+
"Use /help to see available commands",
|
|
67
|
+
];
|
|
49
68
|
const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant, Species, Status — each with heading + value
|
|
50
69
|
const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
|
|
51
70
|
const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
52
71
|
const RIGHT_PANEL_LINE_COUNT =
|
|
53
|
-
RIGHT_PANEL_SPACERS +
|
|
72
|
+
RIGHT_PANEL_SPACERS +
|
|
73
|
+
RIGHT_PANEL_TIPS_HEADING +
|
|
74
|
+
TIPS.length +
|
|
75
|
+
RIGHT_PANEL_INFO_SECTIONS * 2;
|
|
54
76
|
|
|
55
77
|
// Header chrome (borders around panel content)
|
|
56
78
|
const HEADER_TOP_BORDER_LINES = 1; // "── Vellum ───..." line
|
|
57
79
|
const HEADER_BOTTOM_BORDER_LINES = 2; // bottom rule + blank line
|
|
58
|
-
const HEADER_CHROME_LINES =
|
|
80
|
+
const HEADER_CHROME_LINES =
|
|
81
|
+
HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
|
|
59
82
|
|
|
60
83
|
// Selection / Secret windows
|
|
61
84
|
const DIALOG_WINDOW_WIDTH = 60;
|
|
@@ -182,7 +205,10 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
|
|
|
182
205
|
return response.json() as Promise<HealthResponse>;
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
async function bootstrapAccessToken(
|
|
208
|
+
async function bootstrapAccessToken(
|
|
209
|
+
baseUrl: string,
|
|
210
|
+
bearerToken?: string,
|
|
211
|
+
): Promise<string> {
|
|
186
212
|
if (!bearerToken) {
|
|
187
213
|
throw new Error("Missing bearer token; cannot bootstrap actor identity");
|
|
188
214
|
}
|
|
@@ -249,7 +275,12 @@ async function sendMessage(
|
|
|
249
275
|
"/messages",
|
|
250
276
|
{
|
|
251
277
|
method: "POST",
|
|
252
|
-
body: JSON.stringify({
|
|
278
|
+
body: JSON.stringify({
|
|
279
|
+
conversationKey: assistantId,
|
|
280
|
+
content,
|
|
281
|
+
sourceChannel: "vellum",
|
|
282
|
+
interface: "cli",
|
|
283
|
+
}),
|
|
253
284
|
signal,
|
|
254
285
|
},
|
|
255
286
|
bearerToken,
|
|
@@ -318,7 +349,10 @@ async function pollPendingInteractions(
|
|
|
318
349
|
);
|
|
319
350
|
}
|
|
320
351
|
|
|
321
|
-
function formatConfirmationPreview(
|
|
352
|
+
function formatConfirmationPreview(
|
|
353
|
+
toolName: string,
|
|
354
|
+
input: Record<string, unknown>,
|
|
355
|
+
): string {
|
|
322
356
|
switch (toolName) {
|
|
323
357
|
case "bash":
|
|
324
358
|
return String(input.command ?? "");
|
|
@@ -333,7 +367,9 @@ function formatConfirmationPreview(toolName: string, input: Record<string, unkno
|
|
|
333
367
|
case "browser_navigate":
|
|
334
368
|
return `navigate ${String(input.url ?? "").slice(0, 80)}`;
|
|
335
369
|
case "browser_close":
|
|
336
|
-
return input.close_all_pages
|
|
370
|
+
return input.close_all_pages
|
|
371
|
+
? "close all browser pages"
|
|
372
|
+
: "close browser page";
|
|
337
373
|
case "browser_click":
|
|
338
374
|
return `click ${input.element_id ?? input.selector ?? ""}`;
|
|
339
375
|
case "browser_type":
|
|
@@ -354,7 +390,10 @@ async function handleConfirmationPrompt(
|
|
|
354
390
|
bearerToken?: string,
|
|
355
391
|
accessToken?: string,
|
|
356
392
|
): Promise<void> {
|
|
357
|
-
const preview = formatConfirmationPreview(
|
|
393
|
+
const preview = formatConfirmationPreview(
|
|
394
|
+
confirmation.toolName,
|
|
395
|
+
confirmation.input,
|
|
396
|
+
);
|
|
358
397
|
const allowlistOptions = confirmation.allowlistOptions ?? [];
|
|
359
398
|
|
|
360
399
|
chatApp.addStatus(`\u250C ${confirmation.toolName}: ${preview}`);
|
|
@@ -365,14 +404,24 @@ async function handleConfirmationPrompt(
|
|
|
365
404
|
chatApp.addStatus("\u2514");
|
|
366
405
|
|
|
367
406
|
const options = ["Allow once", "Deny once"];
|
|
368
|
-
if (
|
|
407
|
+
if (
|
|
408
|
+
allowlistOptions.length > 0 &&
|
|
409
|
+
confirmation.persistentDecisionsAllowed !== false
|
|
410
|
+
) {
|
|
369
411
|
options.push("Allowlist...", "Denylist...");
|
|
370
412
|
}
|
|
371
413
|
|
|
372
414
|
const index = await chatApp.showSelection("Tool Approval", options);
|
|
373
415
|
|
|
374
416
|
if (index === 0) {
|
|
375
|
-
await submitDecision(
|
|
417
|
+
await submitDecision(
|
|
418
|
+
baseUrl,
|
|
419
|
+
assistantId,
|
|
420
|
+
requestId,
|
|
421
|
+
"allow",
|
|
422
|
+
bearerToken,
|
|
423
|
+
accessToken,
|
|
424
|
+
);
|
|
376
425
|
chatApp.addStatus("\u2714 Allowed", "green");
|
|
377
426
|
return;
|
|
378
427
|
}
|
|
@@ -403,7 +452,14 @@ async function handleConfirmationPrompt(
|
|
|
403
452
|
return;
|
|
404
453
|
}
|
|
405
454
|
|
|
406
|
-
await submitDecision(
|
|
455
|
+
await submitDecision(
|
|
456
|
+
baseUrl,
|
|
457
|
+
assistantId,
|
|
458
|
+
requestId,
|
|
459
|
+
"deny",
|
|
460
|
+
bearerToken,
|
|
461
|
+
accessToken,
|
|
462
|
+
);
|
|
407
463
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
408
464
|
}
|
|
409
465
|
|
|
@@ -421,7 +477,10 @@ async function handlePatternSelection(
|
|
|
421
477
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
422
478
|
const options = allowlistOptions.map((o) => o.label);
|
|
423
479
|
|
|
424
|
-
const index = await chatApp.showSelection(
|
|
480
|
+
const index = await chatApp.showSelection(
|
|
481
|
+
`${label}: choose command pattern`,
|
|
482
|
+
options,
|
|
483
|
+
);
|
|
425
484
|
|
|
426
485
|
if (index >= 0 && index < allowlistOptions.length) {
|
|
427
486
|
const selectedPattern = allowlistOptions[index].pattern;
|
|
@@ -439,7 +498,14 @@ async function handlePatternSelection(
|
|
|
439
498
|
return;
|
|
440
499
|
}
|
|
441
500
|
|
|
442
|
-
await submitDecision(
|
|
501
|
+
await submitDecision(
|
|
502
|
+
baseUrl,
|
|
503
|
+
assistantId,
|
|
504
|
+
requestId,
|
|
505
|
+
"deny",
|
|
506
|
+
bearerToken,
|
|
507
|
+
accessToken,
|
|
508
|
+
);
|
|
443
509
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
444
510
|
}
|
|
445
511
|
|
|
@@ -480,7 +546,8 @@ async function handleScopeSelection(
|
|
|
480
546
|
bearerToken,
|
|
481
547
|
accessToken,
|
|
482
548
|
);
|
|
483
|
-
const ruleLabel =
|
|
549
|
+
const ruleLabel =
|
|
550
|
+
trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
|
|
484
551
|
const ruleColor = trustDecision === "always_deny" ? "yellow" : "green";
|
|
485
552
|
chatApp.addStatus(
|
|
486
553
|
`${trustDecision === "always_deny" ? "\u2718" : "\u2714"} ${ruleLabel}`,
|
|
@@ -489,7 +556,14 @@ async function handleScopeSelection(
|
|
|
489
556
|
return;
|
|
490
557
|
}
|
|
491
558
|
|
|
492
|
-
await submitDecision(
|
|
559
|
+
await submitDecision(
|
|
560
|
+
baseUrl,
|
|
561
|
+
assistantId,
|
|
562
|
+
requestId,
|
|
563
|
+
"deny",
|
|
564
|
+
bearerToken,
|
|
565
|
+
accessToken,
|
|
566
|
+
);
|
|
493
567
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
494
568
|
}
|
|
495
569
|
|
|
@@ -582,7 +656,8 @@ function ToolCallDisplay({ tc }: ToolCallDisplayProps): ReactElement {
|
|
|
582
656
|
: null}
|
|
583
657
|
{tc.result !== undefined ? (
|
|
584
658
|
<Text dimColor>
|
|
585
|
-
{"\u2502"} <Text color={statusColor}>{statusIcon}</Text>
|
|
659
|
+
{"\u2502"} <Text color={statusColor}>{statusIcon}</Text>{" "}
|
|
660
|
+
{truncateValue(tc.result, 70)}
|
|
586
661
|
</Text>
|
|
587
662
|
) : null}
|
|
588
663
|
<Text dimColor>{"\u2514"}</Text>
|
|
@@ -667,7 +742,9 @@ function SpinnerDisplay({ text }: { text: string }): ReactElement {
|
|
|
667
742
|
|
|
668
743
|
export function renderErrorMainScreen(error: unknown): number {
|
|
669
744
|
const msg = error instanceof Error ? error.message : String(error);
|
|
670
|
-
console.log(
|
|
745
|
+
console.log(
|
|
746
|
+
`${ANSI.red}${ANSI.bold}Failed to render MainWindow${ANSI.reset}`,
|
|
747
|
+
);
|
|
671
748
|
console.log(`${ANSI.dim}${msg}${ANSI.reset}`);
|
|
672
749
|
console.log(`${ANSI.dim}Run /clear to retry${ANSI.reset}`);
|
|
673
750
|
return 3;
|
|
@@ -733,7 +810,10 @@ function DefaultMainScreen({
|
|
|
733
810
|
|
|
734
811
|
return (
|
|
735
812
|
<Box flexDirection="column" width={totalWidth}>
|
|
736
|
-
<Text dimColor>
|
|
813
|
+
<Text dimColor>
|
|
814
|
+
{HEADER_PREFIX +
|
|
815
|
+
"─".repeat(Math.max(0, totalWidth - HEADER_PREFIX.length))}
|
|
816
|
+
</Text>
|
|
737
817
|
<Box flexDirection="row">
|
|
738
818
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
739
819
|
{Array.from({ length: maxLines }, (_, i) => {
|
|
@@ -790,7 +870,6 @@ function DefaultMainScreen({
|
|
|
790
870
|
);
|
|
791
871
|
}
|
|
792
872
|
|
|
793
|
-
|
|
794
873
|
export interface SelectionRequest {
|
|
795
874
|
title: string;
|
|
796
875
|
options: string[];
|
|
@@ -817,7 +896,12 @@ interface ErrorLine {
|
|
|
817
896
|
text: string;
|
|
818
897
|
}
|
|
819
898
|
|
|
820
|
-
type FeedItem =
|
|
899
|
+
type FeedItem =
|
|
900
|
+
| RuntimeMessage
|
|
901
|
+
| StatusLine
|
|
902
|
+
| SpinnerLine
|
|
903
|
+
| HelpLine
|
|
904
|
+
| ErrorLine;
|
|
821
905
|
|
|
822
906
|
function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
|
|
823
907
|
return "role" in item;
|
|
@@ -833,8 +917,13 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
|
833
917
|
if (item.role === "assistant" && item.toolCalls) {
|
|
834
918
|
for (const tc of item.toolCalls) {
|
|
835
919
|
const paramCount =
|
|
836
|
-
typeof tc.input === "object" && tc.input
|
|
837
|
-
|
|
920
|
+
typeof tc.input === "object" && tc.input
|
|
921
|
+
? Object.keys(tc.input).length
|
|
922
|
+
: 0;
|
|
923
|
+
lines +=
|
|
924
|
+
TOOL_CALL_CHROME_LINES +
|
|
925
|
+
paramCount +
|
|
926
|
+
(tc.result !== undefined ? 1 : 0);
|
|
838
927
|
}
|
|
839
928
|
}
|
|
840
929
|
return lines + MESSAGE_SPACING;
|
|
@@ -863,7 +952,11 @@ function calculateHeaderHeight(species: Species): number {
|
|
|
863
952
|
|
|
864
953
|
const SCROLL_STEP = 5;
|
|
865
954
|
|
|
866
|
-
export function render(
|
|
955
|
+
export function render(
|
|
956
|
+
runtimeUrl: string,
|
|
957
|
+
assistantId: string,
|
|
958
|
+
species: Species,
|
|
959
|
+
): number {
|
|
867
960
|
const config = SPECIES_CONFIG[species];
|
|
868
961
|
const art = config.art;
|
|
869
962
|
|
|
@@ -871,7 +964,11 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
|
|
|
871
964
|
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
872
965
|
|
|
873
966
|
const { unmount } = inkRender(
|
|
874
|
-
<DefaultMainScreen
|
|
967
|
+
<DefaultMainScreen
|
|
968
|
+
runtimeUrl={runtimeUrl}
|
|
969
|
+
assistantId={assistantId}
|
|
970
|
+
species={species}
|
|
971
|
+
/>,
|
|
875
972
|
{ exitOnCtrlC: false },
|
|
876
973
|
);
|
|
877
974
|
unmount();
|
|
@@ -883,7 +980,9 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
|
|
|
883
980
|
const statusText = health.detail
|
|
884
981
|
? `${withStatusEmoji(health.status)} (${health.detail})`
|
|
885
982
|
: withStatusEmoji(health.status);
|
|
886
|
-
process.stdout.write(
|
|
983
|
+
process.stdout.write(
|
|
984
|
+
`\x1b7\x1b[${statusCanvasLine};${statusCol}H\x1b[K${statusText}\x1b8`,
|
|
985
|
+
);
|
|
887
986
|
})
|
|
888
987
|
.catch(() => {});
|
|
889
988
|
|
|
@@ -917,7 +1016,9 @@ function SelectionWindow({
|
|
|
917
1016
|
},
|
|
918
1017
|
) => {
|
|
919
1018
|
if (key.upArrow) {
|
|
920
|
-
setSelectedIndex(
|
|
1019
|
+
setSelectedIndex(
|
|
1020
|
+
(prev: number) => (prev - 1 + options.length) % options.length,
|
|
1021
|
+
);
|
|
921
1022
|
} else if (key.downArrow) {
|
|
922
1023
|
setSelectedIndex((prev: number) => (prev + 1) % options.length);
|
|
923
1024
|
} else if (key.return) {
|
|
@@ -928,26 +1029,42 @@ function SelectionWindow({
|
|
|
928
1029
|
},
|
|
929
1030
|
);
|
|
930
1031
|
|
|
931
|
-
const borderH = "\u2500".repeat(
|
|
1032
|
+
const borderH = "\u2500".repeat(
|
|
1033
|
+
Math.max(0, DIALOG_WINDOW_WIDTH - title.length - DIALOG_TITLE_CHROME),
|
|
1034
|
+
);
|
|
932
1035
|
|
|
933
1036
|
return (
|
|
934
1037
|
<Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
|
|
935
1038
|
<Text>{"\u250C\u2500 " + title + " " + borderH + "\u2510"}</Text>
|
|
936
1039
|
{options.map((option, i) => {
|
|
937
1040
|
const marker = i === selectedIndex ? "\u276F" : " ";
|
|
938
|
-
const padding = " ".repeat(
|
|
1041
|
+
const padding = " ".repeat(
|
|
1042
|
+
Math.max(
|
|
1043
|
+
0,
|
|
1044
|
+
DIALOG_WINDOW_WIDTH - option.length - SELECTION_OPTION_CHROME,
|
|
1045
|
+
),
|
|
1046
|
+
);
|
|
939
1047
|
return (
|
|
940
1048
|
<Text key={i}>
|
|
941
1049
|
{"\u2502 "}
|
|
942
|
-
<Text color={i === selectedIndex ? "cyan" : undefined}>
|
|
1050
|
+
<Text color={i === selectedIndex ? "cyan" : undefined}>
|
|
1051
|
+
{marker}
|
|
1052
|
+
</Text>{" "}
|
|
943
1053
|
<Text bold={i === selectedIndex}>{option}</Text>
|
|
944
1054
|
{padding}
|
|
945
1055
|
{"\u2502"}
|
|
946
1056
|
</Text>
|
|
947
1057
|
);
|
|
948
1058
|
})}
|
|
949
|
-
<Text>
|
|
950
|
-
|
|
1059
|
+
<Text>
|
|
1060
|
+
{"\u2514" +
|
|
1061
|
+
"\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) +
|
|
1062
|
+
"\u2518"}
|
|
1063
|
+
</Text>
|
|
1064
|
+
<Tooltip
|
|
1065
|
+
text="\u2191/\u2193 navigate Enter select Esc cancel"
|
|
1066
|
+
delay={1000}
|
|
1067
|
+
/>
|
|
951
1068
|
</Box>
|
|
952
1069
|
);
|
|
953
1070
|
}
|
|
@@ -990,11 +1107,19 @@ function SecretInputWindow({
|
|
|
990
1107
|
},
|
|
991
1108
|
);
|
|
992
1109
|
|
|
993
|
-
const borderH = "\u2500".repeat(
|
|
1110
|
+
const borderH = "\u2500".repeat(
|
|
1111
|
+
Math.max(0, DIALOG_WINDOW_WIDTH - label.length - DIALOG_TITLE_CHROME),
|
|
1112
|
+
);
|
|
994
1113
|
const masked = "\u2022".repeat(value.length);
|
|
995
|
-
const displayText =
|
|
1114
|
+
const displayText =
|
|
1115
|
+
value.length > 0 ? masked : (placeholder ?? "Enter secret...");
|
|
996
1116
|
const displayColor = value.length > 0 ? undefined : "gray";
|
|
997
|
-
const contentPad = " ".repeat(
|
|
1117
|
+
const contentPad = " ".repeat(
|
|
1118
|
+
Math.max(
|
|
1119
|
+
0,
|
|
1120
|
+
DIALOG_WINDOW_WIDTH - displayText.length - SECRET_CONTENT_CHROME,
|
|
1121
|
+
),
|
|
1122
|
+
);
|
|
998
1123
|
|
|
999
1124
|
return (
|
|
1000
1125
|
<Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
|
|
@@ -1005,7 +1130,11 @@ function SecretInputWindow({
|
|
|
1005
1130
|
{contentPad}
|
|
1006
1131
|
{"\u2502"}
|
|
1007
1132
|
</Text>
|
|
1008
|
-
<Text>
|
|
1133
|
+
<Text>
|
|
1134
|
+
{"\u2514" +
|
|
1135
|
+
"\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) +
|
|
1136
|
+
"\u2518"}
|
|
1137
|
+
</Text>
|
|
1009
1138
|
<Tooltip text="Enter submit Esc cancel" delay={1000} />
|
|
1010
1139
|
</Box>
|
|
1011
1140
|
);
|
|
@@ -1039,7 +1168,10 @@ export interface ChatAppHandle {
|
|
|
1039
1168
|
showSecretInput: (label: string, placeholder?: string) => Promise<string>;
|
|
1040
1169
|
handleSecretPrompt: (
|
|
1041
1170
|
secret: PendingSecret,
|
|
1042
|
-
onSubmit: (
|
|
1171
|
+
onSubmit: (
|
|
1172
|
+
value: string,
|
|
1173
|
+
delivery?: "store" | "transient_send",
|
|
1174
|
+
) => Promise<void>,
|
|
1043
1175
|
) => Promise<void>;
|
|
1044
1176
|
clearFeed: () => void;
|
|
1045
1177
|
setBusy: (busy: boolean) => void;
|
|
@@ -1071,10 +1203,14 @@ function ChatApp({
|
|
|
1071
1203
|
const [feed, setFeed] = useState<FeedItem[]>([]);
|
|
1072
1204
|
const [spinnerText, setSpinnerText] = useState<string | null>(null);
|
|
1073
1205
|
const [selection, setSelection] = useState<SelectionRequest | null>(null);
|
|
1074
|
-
const [secretInput, setSecretInput] = useState<SecretInputRequest | null>(
|
|
1206
|
+
const [secretInput, setSecretInput] = useState<SecretInputRequest | null>(
|
|
1207
|
+
null,
|
|
1208
|
+
);
|
|
1075
1209
|
const [inputFocused, setInputFocused] = useState(true);
|
|
1076
1210
|
const [scrollIndex, setScrollIndex] = useState<number | null>(null);
|
|
1077
|
-
const [healthStatus, setHealthStatus] = useState<string | undefined>(
|
|
1211
|
+
const [healthStatus, setHealthStatus] = useState<string | undefined>(
|
|
1212
|
+
undefined,
|
|
1213
|
+
);
|
|
1078
1214
|
const prevFeedLengthRef = useRef(0);
|
|
1079
1215
|
const busyRef = useRef(false);
|
|
1080
1216
|
const connectedRef = useRef(false);
|
|
@@ -1098,7 +1234,10 @@ function ChatApp({
|
|
|
1098
1234
|
: spinnerText
|
|
1099
1235
|
? SPINNER_HEIGHT + INPUT_AREA_HEIGHT
|
|
1100
1236
|
: INPUT_AREA_HEIGHT;
|
|
1101
|
-
const availableRows = Math.max(
|
|
1237
|
+
const availableRows = Math.max(
|
|
1238
|
+
MIN_FEED_ROWS,
|
|
1239
|
+
terminalRows - headerHeight - bottomHeight,
|
|
1240
|
+
);
|
|
1102
1241
|
|
|
1103
1242
|
const addMessage = useCallback((msg: RuntimeMessage) => {
|
|
1104
1243
|
setFeed((prev) => [...prev, msg]);
|
|
@@ -1198,24 +1337,33 @@ function ChatApp({
|
|
|
1198
1337
|
setFeed((prev) => [...prev, item]);
|
|
1199
1338
|
}, []);
|
|
1200
1339
|
|
|
1201
|
-
const showSelection = useCallback(
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1340
|
+
const showSelection = useCallback(
|
|
1341
|
+
(title: string, options: string[]): Promise<number> => {
|
|
1342
|
+
setInputFocused(false);
|
|
1343
|
+
return new Promise<number>((resolve) => {
|
|
1344
|
+
setSelection({ title, options, resolve });
|
|
1345
|
+
});
|
|
1346
|
+
},
|
|
1347
|
+
[],
|
|
1348
|
+
);
|
|
1207
1349
|
|
|
1208
|
-
const showSecretInput = useCallback(
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1350
|
+
const showSecretInput = useCallback(
|
|
1351
|
+
(label: string, placeholder?: string): Promise<string> => {
|
|
1352
|
+
setInputFocused(false);
|
|
1353
|
+
return new Promise<string>((resolve) => {
|
|
1354
|
+
setSecretInput({ label, placeholder, resolve });
|
|
1355
|
+
});
|
|
1356
|
+
},
|
|
1357
|
+
[],
|
|
1358
|
+
);
|
|
1214
1359
|
|
|
1215
1360
|
const handleSecretPromptFn = useCallback(
|
|
1216
1361
|
async (
|
|
1217
1362
|
secret: PendingSecret,
|
|
1218
|
-
onSubmit: (
|
|
1363
|
+
onSubmit: (
|
|
1364
|
+
value: string,
|
|
1365
|
+
delivery?: "store" | "transient_send",
|
|
1366
|
+
) => Promise<void>,
|
|
1219
1367
|
): Promise<void> => {
|
|
1220
1368
|
addStatus(`\u250C Secret needed: ${secret.label}`);
|
|
1221
1369
|
addStatus(`\u2502 Service: ${secret.service} / ${secret.field}`);
|
|
@@ -1311,12 +1459,18 @@ function ChatApp({
|
|
|
1311
1459
|
try {
|
|
1312
1460
|
const health = await checkHealthRuntime(runtimeUrl);
|
|
1313
1461
|
if (!accessTokenRef.current) {
|
|
1314
|
-
accessTokenRef.current = await bootstrapAccessToken(
|
|
1462
|
+
accessTokenRef.current = await bootstrapAccessToken(
|
|
1463
|
+
runtimeUrl,
|
|
1464
|
+
bearerToken,
|
|
1465
|
+
);
|
|
1315
1466
|
}
|
|
1316
1467
|
h.hideSpinner();
|
|
1317
1468
|
h.updateHealthStatus(health.status);
|
|
1318
1469
|
if (health.status === "healthy" || health.status === "ok") {
|
|
1319
|
-
h.addStatus(
|
|
1470
|
+
h.addStatus(
|
|
1471
|
+
`${statusEmoji(health.status)} Connected to assistant`,
|
|
1472
|
+
"green",
|
|
1473
|
+
);
|
|
1320
1474
|
} else {
|
|
1321
1475
|
const statusMsg = health.message ? ` - ${health.message}` : "";
|
|
1322
1476
|
h.addStatus(
|
|
@@ -1374,7 +1528,10 @@ function ChatApp({
|
|
|
1374
1528
|
connectingRef.current = false;
|
|
1375
1529
|
h.updateHealthStatus("unreachable");
|
|
1376
1530
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1377
|
-
h.addStatus(
|
|
1531
|
+
h.addStatus(
|
|
1532
|
+
`${statusEmoji("unreachable")} Failed to connect: ${msg}`,
|
|
1533
|
+
"red",
|
|
1534
|
+
);
|
|
1378
1535
|
return false;
|
|
1379
1536
|
}
|
|
1380
1537
|
}, [runtimeUrl, assistantId, bearerToken]);
|
|
@@ -1427,17 +1584,27 @@ function ChatApp({
|
|
|
1427
1584
|
method: "POST",
|
|
1428
1585
|
headers: {
|
|
1429
1586
|
"Content-Type": "application/json",
|
|
1430
|
-
...(bearerToken
|
|
1587
|
+
...(bearerToken
|
|
1588
|
+
? { Authorization: `Bearer ${bearerToken}` }
|
|
1589
|
+
: {}),
|
|
1431
1590
|
},
|
|
1432
|
-
body: JSON.stringify({
|
|
1591
|
+
body: JSON.stringify({
|
|
1592
|
+
pairingRequestId,
|
|
1593
|
+
pairingSecret,
|
|
1594
|
+
gatewayUrl,
|
|
1595
|
+
}),
|
|
1433
1596
|
});
|
|
1434
1597
|
|
|
1435
1598
|
if (!registerRes.ok) {
|
|
1436
1599
|
const body = await registerRes.text().catch(() => "");
|
|
1437
|
-
throw new Error(
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
`HTTP ${registerRes.status}: ${body || registerRes.statusText}`,
|
|
1602
|
+
);
|
|
1438
1603
|
}
|
|
1439
1604
|
|
|
1440
|
-
const hostId = createHash("sha256")
|
|
1605
|
+
const hostId = createHash("sha256")
|
|
1606
|
+
.update(hostname() + userInfo().username)
|
|
1607
|
+
.digest("hex");
|
|
1441
1608
|
const payload = JSON.stringify({
|
|
1442
1609
|
type: "vellum-daemon",
|
|
1443
1610
|
v: 4,
|
|
@@ -1462,14 +1629,18 @@ function ChatApp({
|
|
|
1462
1629
|
);
|
|
1463
1630
|
} catch (err) {
|
|
1464
1631
|
h.hideSpinner();
|
|
1465
|
-
h.showError(
|
|
1632
|
+
h.showError(
|
|
1633
|
+
`Pairing failed: ${err instanceof Error ? err.message : err}`,
|
|
1634
|
+
);
|
|
1466
1635
|
}
|
|
1467
1636
|
return;
|
|
1468
1637
|
}
|
|
1469
1638
|
|
|
1470
1639
|
if (trimmed === "/retire") {
|
|
1471
1640
|
if (!project || !zone) {
|
|
1472
|
-
h.showError(
|
|
1641
|
+
h.showError(
|
|
1642
|
+
"No instance info available. Connect to a hatched instance first.",
|
|
1643
|
+
);
|
|
1473
1644
|
return;
|
|
1474
1645
|
}
|
|
1475
1646
|
|
|
@@ -1524,9 +1695,13 @@ function ChatApp({
|
|
|
1524
1695
|
handleRef_.current?.hideSpinner();
|
|
1525
1696
|
if (code === 0) {
|
|
1526
1697
|
removeAssistantEntry(assistantId);
|
|
1527
|
-
handleRef_.current?.addStatus(
|
|
1698
|
+
handleRef_.current?.addStatus(
|
|
1699
|
+
`Removed ${assistantId} from lockfile.json`,
|
|
1700
|
+
);
|
|
1528
1701
|
} else {
|
|
1529
|
-
handleRef_.current?.showError(
|
|
1702
|
+
handleRef_.current?.showError(
|
|
1703
|
+
`Failed to delete instance (exit code ${code})`,
|
|
1704
|
+
);
|
|
1530
1705
|
}
|
|
1531
1706
|
cleanup();
|
|
1532
1707
|
process.exit(code === 0 ? 0 : 1);
|
|
@@ -1534,14 +1709,18 @@ function ChatApp({
|
|
|
1534
1709
|
|
|
1535
1710
|
child.on("error", (err) => {
|
|
1536
1711
|
handleRef_.current?.hideSpinner();
|
|
1537
|
-
handleRef_.current?.showError(
|
|
1712
|
+
handleRef_.current?.showError(
|
|
1713
|
+
`Failed to retire instance: ${err.message}`,
|
|
1714
|
+
);
|
|
1538
1715
|
});
|
|
1539
1716
|
return;
|
|
1540
1717
|
}
|
|
1541
1718
|
|
|
1542
1719
|
if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
|
|
1543
1720
|
if (!project || !zone) {
|
|
1544
|
-
h.showError(
|
|
1721
|
+
h.showError(
|
|
1722
|
+
"No instance info available. Connect to a hatched instance first.",
|
|
1723
|
+
);
|
|
1545
1724
|
return;
|
|
1546
1725
|
}
|
|
1547
1726
|
const userPrompt = trimmed.slice("/doctor".length).trim() || undefined;
|
|
@@ -1571,7 +1750,9 @@ function ChatApp({
|
|
|
1571
1750
|
(event) => {
|
|
1572
1751
|
switch (event.phase) {
|
|
1573
1752
|
case "invoking_prompt":
|
|
1574
|
-
handleRef_.current?.showSpinner(
|
|
1753
|
+
handleRef_.current?.showSpinner(
|
|
1754
|
+
`Analyzing ${assistantId}...`,
|
|
1755
|
+
);
|
|
1575
1756
|
break;
|
|
1576
1757
|
case "calling_tool":
|
|
1577
1758
|
handleRef_.current?.showSpinner(
|
|
@@ -1579,7 +1760,9 @@ function ChatApp({
|
|
|
1579
1760
|
);
|
|
1580
1761
|
break;
|
|
1581
1762
|
case "processing_tool_result":
|
|
1582
|
-
handleRef_.current?.showSpinner(
|
|
1763
|
+
handleRef_.current?.showSpinner(
|
|
1764
|
+
`Reviewing diagnostics for ${assistantId}...`,
|
|
1765
|
+
);
|
|
1583
1766
|
break;
|
|
1584
1767
|
}
|
|
1585
1768
|
},
|
|
@@ -1589,14 +1772,17 @@ function ChatApp({
|
|
|
1589
1772
|
h.hideSpinner();
|
|
1590
1773
|
if (result.recommendation) {
|
|
1591
1774
|
h.addStatus(`Recommendation:\n${result.recommendation}`);
|
|
1592
|
-
chatLogRef.current.push({
|
|
1775
|
+
chatLogRef.current.push({
|
|
1776
|
+
role: "assistant",
|
|
1777
|
+
content: result.recommendation,
|
|
1778
|
+
});
|
|
1593
1779
|
} else if (result.error) {
|
|
1594
1780
|
h.showError(result.error);
|
|
1595
1781
|
chatLogRef.current.push({ role: "error", content: result.error });
|
|
1596
1782
|
}
|
|
1597
1783
|
} catch (err) {
|
|
1598
1784
|
h.hideSpinner();
|
|
1599
|
-
const errorMsg = `Doctor
|
|
1785
|
+
const errorMsg = `Doctor assistant unreachable: ${err instanceof Error ? err.message : err}`;
|
|
1600
1786
|
h.showError(errorMsg);
|
|
1601
1787
|
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
1602
1788
|
}
|
|
@@ -1657,7 +1843,9 @@ function ChatApp({
|
|
|
1657
1843
|
h.showSpinner("Working...");
|
|
1658
1844
|
|
|
1659
1845
|
while (true) {
|
|
1660
|
-
await new Promise((resolve) =>
|
|
1846
|
+
await new Promise((resolve) =>
|
|
1847
|
+
setTimeout(resolve, RESPONSE_POLL_INTERVAL_MS),
|
|
1848
|
+
);
|
|
1661
1849
|
|
|
1662
1850
|
// Check for pending confirmations/secrets
|
|
1663
1851
|
try {
|
|
@@ -1686,19 +1874,26 @@ function ChatApp({
|
|
|
1686
1874
|
if (pending.pendingSecret) {
|
|
1687
1875
|
const secretRequestId = pending.pendingSecret.requestId ?? "";
|
|
1688
1876
|
h.hideSpinner();
|
|
1689
|
-
await h.handleSecretPrompt(
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1877
|
+
await h.handleSecretPrompt(
|
|
1878
|
+
pending.pendingSecret,
|
|
1879
|
+
async (value, delivery) => {
|
|
1880
|
+
await runtimeRequest(
|
|
1881
|
+
runtimeUrl,
|
|
1882
|
+
assistantId,
|
|
1883
|
+
"/secret",
|
|
1884
|
+
{
|
|
1885
|
+
method: "POST",
|
|
1886
|
+
body: JSON.stringify({
|
|
1887
|
+
requestId: secretRequestId,
|
|
1888
|
+
value,
|
|
1889
|
+
delivery,
|
|
1890
|
+
}),
|
|
1891
|
+
},
|
|
1892
|
+
bearerToken,
|
|
1893
|
+
accessTokenRef.current,
|
|
1894
|
+
);
|
|
1895
|
+
},
|
|
1896
|
+
);
|
|
1702
1897
|
h.showSpinner("Working...");
|
|
1703
1898
|
continue;
|
|
1704
1899
|
}
|
|
@@ -1719,7 +1914,10 @@ function ChatApp({
|
|
|
1719
1914
|
seenMessageIdsRef.current.add(msg.id);
|
|
1720
1915
|
if (msg.role === "assistant") {
|
|
1721
1916
|
h.addMessage(msg);
|
|
1722
|
-
chatLogRef.current.push({
|
|
1917
|
+
chatLogRef.current.push({
|
|
1918
|
+
role: "assistant",
|
|
1919
|
+
content: msg.content,
|
|
1920
|
+
});
|
|
1723
1921
|
h.setBusy(false);
|
|
1724
1922
|
h.hideSpinner();
|
|
1725
1923
|
return;
|
|
@@ -1730,7 +1928,6 @@ function ChatApp({
|
|
|
1730
1928
|
// Poll failure; retry
|
|
1731
1929
|
}
|
|
1732
1930
|
}
|
|
1733
|
-
|
|
1734
1931
|
} catch (error) {
|
|
1735
1932
|
h.setBusy(false);
|
|
1736
1933
|
h.hideSpinner();
|
|
@@ -1746,7 +1943,15 @@ function ChatApp({
|
|
|
1746
1943
|
}
|
|
1747
1944
|
}
|
|
1748
1945
|
},
|
|
1749
|
-
[
|
|
1946
|
+
[
|
|
1947
|
+
runtimeUrl,
|
|
1948
|
+
assistantId,
|
|
1949
|
+
bearerToken,
|
|
1950
|
+
project,
|
|
1951
|
+
zone,
|
|
1952
|
+
cleanup,
|
|
1953
|
+
ensureConnected,
|
|
1954
|
+
],
|
|
1750
1955
|
);
|
|
1751
1956
|
|
|
1752
1957
|
const handleSubmit = useCallback(
|
|
@@ -1890,7 +2095,8 @@ function ChatApp({
|
|
|
1890
2095
|
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
1891
2096
|
{visibleWindow.hiddenAbove > 0 ? (
|
|
1892
2097
|
<Text dimColor>
|
|
1893
|
-
{"\u2191"} {visibleWindow.hiddenAbove} more above
|
|
2098
|
+
{"\u2191"} {visibleWindow.hiddenAbove} more above
|
|
2099
|
+
(Shift+\u2191/Cmd+\u2191)
|
|
1894
2100
|
</Text>
|
|
1895
2101
|
) : null}
|
|
1896
2102
|
|
|
@@ -1905,7 +2111,10 @@ function ChatApp({
|
|
|
1905
2111
|
}
|
|
1906
2112
|
if (item.type === "status") {
|
|
1907
2113
|
return (
|
|
1908
|
-
<Text
|
|
2114
|
+
<Text
|
|
2115
|
+
key={feedIndex}
|
|
2116
|
+
color={item.color as "green" | "yellow" | "red" | undefined}
|
|
2117
|
+
>
|
|
1909
2118
|
{item.text}
|
|
1910
2119
|
</Text>
|
|
1911
2120
|
);
|
|
@@ -1925,7 +2134,8 @@ function ChatApp({
|
|
|
1925
2134
|
|
|
1926
2135
|
{visibleWindow.hiddenBelow > 0 ? (
|
|
1927
2136
|
<Text dimColor>
|
|
1928
|
-
{"\u2193"} {visibleWindow.hiddenBelow} more below
|
|
2137
|
+
{"\u2193"} {visibleWindow.hiddenBelow} more below
|
|
2138
|
+
(Shift+\u2193/Cmd+\u2193)
|
|
1929
2139
|
</Text>
|
|
1930
2140
|
) : null}
|
|
1931
2141
|
</Box>
|
|
@@ -1956,14 +2166,14 @@ function ChatApp({
|
|
|
1956
2166
|
<Box paddingLeft={1}>
|
|
1957
2167
|
<Text color="green" bold>
|
|
1958
2168
|
you{">"}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2169
|
+
{" "}
|
|
2170
|
+
</Text>
|
|
2171
|
+
<TextInput
|
|
2172
|
+
value={inputValue}
|
|
2173
|
+
onChange={setInputValue}
|
|
2174
|
+
onSubmit={handleSubmit}
|
|
2175
|
+
focus={inputFocused}
|
|
2176
|
+
/>
|
|
1967
2177
|
</Box>
|
|
1968
2178
|
<Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
|
|
1969
2179
|
<Text dimColor> ? for shortcuts</Text>
|