@vellumai/cli 0.8.1 → 0.8.2

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.
@@ -1,80 +1,81 @@
1
- import { createInterface } from "readline";
2
-
3
1
  import { resolveAssistant } from "../lib/assistant-config.js";
4
-
5
- async function promptMasked(prompt: string): Promise<string> {
6
- return new Promise((resolve) => {
7
- const rl = createInterface({
8
- input: process.stdin,
9
- output: process.stdout,
10
- });
11
-
12
- process.stdout.write(prompt);
13
-
14
- const stdin = process.stdin;
15
- const wasRaw = stdin.isRaw;
16
- if (stdin.isTTY) {
17
- stdin.setRawMode(true);
18
- }
19
-
20
- let input = "";
21
- const onData = (key: Buffer): void => {
22
- const char = key.toString("utf-8");
23
-
24
- if (char === "\r" || char === "\n") {
25
- stdin.removeListener("data", onData);
26
- if (stdin.isTTY) {
27
- stdin.setRawMode(wasRaw ?? false);
28
- }
29
- process.stdout.write("\n");
30
- rl.close();
31
- resolve(input);
32
- } else if (char === "\u0003") {
33
- process.stdout.write("\n");
34
- process.exit(1);
35
- } else if (char === "\u007F" || char === "\b") {
36
- if (input.length > 0) {
37
- input = input.slice(0, -1);
38
- process.stdout.write("\b \b");
39
- }
40
- } else if (char.length === 1 && char >= " ") {
41
- input += char;
42
- process.stdout.write("*");
2
+ import {
3
+ loadGuardianToken,
4
+ refreshGuardianToken,
5
+ type GuardianTokenData,
6
+ } from "../lib/guardian-token.js";
7
+ import {
8
+ ensureProviderApiKey,
9
+ formatProviderName,
10
+ } from "../lib/provider-secrets.js";
11
+
12
+ function parseSetupArgs(args: string[]): { provider: string } {
13
+ let provider = "anthropic";
14
+
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg === "--provider") {
18
+ const value = args[i + 1];
19
+ if (!value || value.startsWith("-")) {
20
+ throw new Error("--provider requires a provider name.");
43
21
  }
44
- };
22
+ provider = value;
23
+ i++;
24
+ } else if (arg.startsWith("--provider=")) {
25
+ provider = arg.slice("--provider=".length);
26
+ } else {
27
+ throw new Error(`Unknown option: ${arg}`);
28
+ }
29
+ }
45
30
 
46
- stdin.on("data", onData);
47
- });
31
+ return { provider };
48
32
  }
49
33
 
