@vellumai/cli 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/package.json +1 -1
- package/src/__tests__/default-main-screen-formatting.test.ts +166 -0
- package/src/components/DefaultMainScreen.tsx +32 -267
- package/src/lib/default-main-screen-formatting.ts +264 -0
- package/src/lib/provider-secrets.ts +10 -0
- package/src/shared/provider-env-vars.ts +2 -0
|
@@ -3,18 +3,30 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import {
|
|
4
4
|
buildShellInvocation,
|
|
5
5
|
buildShellSpawnFlags,
|
|
6
|
+
CHILD_OOM_SCORE_ADJ,
|
|
6
7
|
pathListDelimiter,
|
|
7
8
|
prependUniquePathEntries,
|
|
8
9
|
} from "./shell.js";
|
|
9
10
|
|
|
10
11
|
describe("buildShellInvocation", () => {
|
|
11
12
|
test("uses Bash on POSIX hosts", () => {
|
|
12
|
-
expect(buildShellInvocation("printf hello", "
|
|
13
|
+
expect(buildShellInvocation("printf hello", "darwin")).toEqual({
|
|
13
14
|
command: "bash",
|
|
14
15
|
args: ["-c", "--", "printf hello"],
|
|
15
16
|
});
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
test("raises the shell's OOM-kill priority on Linux", () => {
|
|
20
|
+
expect(buildShellInvocation("printf hello", "linux")).toEqual({
|
|
21
|
+
command: "bash",
|
|
22
|
+
args: [
|
|
23
|
+
"-c",
|
|
24
|
+
"--",
|
|
25
|
+
`echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; printf hello`,
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
test("uses encoded non-interactive PowerShell on Windows", () => {
|
|
19
31
|
const invocation = buildShellInvocation(
|
|
20
32
|
"Write-Output 'hello 世界'",
|
|
@@ -24,6 +24,23 @@ if (-not $__vellumCommandSucceeded) {
|
|
|
24
24
|
}
|
|
25
25
|
exit 0`;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* `oom_score_adj` for assistant-owned tool processes. Any positive value
|
|
29
|
+
* outranks a 0-scored process; 1000 makes a tool the kernel's first choice
|
|
30
|
+
* whenever the container runs out of memory, so the assistant itself is never
|
|
31
|
+
* the victim of a runaway command.
|
|
32
|
+
*/
|
|
33
|
+
export const CHILD_OOM_SCORE_ADJ = 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Prefix for a Bash `-c` script that raises the shell's own OOM-kill priority
|
|
37
|
+
* before the command runs. Every process the command forks inherits it. Bash
|
|
38
|
+
* performs a builtin's redirection itself, so `/proc/self` is the shell.
|
|
39
|
+
* Redirections apply left to right, so stderr goes to /dev/null before the
|
|
40
|
+
* procfs open can fail and complain about it.
|
|
41
|
+
*/
|
|
42
|
+
const LINUX_OOM_SCORE_ADJ_PREFIX = `echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; `;
|
|
43
|
+
|
|
27
44
|
export function buildShellInvocation(
|
|
28
45
|
command: string,
|
|
29
46
|
hostPlatform: NodeJS.Platform = process.platform,
|
|
@@ -45,7 +62,11 @@ export function buildShellInvocation(
|
|
|
45
62
|
],
|
|
46
63
|
};
|
|
47
64
|
}
|
|
48
|
-
|
|
65
|
+
const script =
|
|
66
|
+
hostPlatform === "linux"
|
|
67
|
+
? `${LINUX_OOM_SCORE_ADJ_PREFIX}${command}`
|
|
68
|
+
: command;
|
|
69
|
+
return { command: "bash", args: ["-c", "--", script] };
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
/**
|
package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/shell.test.ts
CHANGED
|
@@ -3,18 +3,30 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import {
|
|
4
4
|
buildShellInvocation,
|
|
5
5
|
buildShellSpawnFlags,
|
|
6
|
+
CHILD_OOM_SCORE_ADJ,
|
|
6
7
|
pathListDelimiter,
|
|
7
8
|
prependUniquePathEntries,
|
|
8
9
|
} from "./shell.js";
|
|
9
10
|
|
|
10
11
|
describe("buildShellInvocation", () => {
|
|
11
12
|
test("uses Bash on POSIX hosts", () => {
|
|
12
|
-
expect(buildShellInvocation("printf hello", "
|
|
13
|
+
expect(buildShellInvocation("printf hello", "darwin")).toEqual({
|
|
13
14
|
command: "bash",
|
|
14
15
|
args: ["-c", "--", "printf hello"],
|
|
15
16
|
});
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
test("raises the shell's OOM-kill priority on Linux", () => {
|
|
20
|
+
expect(buildShellInvocation("printf hello", "linux")).toEqual({
|
|
21
|
+
command: "bash",
|
|
22
|
+
args: [
|
|
23
|
+
"-c",
|
|
24
|
+
"--",
|
|
25
|
+
`echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; printf hello`,
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
test("uses encoded non-interactive PowerShell on Windows", () => {
|
|
19
31
|
const invocation = buildShellInvocation(
|
|
20
32
|
"Write-Output 'hello 世界'",
|
|
@@ -24,6 +24,23 @@ if (-not $__vellumCommandSucceeded) {
|
|
|
24
24
|
}
|
|
25
25
|
exit 0`;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* `oom_score_adj` for assistant-owned tool processes. Any positive value
|
|
29
|
+
* outranks a 0-scored process; 1000 makes a tool the kernel's first choice
|
|
30
|
+
* whenever the container runs out of memory, so the assistant itself is never
|
|
31
|
+
* the victim of a runaway command.
|
|
32
|
+
*/
|
|
33
|
+
export const CHILD_OOM_SCORE_ADJ = 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Prefix for a Bash `-c` script that raises the shell's own OOM-kill priority
|
|
37
|
+
* before the command runs. Every process the command forks inherits it. Bash
|
|
38
|
+
* performs a builtin's redirection itself, so `/proc/self` is the shell.
|
|
39
|
+
* Redirections apply left to right, so stderr goes to /dev/null before the
|
|
40
|
+
* procfs open can fail and complain about it.
|
|
41
|
+
*/
|
|
42
|
+
const LINUX_OOM_SCORE_ADJ_PREFIX = `echo ${CHILD_OOM_SCORE_ADJ} 2>/dev/null >/proc/self/oom_score_adj; `;
|
|
43
|
+
|
|
27
44
|
export function buildShellInvocation(
|
|
28
45
|
command: string,
|
|
29
46
|
hostPlatform: NodeJS.Platform = process.platform,
|
|
@@ -45,7 +62,11 @@ export function buildShellInvocation(
|
|
|
45
62
|
],
|
|
46
63
|
};
|
|
47
64
|
}
|
|
48
|
-
|
|
65
|
+
const script =
|
|
66
|
+
hostPlatform === "linux"
|
|
67
|
+
? `${LINUX_OOM_SCORE_ADJ_PREFIX}${command}`
|
|
68
|
+
: command;
|
|
69
|
+
return { command: "bash", args: ["-c", "--", script] };
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
/**
|
package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/package.json
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
29
|
"./platform-credential": "./src/platform-credential.ts",
|
|
30
30
|
"./plan-credit": "./src/plan-credit.ts",
|
|
31
|
-
"./integration-categories": "./src/integration-categories.ts"
|
|
31
|
+
"./integration-categories": "./src/integration-categories.ts",
|
|
32
|
+
"./mcp-oauth": "./src/mcp-oauth.ts"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildMcpOAuthCallbackUrl,
|
|
5
|
+
buildMcpOAuthClientMetadata,
|
|
6
|
+
buildMcpOAuthClientMetadataUrl,
|
|
7
|
+
} from "../mcp-oauth.js";
|
|
8
|
+
|
|
9
|
+
describe("MCP OAuth client metadata", () => {
|
|
10
|
+
test("builds the public client document", () => {
|
|
11
|
+
expect(
|
|
12
|
+
buildMcpOAuthClientMetadata({
|
|
13
|
+
clientId:
|
|
14
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
15
|
+
redirectUris: [
|
|
16
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
17
|
+
],
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({
|
|
20
|
+
client_id:
|
|
21
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
22
|
+
redirect_uris: [
|
|
23
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
24
|
+
],
|
|
25
|
+
token_endpoint_auth_method: "none",
|
|
26
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
27
|
+
response_types: ["code"],
|
|
28
|
+
client_name: "Vellum Assistant",
|
|
29
|
+
logo_uri: "https://www.vellum.ai/favicon.svg",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("builds callback and metadata URLs from one normalized base", () => {
|
|
34
|
+
const baseUrl = "https://velay.example/assistant-123///";
|
|
35
|
+
expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
|
|
36
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
37
|
+
);
|
|
38
|
+
expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
|
|
39
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("canonicalizes equivalent public base URL spellings", () => {
|
|
44
|
+
const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
|
|
45
|
+
|
|
46
|
+
expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
|
|
47
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
48
|
+
);
|
|
49
|
+
expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
|
|
50
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export * from "./ingress.js";
|
|
|
30
30
|
export * from "./no-response.js";
|
|
31
31
|
export * from "./plan-credit.js";
|
|
32
32
|
export * from "./integration-categories.js";
|
|
33
|
+
export * from "./mcp-oauth.js";
|
|
33
34
|
export * from "./platform-credential.js";
|
|
34
35
|
export * from "./remote-web-pairing.js";
|
|
35
36
|
export * from "./twilio-ingress.js";
|
package/node_modules/@vellumai/local-mode/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
|
|
2
|
+
|
|
3
|
+
export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
|
|
4
|
+
export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
|
|
5
|
+
export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
|
|
6
|
+
|
|
7
|
+
export interface McpOAuthClientMetadata {
|
|
8
|
+
client_id?: string;
|
|
9
|
+
redirect_uris: string[];
|
|
10
|
+
token_endpoint_auth_method: "none";
|
|
11
|
+
grant_types: ["authorization_code", "refresh_token"];
|
|
12
|
+
response_types: ["code"];
|
|
13
|
+
client_name: string;
|
|
14
|
+
logo_uri: string;
|
|
15
|
+
software_id?: string;
|
|
16
|
+
software_version?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BuildMcpOAuthClientMetadataOptions {
|
|
20
|
+
clientId?: string;
|
|
21
|
+
redirectUris: string[];
|
|
22
|
+
clientName?: string;
|
|
23
|
+
softwareId?: string;
|
|
24
|
+
softwareVersion?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildMcpOAuthClientMetadata(
|
|
28
|
+
options: BuildMcpOAuthClientMetadataOptions,
|
|
29
|
+
): McpOAuthClientMetadata {
|
|
30
|
+
return {
|
|
31
|
+
...(options.clientId && { client_id: options.clientId }),
|
|
32
|
+
redirect_uris: options.redirectUris,
|
|
33
|
+
token_endpoint_auth_method: "none",
|
|
34
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
35
|
+
response_types: ["code"],
|
|
36
|
+
client_name: options.clientName ?? "Vellum Assistant",
|
|
37
|
+
logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
|
|
38
|
+
...(options.softwareId && { software_id: options.softwareId }),
|
|
39
|
+
...(options.softwareVersion && {
|
|
40
|
+
software_version: options.softwareVersion,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
|
|
46
|
+
return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
|
|
50
|
+
return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeBaseUrl(publicBaseUrl: string): string {
|
|
54
|
+
const normalized =
|
|
55
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
29
|
"./platform-credential": "./src/platform-credential.ts",
|
|
30
30
|
"./plan-credit": "./src/plan-credit.ts",
|
|
31
|
-
"./integration-categories": "./src/integration-categories.ts"
|
|
31
|
+
"./integration-categories": "./src/integration-categories.ts",
|
|
32
|
+
"./mcp-oauth": "./src/mcp-oauth.ts"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildMcpOAuthCallbackUrl,
|
|
5
|
+
buildMcpOAuthClientMetadata,
|
|
6
|
+
buildMcpOAuthClientMetadataUrl,
|
|
7
|
+
} from "../mcp-oauth.js";
|
|
8
|
+
|
|
9
|
+
describe("MCP OAuth client metadata", () => {
|
|
10
|
+
test("builds the public client document", () => {
|
|
11
|
+
expect(
|
|
12
|
+
buildMcpOAuthClientMetadata({
|
|
13
|
+
clientId:
|
|
14
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
15
|
+
redirectUris: [
|
|
16
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
17
|
+
],
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({
|
|
20
|
+
client_id:
|
|
21
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
22
|
+
redirect_uris: [
|
|
23
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
24
|
+
],
|
|
25
|
+
token_endpoint_auth_method: "none",
|
|
26
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
27
|
+
response_types: ["code"],
|
|
28
|
+
client_name: "Vellum Assistant",
|
|
29
|
+
logo_uri: "https://www.vellum.ai/favicon.svg",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("builds callback and metadata URLs from one normalized base", () => {
|
|
34
|
+
const baseUrl = "https://velay.example/assistant-123///";
|
|
35
|
+
expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
|
|
36
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
37
|
+
);
|
|
38
|
+
expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
|
|
39
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("canonicalizes equivalent public base URL spellings", () => {
|
|
44
|
+
const baseUrl = " https://VELAY.EXAMPLE:443/a/../assistant-123/// ";
|
|
45
|
+
|
|
46
|
+
expect(buildMcpOAuthCallbackUrl(baseUrl)).toBe(
|
|
47
|
+
"https://velay.example/assistant-123/webhooks/oauth/callback",
|
|
48
|
+
);
|
|
49
|
+
expect(buildMcpOAuthClientMetadataUrl(baseUrl)).toBe(
|
|
50
|
+
"https://velay.example/assistant-123/oauth/client-metadata.json",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -30,6 +30,7 @@ export * from "./ingress.js";
|
|
|
30
30
|
export * from "./no-response.js";
|
|
31
31
|
export * from "./plan-credit.js";
|
|
32
32
|
export * from "./integration-categories.js";
|
|
33
|
+
export * from "./mcp-oauth.js";
|
|
33
34
|
export * from "./platform-credential.js";
|
|
34
35
|
export * from "./remote-web-pairing.js";
|
|
35
36
|
export * from "./twilio-ingress.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "./ingress.js";
|
|
2
|
+
|
|
3
|
+
export const MCP_OAUTH_CALLBACK_PATH = "/webhooks/oauth/callback";
|
|
4
|
+
export const MCP_OAUTH_CLIENT_METADATA_PATH = "/oauth/client-metadata.json";
|
|
5
|
+
export const MCP_OAUTH_CLIENT_LOGO_URI = "https://www.vellum.ai/favicon.svg";
|
|
6
|
+
|
|
7
|
+
export interface McpOAuthClientMetadata {
|
|
8
|
+
client_id?: string;
|
|
9
|
+
redirect_uris: string[];
|
|
10
|
+
token_endpoint_auth_method: "none";
|
|
11
|
+
grant_types: ["authorization_code", "refresh_token"];
|
|
12
|
+
response_types: ["code"];
|
|
13
|
+
client_name: string;
|
|
14
|
+
logo_uri: string;
|
|
15
|
+
software_id?: string;
|
|
16
|
+
software_version?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BuildMcpOAuthClientMetadataOptions {
|
|
20
|
+
clientId?: string;
|
|
21
|
+
redirectUris: string[];
|
|
22
|
+
clientName?: string;
|
|
23
|
+
softwareId?: string;
|
|
24
|
+
softwareVersion?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildMcpOAuthClientMetadata(
|
|
28
|
+
options: BuildMcpOAuthClientMetadataOptions,
|
|
29
|
+
): McpOAuthClientMetadata {
|
|
30
|
+
return {
|
|
31
|
+
...(options.clientId && { client_id: options.clientId }),
|
|
32
|
+
redirect_uris: options.redirectUris,
|
|
33
|
+
token_endpoint_auth_method: "none",
|
|
34
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
35
|
+
response_types: ["code"],
|
|
36
|
+
client_name: options.clientName ?? "Vellum Assistant",
|
|
37
|
+
logo_uri: MCP_OAUTH_CLIENT_LOGO_URI,
|
|
38
|
+
...(options.softwareId && { software_id: options.softwareId }),
|
|
39
|
+
...(options.softwareVersion && {
|
|
40
|
+
software_version: options.softwareVersion,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildMcpOAuthCallbackUrl(publicBaseUrl: string): string {
|
|
46
|
+
return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CALLBACK_PATH}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildMcpOAuthClientMetadataUrl(publicBaseUrl: string): string {
|
|
50
|
+
return `${normalizeBaseUrl(publicBaseUrl)}${MCP_OAUTH_CLIENT_METADATA_PATH}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeBaseUrl(publicBaseUrl: string): string {
|
|
54
|
+
const normalized =
|
|
55
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
throw new Error("MCP OAuth public base URL must be an absolute HTTP URL");
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
calculateHeaderHeight,
|
|
4
|
+
estimateItemHeight,
|
|
5
|
+
formatConfirmationPreview,
|
|
6
|
+
formatHeaderEyebrow,
|
|
7
|
+
formatHeaderTitle,
|
|
8
|
+
formatTimestamp,
|
|
9
|
+
formatToolCallPreview,
|
|
10
|
+
stripAnsi,
|
|
11
|
+
truncateValue,
|
|
12
|
+
type RuntimeMessage,
|
|
13
|
+
} from "../lib/default-main-screen-formatting.js";
|
|
14
|
+
|
|
15
|
+
function message(overrides: Partial<RuntimeMessage> = {}): RuntimeMessage {
|
|
16
|
+
return {
|
|
17
|
+
id: "message-1",
|
|
18
|
+
role: "user",
|
|
19
|
+
content: "Hello",
|
|
20
|
+
timestamp: "2026-09-29T16:00:00.000Z",
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("formatTimestamp", () => {
|
|
26
|
+
test("formats a timestamp with hour and minute fields", () => {
|
|
27
|
+
const timestamp = "2026-09-29T16:05:00.000Z";
|
|
28
|
+
|
|
29
|
+
expect(formatTimestamp(timestamp)).toBe(
|
|
30
|
+
new Date(timestamp).toLocaleTimeString([], {
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("tool-call previews", () => {
|
|
39
|
+
test("formats known tool inputs", () => {
|
|
40
|
+
expect(
|
|
41
|
+
formatToolCallPreview({
|
|
42
|
+
name: "file_read",
|
|
43
|
+
input: { path: "/workspace/report.txt" },
|
|
44
|
+
}),
|
|
45
|
+
).toBe("read /workspace/report.txt");
|
|
46
|
+
expect(
|
|
47
|
+
formatToolCallPreview({
|
|
48
|
+
name: "browser_click",
|
|
49
|
+
input: { element_id: "submit-button" },
|
|
50
|
+
}),
|
|
51
|
+
).toBe("click submit-button");
|
|
52
|
+
expect(
|
|
53
|
+
formatConfirmationPreview("browser_press_key", { key: "Enter" }),
|
|
54
|
+
).toBe('press "Enter"');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("truncates long preview fields at their existing limits", () => {
|
|
58
|
+
expect(
|
|
59
|
+
formatToolCallPreview({
|
|
60
|
+
name: "bash",
|
|
61
|
+
input: { command: "x".repeat(90) },
|
|
62
|
+
}),
|
|
63
|
+
).toBe("x".repeat(80));
|
|
64
|
+
expect(
|
|
65
|
+
formatToolCallPreview({
|
|
66
|
+
name: "browser_type",
|
|
67
|
+
input: { selector: "y".repeat(70) },
|
|
68
|
+
}),
|
|
69
|
+
).toBe(`type into ${"y".repeat(60)}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("serializes unknown tool inputs", () => {
|
|
73
|
+
expect(
|
|
74
|
+
formatToolCallPreview({ name: "custom_tool", input: { value: 7 } }),
|
|
75
|
+
).toBe('{"value":7}');
|
|
76
|
+
expect(formatConfirmationPreview("custom_tool", { value: 7 })).toBe(
|
|
77
|
+
'custom_tool: {"value":7}',
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("truncateValue", () => {
|
|
83
|
+
test("returns values that fit without changing them", () => {
|
|
84
|
+
expect(truncateValue("short", 8)).toBe("short");
|
|
85
|
+
expect(truncateValue({ ok: true }, 20)).toBe('{"ok":true}');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("adds an ellipsis within the requested maximum length", () => {
|
|
89
|
+
expect(truncateValue("abcdefghij", 8)).toBe("abcde...");
|
|
90
|
+
expect(truncateValue({ value: "abcdefghij" }, 12)).toBe('{"value":...');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("header formatting", () => {
|
|
95
|
+
test("uses the default title and eyebrow", () => {
|
|
96
|
+
expect(formatHeaderEyebrow()).toBe(" Assistant");
|
|
97
|
+
expect(formatHeaderTitle()).toBe(" Meet your Assistant!");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("normalizes title whitespace", () => {
|
|
101
|
+
expect(formatHeaderTitle(" Example\n Assistant ")).toBe(
|
|
102
|
+
" Example Assistant",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("truncates titles to the left panel width", () => {
|
|
107
|
+
const title = formatHeaderTitle("A".repeat(40));
|
|
108
|
+
|
|
109
|
+
expect(title).toBe(` ${"A".repeat(31)}...`);
|
|
110
|
+
expect(title).toHaveLength(36);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("strips ANSI color and style sequences from header art", () => {
|
|
114
|
+
expect(stripAnsi("\x1b[31mred\x1b[0m plain")).toBe("red plain");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("estimateItemHeight", () => {
|
|
119
|
+
test("accounts for message prefixes, wrapping, and spacing", () => {
|
|
120
|
+
expect(estimateItemHeight(message({ content: "abcdefghij" }), 10)).toBe(4);
|
|
121
|
+
expect(estimateItemHeight(message({ content: "a\nb" }), 80)).toBe(3);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("accounts for tool-call chrome, parameters, and results", () => {
|
|
125
|
+
expect(
|
|
126
|
+
estimateItemHeight(
|
|
127
|
+
message({
|
|
128
|
+
role: "assistant",
|
|
129
|
+
toolCalls: [
|
|
130
|
+
{
|
|
131
|
+
name: "file_read",
|
|
132
|
+
input: { path: "/workspace/a", max_chars: 100 },
|
|
133
|
+
result: "done",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
80,
|
|
138
|
+
),
|
|
139
|
+
).toBe(7);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("estimates non-message feed items", () => {
|
|
143
|
+
expect(estimateItemHeight({ type: "help" }, 80)).toBe(5);
|
|
144
|
+
expect(estimateItemHeight({ type: "status", text: "123456\n12" }, 5)).toBe(
|
|
145
|
+
3,
|
|
146
|
+
);
|
|
147
|
+
expect(estimateItemHeight({ type: "spinner", text: "Working" }, 80)).toBe(
|
|
148
|
+
1,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("calculateHeaderHeight", () => {
|
|
154
|
+
test("uses one row in compact terminals", () => {
|
|
155
|
+
expect(calculateHeaderHeight("vellum", 59)).toBe(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("uses the larger of the art and right-panel layouts", () => {
|
|
159
|
+
expect(calculateHeaderHeight("vellum", 60)).toBe(16);
|
|
160
|
+
expect(calculateHeaderHeight("openclaw", 80)).toBe(19);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("defaults to the standard terminal width", () => {
|
|
164
|
+
expect(calculateHeaderHeight("vellum")).toBe(16);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -21,6 +21,35 @@ import { trustedRefreshUrl } from "../lib/runtime-url";
|
|
|
21
21
|
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
22
22
|
import { tuiLog } from "../lib/tui-log";
|
|
23
23
|
import { segmentsToPlainText } from "../lib/segments-to-plain-text";
|
|
24
|
+
import {
|
|
25
|
+
calculateHeaderHeight,
|
|
26
|
+
COMPACT_THRESHOLD,
|
|
27
|
+
DEFAULT_TERMINAL_COLUMNS,
|
|
28
|
+
estimateItemHeight,
|
|
29
|
+
formatConfirmationPreview,
|
|
30
|
+
formatHeaderEyebrow,
|
|
31
|
+
formatHeaderTitle,
|
|
32
|
+
formatTimestamp,
|
|
33
|
+
isRuntimeMessage,
|
|
34
|
+
formatToolCallPreview,
|
|
35
|
+
stripAnsi,
|
|
36
|
+
truncateValue,
|
|
37
|
+
HELP_COMMANDS,
|
|
38
|
+
LEFT_PANEL_WIDTH,
|
|
39
|
+
RIGHT_PANEL_LINE_COUNT,
|
|
40
|
+
TIPS,
|
|
41
|
+
type ErrorLine,
|
|
42
|
+
type FeedItem,
|
|
43
|
+
type HelpLine,
|
|
44
|
+
type RuntimeMessage,
|
|
45
|
+
type StatusLine,
|
|
46
|
+
type ToolCallInfo,
|
|
47
|
+
} from "../lib/default-main-screen-formatting.js";
|
|
48
|
+
export {
|
|
49
|
+
formatTimestamp,
|
|
50
|
+
type RuntimeMessage,
|
|
51
|
+
type ToolCallInfo,
|
|
52
|
+
} from "../lib/default-main-screen-formatting.js";
|
|
24
53
|
import { statusEmoji, withStatusEmoji } from "../lib/status-emoji";
|
|
25
54
|
import {
|
|
26
55
|
getTerminalCapabilities,
|
|
@@ -50,25 +79,6 @@ export const SLASH_COMMANDS = [
|
|
|
50
79
|
"/quit",
|
|
51
80
|
];
|
|
52
81
|
|
|
53
|
-
const HELP_COMMANDS = [
|
|
54
|
-
{
|
|
55
|
-
command: "/btw <question>",
|
|
56
|
-
description: "Ask a side question while the assistant is working",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
command: "/quit, /exit, /q",
|
|
60
|
-
description: "Disconnect and exit",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
command: "/clear",
|
|
64
|
-
description: "Clear the screen",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
command: "/help, ?",
|
|
68
|
-
description: "Show this help",
|
|
69
|
-
},
|
|
70
|
-
] as const;
|
|
71
|
-
|
|
72
82
|
const SEND_TIMEOUT_MS = 5000;
|
|
73
83
|
/** Fresh deadline for a request retried after a mid-session token refresh —
|
|
74
84
|
* the original caller signal may have already timed out during the refresh. */
|
|
@@ -76,37 +86,13 @@ const RETRY_TIMEOUT_MS = 30_000;
|
|
|
76
86
|
|
|
77
87
|
// ── Layout constants ──────────────────────────────────────
|
|
78
88
|
const MAX_TOTAL_WIDTH = 72;
|
|
79
|
-
const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
80
89
|
const DEFAULT_TERMINAL_ROWS = 24;
|
|
81
|
-
const LEFT_PANEL_WIDTH = 36;
|
|
82
|
-
|
|
83
|
-
const COMPACT_THRESHOLD = 60;
|
|
84
90
|
const HEADER_PREFIX_UNICODE = "── Vellum ";
|
|
85
91
|
const HEADER_PREFIX_ASCII = "-- Vellum ";
|
|
86
92
|
|
|
87
93
|
// Left panel structure: HEADER lines + art + FOOTER lines
|
|
88
|
-
const LEFT_HEADER_LINES = 4; // spacer + eyebrow + title + spacer
|
|
89
|
-
const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
|
|
90
|
-
|
|
91
|
-
// Right panel structure
|
|
92
|
-
const TIPS = [
|
|
93
|
-
"Send a message to start chatting",
|
|
94
|
-
"Use /help to see available commands",
|
|
95
|
-
];
|
|
96
|
-
const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant ID, Species, Status — each with heading + value
|
|
97
|
-
const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
|
|
98
|
-
const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
99
|
-
const RIGHT_PANEL_LINE_COUNT =
|
|
100
|
-
RIGHT_PANEL_SPACERS +
|
|
101
|
-
RIGHT_PANEL_TIPS_HEADING +
|
|
102
|
-
TIPS.length +
|
|
103
|
-
RIGHT_PANEL_INFO_SECTIONS * 2;
|
|
104
|
-
|
|
105
94
|
// Header chrome (borders around panel content)
|
|
106
95
|
const HEADER_TOP_BORDER_LINES = 1; // "── Vellum ───..." line
|
|
107
|
-
const HEADER_BOTTOM_BORDER_LINES = 2; // bottom rule + blank line
|
|
108
|
-
const HEADER_CHROME_LINES =
|
|
109
|
-
HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
|
|
110
96
|
|
|
111
97
|
// Selection / Secret windows
|
|
112
98
|
const DIALOG_WINDOW_WIDTH = 60;
|
|
@@ -123,11 +109,6 @@ const SECRET_INPUT_HEIGHT = 5; // title bar + content row + bottom border + tool
|
|
|
123
109
|
const SPINNER_HEIGHT = 1;
|
|
124
110
|
const MIN_FEED_ROWS = 3;
|
|
125
111
|
|
|
126
|
-
// Feed item height estimation
|
|
127
|
-
const TOOL_CALL_CHROME_LINES = 2; // header (┌) + footer (└)
|
|
128
|
-
const MESSAGE_SPACING = 1;
|
|
129
|
-
const HELP_DISPLAY_HEIGHT = HELP_COMMANDS.length + 1;
|
|
130
|
-
|
|
131
112
|
interface ListMessagesResponse {
|
|
132
113
|
messages: RuntimeMessage[];
|
|
133
114
|
nextCursor?: string;
|
|
@@ -497,38 +478,6 @@ async function* streamEvents(
|
|
|
497
478
|
}
|
|
498
479
|
}
|
|
499
480
|
|
|
500
|
-
function formatConfirmationPreview(
|
|
501
|
-
toolName: string,
|
|
502
|
-
input: Record<string, unknown>,
|
|
503
|
-
): string {
|
|
504
|
-
switch (toolName) {
|
|
505
|
-
case "bash":
|
|
506
|
-
return String(input.command ?? "");
|
|
507
|
-
case "file_read":
|
|
508
|
-
return `read ${input.path ?? ""}`;
|
|
509
|
-
case "file_write":
|
|
510
|
-
return `write ${input.path ?? ""}`;
|
|
511
|
-
case "file_edit":
|
|
512
|
-
return `edit ${input.path ?? ""}`;
|
|
513
|
-
case "web_fetch":
|
|
514
|
-
return String(input.url ?? "").slice(0, 80);
|
|
515
|
-
case "browser_navigate":
|
|
516
|
-
return `navigate ${String(input.url ?? "").slice(0, 80)}`;
|
|
517
|
-
case "browser_close":
|
|
518
|
-
return input.close_all_pages
|
|
519
|
-
? "close all browser pages"
|
|
520
|
-
: "close browser page";
|
|
521
|
-
case "browser_click":
|
|
522
|
-
return `click ${input.element_id ?? input.selector ?? ""}`;
|
|
523
|
-
case "browser_type":
|
|
524
|
-
return `type into ${input.element_id ?? input.selector ?? ""}`;
|
|
525
|
-
case "browser_press_key":
|
|
526
|
-
return `press "${input.key ?? ""}"`;
|
|
527
|
-
default:
|
|
528
|
-
return `${toolName}: ${JSON.stringify(input).slice(0, 80)}`;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
481
|
async function handleConfirmationPrompt(
|
|
533
482
|
baseUrl: string,
|
|
534
483
|
assistantId: string,
|
|
@@ -567,93 +516,6 @@ export const TYPING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "
|
|
|
567
516
|
/** ASCII-safe spinner frames for the connection screen. */
|
|
568
517
|
const CONNECTION_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
569
518
|
|
|
570
|
-
export interface ToolCallInfo {
|
|
571
|
-
name: string;
|
|
572
|
-
input: Record<string, unknown>;
|
|
573
|
-
result?: string;
|
|
574
|
-
isError?: boolean;
|
|
575
|
-
toolUseId?: string;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
export interface RuntimeMessage {
|
|
579
|
-
id: string;
|
|
580
|
-
role: "user" | "assistant";
|
|
581
|
-
content: string;
|
|
582
|
-
/**
|
|
583
|
-
* Ordered text segments from the daemon's history payload, split at
|
|
584
|
-
* tool_use/surface boundaries. The flat `content` body is derived from
|
|
585
|
-
* these (see `segmentsToPlainText`); the daemon no longer sends a
|
|
586
|
-
* redundant flattened `content` field on the wire.
|
|
587
|
-
*/
|
|
588
|
-
textSegments?: string[];
|
|
589
|
-
timestamp: string;
|
|
590
|
-
toolCalls?: ToolCallInfo[];
|
|
591
|
-
label?: string;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export function formatTimestamp(ts: string): string {
|
|
595
|
-
try {
|
|
596
|
-
const date = new Date(ts);
|
|
597
|
-
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
598
|
-
} catch {
|
|
599
|
-
return "";
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function formatToolCallPreview(tc: ToolCallInfo): string {
|
|
604
|
-
switch (tc.name) {
|
|
605
|
-
case "bash":
|
|
606
|
-
return String(tc.input.command ?? "").slice(0, 80);
|
|
607
|
-
case "file_read":
|
|
608
|
-
return `read ${tc.input.path ?? ""}`;
|
|
609
|
-
case "file_write":
|
|
610
|
-
return `write ${tc.input.path ?? ""}`;
|
|
611
|
-
case "file_edit":
|
|
612
|
-
return `edit ${tc.input.path ?? ""}`;
|
|
613
|
-
case "web_search":
|
|
614
|
-
return String(tc.input.query ?? "").slice(0, 80);
|
|
615
|
-
case "web_fetch":
|
|
616
|
-
return String(tc.input.url ?? "").slice(0, 80);
|
|
617
|
-
case "browser_navigate":
|
|
618
|
-
return `navigate ${String(tc.input.url ?? "").slice(0, 80)}`;
|
|
619
|
-
case "browser_click":
|
|
620
|
-
return `click ${String(tc.input.element_id ?? tc.input.selector ?? "").slice(0, 60)}`;
|
|
621
|
-
case "browser_type":
|
|
622
|
-
return `type into ${String(tc.input.element_id ?? tc.input.selector ?? "").slice(0, 60)}`;
|
|
623
|
-
default:
|
|
624
|
-
return JSON.stringify(tc.input).slice(0, 80);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function truncateValue(value: unknown, maxLen: number): string {
|
|
629
|
-
if (typeof value === "string") {
|
|
630
|
-
if (value.length > maxLen) {
|
|
631
|
-
return value.slice(0, maxLen - 3) + "...";
|
|
632
|
-
}
|
|
633
|
-
return value;
|
|
634
|
-
}
|
|
635
|
-
const serialized = JSON.stringify(value);
|
|
636
|
-
if (serialized.length > maxLen) {
|
|
637
|
-
return serialized.slice(0, maxLen - 3) + "...";
|
|
638
|
-
}
|
|
639
|
-
return serialized;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function formatHeaderTitle(assistantName?: string): string {
|
|
643
|
-
const rawTitle = assistantName?.trim() || "Meet your Assistant!";
|
|
644
|
-
const title = rawTitle.replace(/\s+/g, " ");
|
|
645
|
-
const maxTitleLength = LEFT_PANEL_WIDTH - 2;
|
|
646
|
-
const displayTitle =
|
|
647
|
-
title.length > maxTitleLength
|
|
648
|
-
? title.slice(0, maxTitleLength - 3) + "..."
|
|
649
|
-
: title;
|
|
650
|
-
return ` ${displayTitle}`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function formatHeaderEyebrow(): string {
|
|
654
|
-
return " Assistant";
|
|
655
|
-
}
|
|
656
|
-
|
|
657
519
|
interface ToolCallDisplayProps {
|
|
658
520
|
tc: ToolCallInfo;
|
|
659
521
|
}
|
|
@@ -822,10 +684,6 @@ export function renderErrorMainScreen(error: unknown): number {
|
|
|
822
684
|
return 3;
|
|
823
685
|
}
|
|
824
686
|
|
|
825
|
-
function stripAnsi(str: string): string {
|
|
826
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
827
|
-
}
|
|
828
|
-
|
|
829
687
|
interface DefaultMainScreenProps {
|
|
830
688
|
runtimeUrl: string;
|
|
831
689
|
assistantId: string;
|
|
@@ -1018,96 +876,6 @@ export interface SelectionRequest {
|
|
|
1018
876
|
resolve: (index: number) => void;
|
|
1019
877
|
}
|
|
1020
878
|
|
|
1021
|
-
interface StatusLine {
|
|
1022
|
-
type: "status";
|
|
1023
|
-
text: string;
|
|
1024
|
-
color?: string;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
interface SpinnerLine {
|
|
1028
|
-
type: "spinner";
|
|
1029
|
-
text: string;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
interface HelpLine {
|
|
1033
|
-
type: "help";
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
interface ErrorLine {
|
|
1037
|
-
type: "error";
|
|
1038
|
-
text: string;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
type FeedItem =
|
|
1042
|
-
| RuntimeMessage
|
|
1043
|
-
| StatusLine
|
|
1044
|
-
| SpinnerLine
|
|
1045
|
-
| HelpLine
|
|
1046
|
-
| ErrorLine;
|
|
1047
|
-
|
|
1048
|
-
function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
|
|
1049
|
-
return "role" in item;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
|
|
1053
|
-
if (isRuntimeMessage(item)) {
|
|
1054
|
-
const cols = Math.max(1, terminalColumns);
|
|
1055
|
-
// Account for "HH:MM AM Label: " prefix on the first line
|
|
1056
|
-
const defaultLabel = item.role === "user" ? "You:" : "Assistant:";
|
|
1057
|
-
const label = item.label ?? defaultLabel;
|
|
1058
|
-
const prefixLen = 10 + label.length + 1; // timestamp + space + label + space
|
|
1059
|
-
let lines = 0;
|
|
1060
|
-
const contentLines = item.content.split("\n");
|
|
1061
|
-
for (let idx = 0; idx < contentLines.length; idx++) {
|
|
1062
|
-
const lineLen =
|
|
1063
|
-
idx === 0
|
|
1064
|
-
? contentLines[idx].length + prefixLen
|
|
1065
|
-
: contentLines[idx].length;
|
|
1066
|
-
lines += Math.max(1, Math.ceil(lineLen / cols));
|
|
1067
|
-
}
|
|
1068
|
-
if (item.role === "assistant" && item.toolCalls) {
|
|
1069
|
-
for (const tc of item.toolCalls) {
|
|
1070
|
-
const paramCount =
|
|
1071
|
-
typeof tc.input === "object" && tc.input
|
|
1072
|
-
? Object.keys(tc.input).length
|
|
1073
|
-
: 0;
|
|
1074
|
-
lines +=
|
|
1075
|
-
TOOL_CALL_CHROME_LINES +
|
|
1076
|
-
paramCount +
|
|
1077
|
-
(tc.result !== undefined ? 1 : 0);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
return lines + MESSAGE_SPACING;
|
|
1081
|
-
}
|
|
1082
|
-
if (item.type === "help") {
|
|
1083
|
-
return HELP_DISPLAY_HEIGHT;
|
|
1084
|
-
}
|
|
1085
|
-
if (item.type === "status" || item.type === "error") {
|
|
1086
|
-
const cols = Math.max(1, terminalColumns);
|
|
1087
|
-
let lines = 0;
|
|
1088
|
-
for (const line of item.text.split("\n")) {
|
|
1089
|
-
lines += Math.max(1, Math.ceil(line.length / cols));
|
|
1090
|
-
}
|
|
1091
|
-
return lines;
|
|
1092
|
-
}
|
|
1093
|
-
return 1;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const COMPACT_HEADER_HEIGHT = 1;
|
|
1097
|
-
|
|
1098
|
-
function calculateHeaderHeight(
|
|
1099
|
-
species: Species,
|
|
1100
|
-
terminalColumns?: number,
|
|
1101
|
-
): number {
|
|
1102
|
-
if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
|
|
1103
|
-
return COMPACT_HEADER_HEIGHT;
|
|
1104
|
-
}
|
|
1105
|
-
const artLength = SPECIES_CONFIG[species].art.length;
|
|
1106
|
-
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
1107
|
-
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
1108
|
-
return maxLines + HEADER_CHROME_LINES;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
879
|
const SCROLL_STEP = 5;
|
|
1112
880
|
|
|
1113
881
|
export function render(
|
|
@@ -1118,10 +886,7 @@ export function render(
|
|
|
1118
886
|
): number {
|
|
1119
887
|
const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1120
888
|
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
const leftLineCount = LEFT_HEADER_LINES + art.length + LEFT_FOOTER_LINES;
|
|
1124
|
-
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
889
|
+
const headerHeight = calculateHeaderHeight(species, terminalColumns);
|
|
1125
890
|
|
|
1126
891
|
const { unmount } = inkRender(
|
|
1127
892
|
<DefaultMainScreen
|
|
@@ -1135,7 +900,7 @@ export function render(
|
|
|
1135
900
|
unmount();
|
|
1136
901
|
|
|
1137
902
|
if (isCompact) {
|
|
1138
|
-
return
|
|
903
|
+
return headerHeight;
|
|
1139
904
|
}
|
|
1140
905
|
|
|
1141
906
|
const statusCanvasLine = RIGHT_PANEL_LINE_COUNT + HEADER_TOP_BORDER_LINES;
|
|
@@ -1151,7 +916,7 @@ export function render(
|
|
|
1151
916
|
})
|
|
1152
917
|
.catch(() => {});
|
|
1153
918
|
|
|
1154
|
-
return
|
|
919
|
+
return headerHeight;
|
|
1155
920
|
}
|
|
1156
921
|
|
|
1157
922
|
interface SelectionWindowProps {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { SPECIES_CONFIG, type Species } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export const LEFT_PANEL_WIDTH = 36;
|
|
4
|
+
export const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
5
|
+
export const COMPACT_THRESHOLD = 60;
|
|
6
|
+
|
|
7
|
+
const LEFT_HEADER_LINES = 4;
|
|
8
|
+
const LEFT_FOOTER_LINES = 3;
|
|
9
|
+
const HEADER_TOP_BORDER_LINES = 1;
|
|
10
|
+
const HEADER_BOTTOM_BORDER_LINES = 2;
|
|
11
|
+
const HEADER_CHROME_LINES =
|
|
12
|
+
HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
|
|
13
|
+
const COMPACT_HEADER_HEIGHT = 1;
|
|
14
|
+
|
|
15
|
+
const RIGHT_PANEL_INFO_SECTIONS = 3;
|
|
16
|
+
const RIGHT_PANEL_SPACERS = 2;
|
|
17
|
+
const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
18
|
+
|
|
19
|
+
const TOOL_CALL_CHROME_LINES = 2;
|
|
20
|
+
const MESSAGE_SPACING = 1;
|
|
21
|
+
|
|
22
|
+
export const HELP_COMMANDS = [
|
|
23
|
+
{
|
|
24
|
+
command: "/btw <question>",
|
|
25
|
+
description: "Ask a side question while the assistant is working",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: "/quit, /exit, /q",
|
|
29
|
+
description: "Disconnect and exit",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
command: "/clear",
|
|
33
|
+
description: "Clear the screen",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: "/help, ?",
|
|
37
|
+
description: "Show this help",
|
|
38
|
+
},
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export const TIPS = [
|
|
42
|
+
"Send a message to start chatting",
|
|
43
|
+
"Use /help to see available commands",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
export const RIGHT_PANEL_LINE_COUNT =
|
|
47
|
+
RIGHT_PANEL_SPACERS +
|
|
48
|
+
RIGHT_PANEL_TIPS_HEADING +
|
|
49
|
+
TIPS.length +
|
|
50
|
+
RIGHT_PANEL_INFO_SECTIONS * 2;
|
|
51
|
+
const HELP_DISPLAY_HEIGHT = HELP_COMMANDS.length + 1;
|
|
52
|
+
|
|
53
|
+
export interface ToolCallInfo {
|
|
54
|
+
name: string;
|
|
55
|
+
input: Record<string, unknown>;
|
|
56
|
+
result?: string;
|
|
57
|
+
isError?: boolean;
|
|
58
|
+
toolUseId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RuntimeMessage {
|
|
62
|
+
id: string;
|
|
63
|
+
role: "user" | "assistant";
|
|
64
|
+
content: string;
|
|
65
|
+
/**
|
|
66
|
+
* Ordered text segments from the daemon's history payload, split at
|
|
67
|
+
* tool_use/surface boundaries. The flat `content` body is derived from
|
|
68
|
+
* these (see `segmentsToPlainText`); the daemon no longer sends a
|
|
69
|
+
* redundant flattened `content` field on the wire.
|
|
70
|
+
*/
|
|
71
|
+
textSegments?: string[];
|
|
72
|
+
timestamp: string;
|
|
73
|
+
toolCalls?: ToolCallInfo[];
|
|
74
|
+
label?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface StatusLine {
|
|
78
|
+
type: "status";
|
|
79
|
+
text: string;
|
|
80
|
+
color?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SpinnerLine {
|
|
84
|
+
type: "spinner";
|
|
85
|
+
text: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface HelpLine {
|
|
89
|
+
type: "help";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ErrorLine {
|
|
93
|
+
type: "error";
|
|
94
|
+
text: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type FeedItem =
|
|
98
|
+
| RuntimeMessage
|
|
99
|
+
| StatusLine
|
|
100
|
+
| SpinnerLine
|
|
101
|
+
| HelpLine
|
|
102
|
+
| ErrorLine;
|
|
103
|
+
|
|
104
|
+
export function formatTimestamp(timestamp: string): string {
|
|
105
|
+
try {
|
|
106
|
+
const date = new Date(timestamp);
|
|
107
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
108
|
+
} catch {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatConfirmationPreview(
|
|
114
|
+
toolName: string,
|
|
115
|
+
input: Record<string, unknown>,
|
|
116
|
+
): string {
|
|
117
|
+
switch (toolName) {
|
|
118
|
+
case "bash":
|
|
119
|
+
return String(input.command ?? "");
|
|
120
|
+
case "file_read":
|
|
121
|
+
return `read ${input.path ?? ""}`;
|
|
122
|
+
case "file_write":
|
|
123
|
+
return `write ${input.path ?? ""}`;
|
|
124
|
+
case "file_edit":
|
|
125
|
+
return `edit ${input.path ?? ""}`;
|
|
126
|
+
case "web_fetch":
|
|
127
|
+
return String(input.url ?? "").slice(0, 80);
|
|
128
|
+
case "browser_navigate":
|
|
129
|
+
return `navigate ${String(input.url ?? "").slice(0, 80)}`;
|
|
130
|
+
case "browser_close":
|
|
131
|
+
return input.close_all_pages
|
|
132
|
+
? "close all browser pages"
|
|
133
|
+
: "close browser page";
|
|
134
|
+
case "browser_click":
|
|
135
|
+
return `click ${input.element_id ?? input.selector ?? ""}`;
|
|
136
|
+
case "browser_type":
|
|
137
|
+
return `type into ${input.element_id ?? input.selector ?? ""}`;
|
|
138
|
+
case "browser_press_key":
|
|
139
|
+
return `press "${input.key ?? ""}"`;
|
|
140
|
+
default:
|
|
141
|
+
return `${toolName}: ${JSON.stringify(input).slice(0, 80)}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatToolCallPreview(toolCall: ToolCallInfo): string {
|
|
146
|
+
switch (toolCall.name) {
|
|
147
|
+
case "bash":
|
|
148
|
+
return String(toolCall.input.command ?? "").slice(0, 80);
|
|
149
|
+
case "file_read":
|
|
150
|
+
return `read ${toolCall.input.path ?? ""}`;
|
|
151
|
+
case "file_write":
|
|
152
|
+
return `write ${toolCall.input.path ?? ""}`;
|
|
153
|
+
case "file_edit":
|
|
154
|
+
return `edit ${toolCall.input.path ?? ""}`;
|
|
155
|
+
case "web_search":
|
|
156
|
+
return String(toolCall.input.query ?? "").slice(0, 80);
|
|
157
|
+
case "web_fetch":
|
|
158
|
+
return String(toolCall.input.url ?? "").slice(0, 80);
|
|
159
|
+
case "browser_navigate":
|
|
160
|
+
return `navigate ${String(toolCall.input.url ?? "").slice(0, 80)}`;
|
|
161
|
+
case "browser_click":
|
|
162
|
+
return `click ${String(toolCall.input.element_id ?? toolCall.input.selector ?? "").slice(0, 60)}`;
|
|
163
|
+
case "browser_type":
|
|
164
|
+
return `type into ${String(toolCall.input.element_id ?? toolCall.input.selector ?? "").slice(0, 60)}`;
|
|
165
|
+
default:
|
|
166
|
+
return JSON.stringify(toolCall.input).slice(0, 80);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function truncateValue(value: unknown, maxLength: number): string {
|
|
171
|
+
if (typeof value === "string") {
|
|
172
|
+
if (value.length > maxLength) {
|
|
173
|
+
return value.slice(0, maxLength - 3) + "...";
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
const serialized = JSON.stringify(value);
|
|
178
|
+
if (serialized.length > maxLength) {
|
|
179
|
+
return serialized.slice(0, maxLength - 3) + "...";
|
|
180
|
+
}
|
|
181
|
+
return serialized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function formatHeaderTitle(assistantName?: string): string {
|
|
185
|
+
const rawTitle = assistantName?.trim() || "Meet your Assistant!";
|
|
186
|
+
const title = rawTitle.replace(/\s+/g, " ");
|
|
187
|
+
const maxTitleLength = LEFT_PANEL_WIDTH - 2;
|
|
188
|
+
const displayTitle =
|
|
189
|
+
title.length > maxTitleLength
|
|
190
|
+
? title.slice(0, maxTitleLength - 3) + "..."
|
|
191
|
+
: title;
|
|
192
|
+
return ` ${displayTitle}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatHeaderEyebrow(): string {
|
|
196
|
+
return " Assistant";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function stripAnsi(value: string): string {
|
|
200
|
+
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
|
|
204
|
+
return "role" in item;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function estimateItemHeight(
|
|
208
|
+
item: FeedItem,
|
|
209
|
+
terminalColumns: number,
|
|
210
|
+
): number {
|
|
211
|
+
if (isRuntimeMessage(item)) {
|
|
212
|
+
const columns = Math.max(1, terminalColumns);
|
|
213
|
+
const defaultLabel = item.role === "user" ? "You:" : "Assistant:";
|
|
214
|
+
const label = item.label ?? defaultLabel;
|
|
215
|
+
const prefixLength = 10 + label.length + 1;
|
|
216
|
+
let lines = 0;
|
|
217
|
+
const contentLines = item.content.split("\n");
|
|
218
|
+
for (let index = 0; index < contentLines.length; index++) {
|
|
219
|
+
const lineLength =
|
|
220
|
+
index === 0
|
|
221
|
+
? contentLines[index].length + prefixLength
|
|
222
|
+
: contentLines[index].length;
|
|
223
|
+
lines += Math.max(1, Math.ceil(lineLength / columns));
|
|
224
|
+
}
|
|
225
|
+
if (item.role === "assistant" && item.toolCalls) {
|
|
226
|
+
for (const toolCall of item.toolCalls) {
|
|
227
|
+
const parameterCount =
|
|
228
|
+
typeof toolCall.input === "object" && toolCall.input
|
|
229
|
+
? Object.keys(toolCall.input).length
|
|
230
|
+
: 0;
|
|
231
|
+
lines +=
|
|
232
|
+
TOOL_CALL_CHROME_LINES +
|
|
233
|
+
parameterCount +
|
|
234
|
+
(toolCall.result !== undefined ? 1 : 0);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return lines + MESSAGE_SPACING;
|
|
238
|
+
}
|
|
239
|
+
if (item.type === "help") {
|
|
240
|
+
return HELP_DISPLAY_HEIGHT;
|
|
241
|
+
}
|
|
242
|
+
if (item.type === "status" || item.type === "error") {
|
|
243
|
+
const columns = Math.max(1, terminalColumns);
|
|
244
|
+
let lines = 0;
|
|
245
|
+
for (const line of item.text.split("\n")) {
|
|
246
|
+
lines += Math.max(1, Math.ceil(line.length / columns));
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function calculateHeaderHeight(
|
|
254
|
+
species: Species,
|
|
255
|
+
terminalColumns = DEFAULT_TERMINAL_COLUMNS,
|
|
256
|
+
): number {
|
|
257
|
+
if (terminalColumns < COMPACT_THRESHOLD) {
|
|
258
|
+
return COMPACT_HEADER_HEIGHT;
|
|
259
|
+
}
|
|
260
|
+
const artLength = SPECIES_CONFIG[species].art.length;
|
|
261
|
+
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
262
|
+
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
263
|
+
return maxLines + HEADER_CHROME_LINES;
|
|
264
|
+
}
|
|
@@ -76,6 +76,7 @@ const PROVIDER_LABELS: Record<LlmProviderId, string> = {
|
|
|
76
76
|
litellm: "LiteLLM",
|
|
77
77
|
opencode: "OpenCode",
|
|
78
78
|
baseten: "Baseten",
|
|
79
|
+
ionet: "IO Intelligence",
|
|
79
80
|
poolside: "Poolside",
|
|
80
81
|
typesafe: "TypeSafe",
|
|
81
82
|
};
|
|
@@ -96,6 +97,7 @@ export const HATCH_PROVIDER_CHOICES: readonly LlmProviderId[] = [
|
|
|
96
97
|
"minimax",
|
|
97
98
|
"atlascloud",
|
|
98
99
|
"baseten",
|
|
100
|
+
"ionet",
|
|
99
101
|
"poolside",
|
|
100
102
|
];
|
|
101
103
|
|
|
@@ -229,6 +231,14 @@ export function inferProviderFromModel(model: string): string | undefined {
|
|
|
229
231
|
// AI Gateway does not carry these IDs.
|
|
230
232
|
return "openrouter";
|
|
231
233
|
}
|
|
234
|
+
if (
|
|
235
|
+
model === "meta-llama/Llama-3.3-70B-Instruct" ||
|
|
236
|
+
model === "openai/gpt-oss-20b"
|
|
237
|
+
) {
|
|
238
|
+
// IO Intelligence (io.net) exclusives; the remaining meta-llama/ IDs
|
|
239
|
+
// belong to OpenRouter.
|
|
240
|
+
return "ionet";
|
|
241
|
+
}
|
|
232
242
|
if (model.startsWith("openai/") || model.startsWith("xai/")) {
|
|
233
243
|
return "vercel-ai-gateway";
|
|
234
244
|
}
|
|
@@ -33,6 +33,7 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
|
|
|
33
33
|
litellm: "LITELLM_API_KEY",
|
|
34
34
|
opencode: "OPENCODE_API_KEY",
|
|
35
35
|
baseten: "BASETEN_API_KEY",
|
|
36
|
+
ionet: "IONET_API_KEY",
|
|
36
37
|
poolside: "POOLSIDE_API_KEY",
|
|
37
38
|
typesafe: "TYPESAFE_API_KEY",
|
|
38
39
|
};
|
|
@@ -48,6 +49,7 @@ export const SEARCH_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
|
|
|
48
49
|
searxng: "SEARXNG_API_KEY",
|
|
49
50
|
tinyfish: "TINYFISH_API_KEY",
|
|
50
51
|
exa: "EXA_API_KEY",
|
|
52
|
+
serply: "SERPLY_API_KEY",
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
/**
|