50
- async function validateAnthropicKey(apiKey: string): Promise<boolean> {
51
- try {
52
- const resp = await fetch("https://api.anthropic.com/v1/models", {
53
- headers: {
54
- "x-api-key": apiKey,
55
- "anthropic-version": "2023-06-01",
56
- },
57
- signal: AbortSignal.timeout(10_000),
58
- });
59
- return resp.ok;
60
- } catch {
34
+ function isGuardianAccessTokenUsable(
35
+ tokenData: GuardianTokenData | null,
36
+ ): tokenData is GuardianTokenData {
37
+ if (!tokenData?.accessToken) {
61
38
  return false;
62
39
  }
40
+ const expiresAt = new Date(tokenData.accessTokenExpiresAt).getTime();
41
+ return Number.isFinite(expiresAt) && expiresAt > Date.now();
63
42
  }
64
43
 
65
44
  export async function setup(): Promise<void> {
66
45
  const args = process.argv.slice(3);
67
46
 
68
47
  if (args.includes("--help") || args.includes("-h")) {
69
- console.log("Usage: vellum setup");
48
+ console.log("Usage: vellum setup [--provider <provider>]");
49
+ console.log("");
50
+ console.log("Configure a provider API key on the active assistant.");
70
51
  console.log("");
71
- console.log("Interactive wizard to configure API keys.");
52
+ console.log("Options:");
72
53
  console.log(
73
- "Injects secrets into your running assistant via the gateway API.",
54
+ " --provider <provider> Provider to configure. Defaults to anthropic.",
74
55
  );
56
+ console.log("");
57
+ console.log("Behavior:");
58
+ console.log(
59
+ " - Checks the active assistant for an existing provider key.",
60
+ );
61
+ console.log(" - Uses the matching environment variable when it is set.");
62
+ console.log(" - Otherwise prompts securely without echoing the key.");
63
+ console.log("");
64
+ console.log("Examples:");
65
+ console.log(" vellum setup");
66
+ console.log(" ANTHROPIC_API_KEY=... vellum setup");
67
+ console.log(" vellum setup --provider openai");
75
68
  process.exit(0);
76
69
  }
77
70
 
71
+ let parsed: { provider: string };
72
+ try {
73
+ parsed = parseSetupArgs(args);
74
+ } catch (error) {
75
+ console.error(error instanceof Error ? `Error: ${error.message}` : error);
76
+ process.exit(1);
77
+ }
78
+
78
79
  const entry = resolveAssistant();
79
80
  if (!entry) {
80
81
  console.error(
@@ -84,54 +85,58 @@ export async function setup(): Promise<void> {
84
85
  }
85
86
 
86
87
  const gatewayUrl = entry.localUrl ?? entry.runtimeUrl;
88
+ let bearerToken: string | undefined;
89
+ const guardianToken = loadGuardianToken(entry.assistantId);
90
+ if (isGuardianAccessTokenUsable(guardianToken)) {
91
+ bearerToken = guardianToken.accessToken;
92
+ } else {
93
+ const refreshedToken = guardianToken
94
+ ? await refreshGuardianToken(gatewayUrl, entry.assistantId)
95
+ : null;
96
+ bearerToken = isGuardianAccessTokenUsable(refreshedToken)
97
+ ? refreshedToken.accessToken
98
+ : entry.bearerToken;
99
+ }
87
100
 
88
101
  console.log("Vellum Setup");
89
102
  console.log("============\n");
90
103
 
91
- const apiKey = await promptMasked(
92
- "Enter your Anthropic API key (sk-ant-...): ",
93
- );
94
-
95
- if (!apiKey.trim()) {
96
- console.error("Error: API key cannot be empty.");
97
- process.exit(1);
98
- }
99
-
100
- console.log("Validating key...");
101
- const valid = await validateAnthropicKey(apiKey.trim());
104
+ try {
105
+ const result = await ensureProviderApiKey({
106
+ gatewayUrl,
107
+ provider: parsed.provider,
108
+ bearerToken,
109
+ env: process.env,
110
+ });
102
111
 
103
- if (!valid) {
104
- console.error(
105
- "Error: Invalid API key. Could not authenticate with the Anthropic API.",
106
- );
107
- process.exit(1);
108
- }
112
+ if (result.status === "already_configured") {
113
+ console.log(
114
+ `${formatProviderName(result.provider)} API key is already configured.`,
115
+ );
116
+ return;
117
+ }
109
118
 
110
- const headers: Record<string, string> = {
111
- "Content-Type": "application/json",
112
- Accept: "application/json",
113
- };
114
- if (entry.bearerToken) {
115
- headers["Authorization"] = `Bearer ${entry.bearerToken}`;
116
- }
119
+ if (result.status === "configured") {
120
+ const providerName = formatProviderName(result.provider);
121
+ const source = result.source === "env" ? " from the environment" : "";
122
+ console.log(`\n${providerName} API key saved to assistant${source}.`);
123
+ console.log("Setup complete.");
124
+ return;
125
+ }
117
126
 
118
- const response = await fetch(`${gatewayUrl}/v1/secrets`, {
119
- method: "POST",
120
- headers,
121
- body: JSON.stringify({
122
- type: "credential",
123
- name: "ANTHROPIC_API_KEY",
124
- value: apiKey.trim(),
125
- }),
126
- signal: AbortSignal.timeout(10_000),
127
- });
127
+ if (result.status === "skipped") {
128
+ console.log(result.message);
129
+ return;
130
+ }
128
131
 
129
- if (!response.ok) {
132
+ console.error(`Error: ${result.message}`);
133
+ process.exit(1);
134
+ } catch (error) {
130
135
  console.error(
131
- `Error: Failed to store API key in assistant (${response.status}).`,
136
+ error instanceof Error
137
+ ? `Error: ${error.message}`
138
+ : "Error: Setup failed.",
132
139
  );
133
140
  process.exit(1);
134
141
  }
135
-
136
- console.log("\nAPI key saved to assistant. Setup complete.");
137
142
  }
@@ -1,5 +1,4 @@
1
1
  import { spawn } from "child_process";
2
- import { randomUUID } from "crypto";
3
2
  import { basename } from "path";
4
3
  import {
5
4
  useCallback,
@@ -14,7 +13,6 @@ import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
14
13
  import { removeAssistantEntry } from "../lib/assistant-config";
15
14
 
16
15
  import { SPECIES_CONFIG, type Species } from "../lib/constants";
17
- import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
18
16
  import { checkHealth } from "../lib/health-check";
19
17
  import { appendHistory, loadHistory } from "../lib/input-history";
20
18
  import { tuiLog } from "../lib/tui-log";
@@ -41,7 +39,6 @@ export const ANSI = {
41
39
  export const SLASH_COMMANDS = [
42
40
  "/btw",
43
41
  "/clear",
44
- "/doctor",
45
42
  "/exit",
46
43
  "/help",
47
44
  "/q",
@@ -62,7 +59,7 @@ const HEADER_PREFIX_UNICODE = "── Vellum ";
62
59
  const HEADER_PREFIX_ASCII = "-- Vellum ";
63
60
 
64
61
  // Left panel structure: HEADER lines + art + FOOTER lines
65
- const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
62
+ const LEFT_HEADER_LINES = 4; // spacer + eyebrow + title + spacer
66
63
  const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
67
64
 
68
65
  // Right panel structure
@@ -70,7 +67,7 @@ const TIPS = [
70
67
  "Send a message to start chatting",
71
68
  "Use /help to see available commands",
72
69
  ];
73
- const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant, Species, Status — each with heading + value
70
+ const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant ID, Species, Status — each with heading + value
74
71
  const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
75
72
  const RIGHT_PANEL_TIPS_HEADING = 1;
76
73
  const RIGHT_PANEL_LINE_COUNT =
@@ -103,7 +100,7 @@ const MIN_FEED_ROWS = 3;
103
100
  // Feed item height estimation
104
101
  const TOOL_CALL_CHROME_LINES = 2; // header (┌) + footer (└)
105
102
  const MESSAGE_SPACING = 1;
106
- const HELP_DISPLAY_HEIGHT = 8;
103
+ const HELP_DISPLAY_HEIGHT = 7;
107
104
 
108
105
  interface ListMessagesResponse {
109
106
  messages: RuntimeMessage[];
@@ -668,6 +665,21 @@ function truncateValue(value: unknown, maxLen: number): string {
668
665
  return serialized;
669
666
  }
670
667
 
668
+ function formatHeaderTitle(assistantName?: string): string {
669
+ const rawTitle = assistantName?.trim() || "Meet your Assistant!";
670
+ const title = rawTitle.replace(/\s+/g, " ");
671
+ const maxTitleLength = LEFT_PANEL_WIDTH - 2;
672
+ const displayTitle =
673
+ title.length > maxTitleLength
674
+ ? title.slice(0, maxTitleLength - 3) + "..."
675
+ : title;
676
+ return ` ${displayTitle}`;
677
+ }
678
+
679
+ function formatHeaderEyebrow(): string {
680
+ return " Assistant";
681
+ }
682
+
671
683
  interface ToolCallDisplayProps {
672
684
  tc: ToolCallInfo;
673
685
  }
@@ -734,10 +746,6 @@ function HelpDisplay(): ReactElement {
734
746
  {" /btw <question> "}
735
747
  <Text dimColor>Ask a side question while the assistant is working</Text>
736
748
  </Text>
737
- <Text>
738
- {" /doctor [question] "}
739
- <Text dimColor>Run diagnostics on the remote instance via SSH</Text>
740
- </Text>
741
749
  <Text>
742
750
  {" /retire "}
743
751
  <Text dimColor>Retire the remote instance and exit</Text>
@@ -861,28 +869,33 @@ function stripAnsi(str: string): string {
861
869
  interface DefaultMainScreenProps {
862
870
  runtimeUrl: string;
863
871
  assistantId: string;
872
+ assistantName?: string;
864
873
  species: Species;
865
874
  healthStatus?: string;
866
875
  }
867
876
 
868
877
  interface StyledLine {
869
878
  text: string;
870
- style: "heading" | "dim" | "normal";
879
+ style: "heading" | "dim" | "normal" | "eyebrow" | "title" | "art";
871
880
  }
872
881
 
873
882
  function CompactHeader({
883
+ assistantName,
874
884
  species,
875
885
  healthStatus,
876
886
  totalWidth,
877
887
  }: {
888
+ assistantName?: string;
878
889
  species: Species;
879
890
  healthStatus?: string;
880
891
  totalWidth: number;
881
892
  }): ReactElement {
882
- const config = SPECIES_CONFIG[species];
883
893
  const accentColor = species === "openclaw" ? "red" : "magenta";
884
894
  const status = healthStatus ?? "checking...";
885
- const label = ` ${config.hatchedEmoji} ${species} ${statusEmoji(status)} `;
895
+ const identity = assistantName?.trim() || species;
896
+ const compactIdentity =
897
+ identity.length > 18 ? `${identity.slice(0, 15)}...` : identity;
898
+ const label = ` ${compactIdentity} ${statusEmoji(status)} `;
886
899
  const prefix = "── Vellum";
887
900
  const suffix = "──";
888
901
  const fillLen = Math.max(
@@ -904,13 +917,13 @@ function CompactHeader({
904
917
  function DefaultMainScreen({
905
918
  runtimeUrl,
906
919
  assistantId,
920
+ assistantName,
907
921
  species,
908
922
  healthStatus,
909
923
  }: DefaultMainScreenProps): ReactElement {
910
924
  const cwd = process.cwd();
911
925
  const dirName = basename(cwd);
912
926
  const config = SPECIES_CONFIG[species];
913
- const art = config.art;
914
927
  const accentColor = species === "openclaw" ? "red" : "magenta";
915
928
  const caps = getTerminalCapabilities();
916
929
  const headerPrefix = caps.unicodeSupported
@@ -926,6 +939,7 @@ function DefaultMainScreen({
926
939
  if (isCompact) {
927
940
  return (
928
941
  <CompactHeader
942
+ assistantName={assistantName}
929
943
  species={species}
930
944
  healthStatus={healthStatus}
931
945
  totalWidth={totalWidth}
@@ -933,16 +947,21 @@ function DefaultMainScreen({
933
947
  );
934
948
  }
935
949
 
950
+ const art = config.art;
936
951
  const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
937
952
 
938
- const leftLines = [
939
- " ",
940
- " Meet your Assistant!",
941
- " ",
942
- ...art.map((l) => ` ${stripAnsi(l)}`),
943
- " ",
944
- ` ${runtimeUrl}`,
945
- ` ~/${dirName}`,
953
+ const leftLines: StyledLine[] = [
954
+ { text: " ", style: "normal" },
955
+ { text: formatHeaderEyebrow(), style: "eyebrow" },
956
+ { text: formatHeaderTitle(assistantName), style: "title" },
957
+ { text: " ", style: "normal" },
958
+ ...art.map((line) => ({
959
+ text: ` ${stripAnsi(line)}`,
960
+ style: "art" as const,
961
+ })),
962
+ { text: " ", style: "normal" },
963
+ { text: ` ${runtimeUrl}`, style: "dim" },
964
+ { text: ` ~/${dirName}`, style: "dim" },
946
965
  ];
947
966
 
948
967
  const rightLines: StyledLine[] = [
@@ -950,7 +969,7 @@ function DefaultMainScreen({
950
969
  { text: "Tips for getting started", style: "heading" },
951
970
  ...TIPS.map((t) => ({ text: t, style: "normal" as const })),
952
971
  { text: " ", style: "normal" },
953
- { text: "Assistant", style: "heading" },
972
+ { text: "Assistant ID", style: "heading" },
954
973
  { text: assistantId, style: "dim" },
955
974
  { text: "Species", style: "heading" },
956
975
  {
@@ -972,29 +991,37 @@ function DefaultMainScreen({
972
991
  <Box flexDirection="row">
973
992
  <Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
974
993
  {Array.from({ length: maxLines }, (_, i) => {
975
- const line = leftLines[i] ?? " ";
976
- if (i === 1) {
994
+ const item = leftLines[i];
995
+ if (!item) return <Text key={i}> </Text>;
996
+ if (item.style === "eyebrow") {
997
+ return (
998
+ <Text key={i} color={caps.isDumb ? undefined : accentColor}>
999
+ {item.text}
1000
+ </Text>
1001
+ );
1002
+ }
1003
+ if (item.style === "title") {
977
1004
  return (
978
1005
  <Text key={i} bold>
979
- {line}
1006
+ {item.text}
980
1007
  </Text>
981
1008
  );
982
1009
  }
983
- if (i > 2 && i <= 2 + art.length) {
1010
+ if (item.style === "art") {
984
1011
  return (
985
1012
  <Text key={i} color={caps.isDumb ? undefined : accentColor}>
986
- {line}
1013
+ {item.text}
987
1014
  </Text>
988
1015
  );
989
1016
  }
990
- if (i > 2 + art.length) {
1017
+ if (item.style === "dim") {
991
1018
  return (
992
1019
  <Text key={i} dimColor>
993
- {line}
1020
+ {item.text}
994
1021
  </Text>
995
1022
  );
996
1023
  }
997
- return <Text key={i}>{line}</Text>;
1024
+ return <Text key={i}>{item.text}</Text>;
998
1025
  })}
999
1026
  </Box>
1000
1027
  <Box flexDirection="column" width={rightPanelWidth}>
@@ -1115,8 +1142,7 @@ function calculateHeaderHeight(
1115
1142
  if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
1116
1143
  return COMPACT_HEADER_HEIGHT;
1117
1144
  }
1118
- const config = SPECIES_CONFIG[species];
1119
- const artLength = config.art.length;
1145
+ const artLength = SPECIES_CONFIG[species].art.length;
1120
1146
  const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
1121
1147
  const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
1122
1148
  return maxLines + HEADER_CHROME_LINES;
@@ -1128,12 +1154,11 @@ export function render(
1128
1154
  runtimeUrl: string,
1129
1155
  assistantId: string,
1130
1156
  species: Species,
1157
+ assistantName?: string,
1131
1158
  ): number {
1132
1159
  const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
1133
1160
  const isCompact = terminalColumns < COMPACT_THRESHOLD;
1134
-
1135
- const config = SPECIES_CONFIG[species];
1136
- const art = config.art;
1161
+ const art = SPECIES_CONFIG[species].art;
1137
1162
 
1138
1163
  const leftLineCount = LEFT_HEADER_LINES + art.length + LEFT_FOOTER_LINES;
1139
1164
  const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
@@ -1142,6 +1167,7 @@ export function render(
1142
1167
  <DefaultMainScreen
1143
1168
  runtimeUrl={runtimeUrl}
1144
1169
  assistantId={assistantId}
1170
+ assistantName={assistantName}
1145
1171
  species={species}
1146
1172
  />,
1147
1173
  { exitOnCtrlC: false },
@@ -1360,6 +1386,7 @@ export interface ChatAppHandle {
1360
1386
  interface ChatAppProps {
1361
1387
  runtimeUrl: string;
1362
1388
  assistantId: string;
1389
+ assistantName?: string;
1363
1390
  species: Species;
1364
1391
  /** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
1365
1392
  * { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
@@ -1373,6 +1400,7 @@ interface ChatAppProps {
1373
1400
  function ChatApp({
1374
1401
  runtimeUrl,
1375
1402
  assistantId,
1403
+ assistantName,
1376
1404
  species,
1377
1405
  auth,
1378
1406
  project,
@@ -1405,11 +1433,9 @@ function ChatApp({
1405
1433
  const connectedRef = useRef(false);
1406
1434
  const connectingRef = useRef(false);
1407
1435
  const seenMessageIdsRef = useRef(new Set<string>());
1408
- const chatLogRef = useRef<ChatLogEntry[]>([]);
1409
1436
  const sseAbortRef = useRef<AbortController | null>(null);
1410
1437
  const streamingTextRef = useRef("");
1411
1438
  const streamingToolCallsRef = useRef<ToolCallInfo[]>([]);
1412
- const doctorSessionIdRef = useRef(randomUUID());
1413
1439
  const handleRef_ = useRef<ChatAppHandle | null>(null);
1414
1440
 
1415
1441
  const { stdout } = useStdout();
@@ -1840,10 +1866,6 @@ function ChatApp({
1840
1866
  };
1841
1867
  seenMessageIdsRef.current.add(msg.id);
1842
1868
  hRef.addMessage(msg);
1843
- chatLogRef.current.push({
1844
- role: "assistant",
1845
- content: text,
1846
- });
1847
1869
  process.stdout.write("\x07");
1848
1870
  }
1849
1871
 
@@ -2012,79 +2034,6 @@ function ChatApp({
2012
2034
  return;
2013
2035
  }
2014
2036
 
2015
- if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
2016
- if (!project || !zone) {
2017
- h.showError(
2018
- "No instance info available. Connect to a hatched instance first.",
2019
- );
2020
- return;
2021
- }
2022
- const userPrompt = trimmed.slice("/doctor".length).trim() || undefined;
2023
- const recentChatContext = chatLogRef.current.slice(-20);
2024
-
2025
- chatLogRef.current.push({ role: "user", content: trimmed });
2026
-
2027
- if (userPrompt) {
2028
- const doctorUserMsg: RuntimeMessage = {
2029
- id: "local-user-" + Date.now(),
2030
- role: "user",
2031
- content: userPrompt,
2032
- timestamp: new Date().toISOString(),
2033
- label: "You (to Doctor):",
2034
- };
2035
- h.addMessage(doctorUserMsg);
2036
- }
2037
-
2038
- h.showSpinner(`Analyzing ${assistantId}...`);
2039
-
2040
- try {
2041
- const result = await callDoctorDaemon(
2042
- assistantId,
2043
- project,
2044
- zone,
2045
- userPrompt,
2046
- (event) => {
2047
- switch (event.phase) {
2048
- case "invoking_prompt":
2049
- handleRef_.current?.showSpinner(
2050
- `Analyzing ${assistantId}...`,
2051
- );
2052
- break;
2053
- case "calling_tool":
2054
- handleRef_.current?.showSpinner(
2055
- `Running ${event.toolName ?? "tool"} on ${assistantId}...`,
2056
- );
2057
- break;
2058
- case "processing_tool_result":
2059
- handleRef_.current?.showSpinner(
2060
- `Reviewing diagnostics for ${assistantId}...`,
2061
- );
2062
- break;
2063
- }
2064
- },
2065
- doctorSessionIdRef.current,
2066
- recentChatContext,
2067
- );
2068
- h.hideSpinner();
2069
- if (result.recommendation) {
2070
- h.addStatus(`Recommendation:\n${result.recommendation}`);
2071
- chatLogRef.current.push({
2072
- role: "assistant",
2073
- content: result.recommendation,
2074
- });
2075
- } else if (result.error) {
2076
- h.showError(result.error);
2077
- chatLogRef.current.push({ role: "error", content: result.error });
2078
- }
2079
- } catch (err) {
2080
- h.hideSpinner();
2081
- const errorMsg = `Doctor assistant unreachable: ${err instanceof Error ? err.message : err}`;
2082
- h.showError(errorMsg);
2083
- chatLogRef.current.push({ role: "error", content: errorMsg });
2084
- }
2085
- return;
2086
- }
2087
-
2088
2037
  // If a connection attempt is already in progress, don't silently drop input
2089
2038
  if (connectingRef.current) {
2090
2039
  h.addStatus(
@@ -2201,7 +2150,6 @@ function ChatApp({
2201
2150
  );
2202
2151
  clearTimeout(timeoutId);
2203
2152
  if (sendResult.accepted) {
2204
- chatLogRef.current.push({ role: "user", content: trimmed });
2205
2153
  h.addStatus(
2206
2154
  "Message queued — will be processed after current response",
2207
2155
  "gray",
@@ -2233,7 +2181,6 @@ function ChatApp({
2233
2181
  return;
2234
2182
  }
2235
2183
 
2236
- chatLogRef.current.push({ role: "user", content: trimmed });
2237
2184
  seenMessageIdsRef.current.add("pending-user-" + Date.now());
2238
2185
 
2239
2186
  h.showSpinner("Sending...");
@@ -2264,7 +2211,6 @@ function ChatApp({
2264
2211
  const errorMsg =
2265
2212
  sendErr instanceof Error ? sendErr.message : String(sendErr);
2266
2213
  h.showError(errorMsg);
2267
- chatLogRef.current.push({ role: "error", content: errorMsg });
2268
2214
  return;
2269
2215
  }
2270
2216
 
@@ -2462,6 +2408,7 @@ function ChatApp({
2462
2408
  <DefaultMainScreen
2463
2409
  runtimeUrl={runtimeUrl}
2464
2410
  assistantId={assistantId}
2411
+ assistantName={assistantName}
2465
2412
  species={species}
2466
2413
  healthStatus={healthStatus}
2467
2414
  />
@@ -2581,7 +2528,12 @@ export function renderChatApp(
2581
2528
  assistantId: string,
2582
2529
  species: Species,
2583
2530
  onExit: () => void,
2584
- options?: { auth?: Record<string, string>; project?: string; zone?: string },
2531
+ options?: {
2532
+ auth?: Record<string, string>;
2533
+ project?: string;
2534
+ zone?: string;
2535
+ assistantName?: string;
2536
+ },
2585
2537
  ): ChatAppInstance {
2586
2538
  let chatHandle: ChatAppHandle | null = null;
2587
2539
 
@@ -2589,6 +2541,7 @@ export function renderChatApp(
2589
2541
  <ChatApp
2590
2542
  runtimeUrl={runtimeUrl}
2591
2543
  assistantId={assistantId}
2544
+ assistantName={options?.assistantName}
2592
2545
  species={species}
2593
2546
  auth={options?.auth}
2594
2547
  project={options?.project}
@@ -67,6 +67,10 @@ export interface ContainerInfo {
67
67
 
68
68
  export interface AssistantEntry {
69
69
  assistantId: string;
70
+ /** Platform-provided display name, when available. */
71
+ name?: string;
72
+ /** Older lockfile key for the display name, if present. */
73
+ assistantName?: string;
70
74
  runtimeUrl: string;
71
75
  /** Loopback URL for same-machine health checks (e.g. `http://127.0.0.1:7831`).
72
76
  * Avoids mDNS resolution issues when the machine checks its own gateway. */
@@ -572,5 +576,3 @@ export function getLockfilePlatformBaseUrl(): string | undefined {
572
576
  if (typeof url === "string" && url.trim()) return url.trim();
573
577
  return undefined;
574
578
  }
575
-
576
-