@vellumai/assistant 0.12.2 → 0.12.3-staging.1
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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
2
|
+
import { desktopAutomationLease } from "./desktop-automation-lease.js";
|
|
3
|
+
import { isVirtualDesktopEnabled } from "./virtual-desktop-feature.js";
|
|
4
|
+
|
|
5
|
+
export async function prepareDesktopHelp(
|
|
6
|
+
context: ToolContext,
|
|
7
|
+
): Promise<ToolExecutionResult | ((resume: boolean) => Promise<void>)> {
|
|
8
|
+
if (!isVirtualDesktopEnabled()) {
|
|
9
|
+
return {
|
|
10
|
+
content: "Virtual desktop help is unavailable on this assistant.",
|
|
11
|
+
isError: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return desktopAutomationLease.reserveForHuman(context);
|
|
15
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
5
|
+
import { afterAll, describe, expect, mock, spyOn, test } from "bun:test";
|
|
5
6
|
|
|
7
|
+
import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
|
|
6
8
|
import { newFakeDesktop, newViewer, settle } from "./__tests__/fake-desktop.js";
|
|
7
9
|
import { DesktopDependencyInstaller } from "./desktop-dependencies.js";
|
|
8
10
|
import {
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
getDesktopSessionManager,
|
|
14
16
|
} from "./desktop-session-manager.js";
|
|
15
17
|
import { DesktopStreamBridge } from "./desktop-stream-bridge.js";
|
|
18
|
+
import { isVirtualDesktopEnabled } from "./virtual-desktop-feature.js";
|
|
16
19
|
|
|
17
20
|
const profileDir = mkdtempSync(join(tmpdir(), "desktop-bridge-test-"));
|
|
18
21
|
afterAll(() => {
|
|
@@ -87,6 +90,63 @@ function slotIsFree(manager: DesktopSessionManager): boolean {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
describe("DesktopStreamBridge", () => {
|
|
93
|
+
test("streams without flag or filesystem checks after connecting", async () => {
|
|
94
|
+
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
95
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
96
|
+
const h = newFakeDesktop({ profileDir });
|
|
97
|
+
const isEnabled = mock(isVirtualDesktopEnabled);
|
|
98
|
+
const b = newBridge(h.manager, async () => {}, isEnabled);
|
|
99
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
100
|
+
process.env.IS_PLATFORM = "true";
|
|
101
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
102
|
+
try {
|
|
103
|
+
b.connectNow();
|
|
104
|
+
await b.bridge.start();
|
|
105
|
+
expect(isEnabled).toHaveBeenCalled();
|
|
106
|
+
isEnabled.mockClear();
|
|
107
|
+
const stat = spyOn(fs, "statSync");
|
|
108
|
+
const read = spyOn(fs, "readFileSync");
|
|
109
|
+
try {
|
|
110
|
+
const frame = new Uint8Array([1, 2, 3]);
|
|
111
|
+
for (let i = 0; i < 100; i++) {
|
|
112
|
+
b.bridge.handleClientFrame(frame);
|
|
113
|
+
b.tcp.handlers.onData(frame);
|
|
114
|
+
b.tcp.handlers.onDrain();
|
|
115
|
+
}
|
|
116
|
+
expect(b.tcp.writes).toHaveLength(100);
|
|
117
|
+
expect(b.ws.sent).toHaveLength(100);
|
|
118
|
+
setOverridesForTesting({ "assistant-desktop": false });
|
|
119
|
+
b.bridge.handleClientFrame(frame);
|
|
120
|
+
b.tcp.handlers.onData(frame);
|
|
121
|
+
b.tcp.handlers.onDrain();
|
|
122
|
+
expect(b.tcp.writes).toHaveLength(101);
|
|
123
|
+
expect(b.ws.sent).toHaveLength(101);
|
|
124
|
+
expect(b.ws.closeCode).toBeNull();
|
|
125
|
+
expect(b.tcp.ended).toBe(false);
|
|
126
|
+
expect(isEnabled).not.toHaveBeenCalled();
|
|
127
|
+
expect(stat).not.toHaveBeenCalled();
|
|
128
|
+
expect(read).not.toHaveBeenCalled();
|
|
129
|
+
} finally {
|
|
130
|
+
stat.mockRestore();
|
|
131
|
+
read.mockRestore();
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
b.bridge.handleClose();
|
|
135
|
+
await h.manager.destroy();
|
|
136
|
+
setOverridesForTesting({});
|
|
137
|
+
if (originalContainerized === undefined) {
|
|
138
|
+
delete process.env.IS_CONTAINERIZED;
|
|
139
|
+
} else {
|
|
140
|
+
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
141
|
+
}
|
|
142
|
+
if (originalPlatform === undefined) {
|
|
143
|
+
delete process.env.IS_PLATFORM;
|
|
144
|
+
} else {
|
|
145
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
90
150
|
test("refuses a disabled desktop before installation or process startup", async () => {
|
|
91
151
|
const h = newFakeDesktop({ profileDir });
|
|
92
152
|
let installs = 0;
|
|
@@ -124,24 +184,19 @@ describe("DesktopStreamBridge", () => {
|
|
|
124
184
|
});
|
|
125
185
|
|
|
126
186
|
for (const direction of ["viewer", "desktop"] as const) {
|
|
127
|
-
test(`
|
|
187
|
+
test(`ignores ${direction} frames after the stream closes`, async () => {
|
|
128
188
|
const h = newFakeDesktop({ profileDir });
|
|
129
|
-
|
|
130
|
-
const b = newBridge(
|
|
131
|
-
h.manager,
|
|
132
|
-
async () => {},
|
|
133
|
-
() => enabled,
|
|
134
|
-
);
|
|
189
|
+
const b = newBridge(h.manager);
|
|
135
190
|
b.connectNow();
|
|
136
191
|
await b.bridge.start();
|
|
137
|
-
|
|
192
|
+
b.bridge.handleClose();
|
|
138
193
|
const frame = new Uint8Array([1, 2, 3]);
|
|
139
194
|
if (direction === "viewer") {
|
|
140
195
|
b.bridge.handleClientFrame(frame);
|
|
141
196
|
} else {
|
|
142
197
|
b.tcp.handlers.onData(frame);
|
|
143
198
|
}
|
|
144
|
-
expect(b.ws.closeCode).
|
|
199
|
+
expect(b.ws.closeCode).toBeNull();
|
|
145
200
|
expect(b.ws.sent).toHaveLength(0);
|
|
146
201
|
expect(b.tcp.writes).toHaveLength(0);
|
|
147
202
|
expect(b.tcp.ended).toBe(true);
|
|
@@ -102,7 +102,7 @@ export class DesktopStreamBridge {
|
|
|
102
102
|
try {
|
|
103
103
|
tcp = await this.connect(DESKTOP_VNC_PORT, {
|
|
104
104
|
onData: (data) => {
|
|
105
|
-
if (
|
|
105
|
+
if (this.closed) {
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
if (this.ws.send(data) === 0) {
|
|
@@ -128,7 +128,7 @@ export class DesktopStreamBridge {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
handleClientFrame(message: string | Uint8Array | ArrayBuffer): void {
|
|
131
|
-
if (
|
|
131
|
+
if (this.closed || typeof message === "string") {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
const bytes =
|
|
@@ -148,7 +148,7 @@ export class DesktopStreamBridge {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
private flush(): void {
|
|
151
|
-
if (
|
|
151
|
+
if (this.closed) {
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
const tcp = this.tcp;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readAvatarState } from "../avatar/avatar-manifest.js";
|
|
2
|
+
import { readContainedAvatarRaster } from "../avatar/ensure-raster.js";
|
|
3
|
+
import {
|
|
4
|
+
renderNotificationAvatarPng,
|
|
5
|
+
resolveNotificationAccentHex,
|
|
6
|
+
} from "../avatar/notification-avatar.js";
|
|
7
|
+
import { renderCharacterPng } from "../avatar/png-renderer.js";
|
|
8
|
+
import { getResvg, isResvgAvailable } from "../avatar/resvg-lazy.js";
|
|
9
|
+
import { isTemplatePlaceholder } from "../daemon/handlers/identity.js";
|
|
10
|
+
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
11
|
+
import { getLogger } from "../util/logger.js";
|
|
12
|
+
import { getAvatarImagePath } from "../util/platform.js";
|
|
13
|
+
import { escapeXmlContent } from "../util/xml.js";
|
|
14
|
+
|
|
15
|
+
function wallpaperWordmark(
|
|
16
|
+
width: number,
|
|
17
|
+
height: number,
|
|
18
|
+
name: string | null,
|
|
19
|
+
): string {
|
|
20
|
+
const normalized = name?.replace(/\s+/g, " ").trim();
|
|
21
|
+
const label =
|
|
22
|
+
normalized && !isTemplatePlaceholder(normalized) ? normalized : "Vellum";
|
|
23
|
+
const text = `<text id="wordmark" font-family="Liberation Sans, Arial, sans-serif" font-size="64" font-weight="700" letter-spacing="1.5">${escapeXmlContent(`${label} OS`)}</text>`;
|
|
24
|
+
const Resvg = getResvg();
|
|
25
|
+
const bounds = new Resvg(
|
|
26
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${text}</svg>`,
|
|
27
|
+
).getBBox();
|
|
28
|
+
if (!bounds || bounds.width === 0) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
const depth = 6;
|
|
32
|
+
const scale = Math.min(
|
|
33
|
+
(height * 0.06) / 64,
|
|
34
|
+
(width * 0.72) / (bounds.width + depth),
|
|
35
|
+
);
|
|
36
|
+
const x = width / 2 - (bounds.x + (bounds.width + depth) / 2) * scale;
|
|
37
|
+
const layers = Array.from({ length: depth }, (_, index) => {
|
|
38
|
+
const offset = depth - index;
|
|
39
|
+
return `<use href="#wordmark" transform="translate(${offset} ${offset})" fill="#40535b"/>`;
|
|
40
|
+
}).join("");
|
|
41
|
+
return `<defs>
|
|
42
|
+
${text}
|
|
43
|
+
<linearGradient id="letter-face" x2="0" y2="1">
|
|
44
|
+
<stop stop-color="#ffffff"/><stop offset=".48" stop-color="#e0e9e7"/>
|
|
45
|
+
<stop offset="1" stop-color="#9eb4ba"/>
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<g transform="translate(${x} ${height * 0.69}) scale(${scale})">
|
|
49
|
+
<use href="#wordmark" transform="translate(9 12)" fill="#080f14" opacity=".35"/>
|
|
50
|
+
${layers}
|
|
51
|
+
<use href="#wordmark" fill="url(#letter-face)" stroke="#e8f1ed" stroke-opacity=".4" stroke-width=".5"/>
|
|
52
|
+
</g>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function renderDesktopWallpaper(
|
|
56
|
+
width: number,
|
|
57
|
+
height: number,
|
|
58
|
+
avatar: Buffer | null,
|
|
59
|
+
accentHex: string | null,
|
|
60
|
+
assistantName: string | null = null,
|
|
61
|
+
): Buffer {
|
|
62
|
+
const accent = /^#[\da-f]{6}$/i.test(accentHex ?? "")
|
|
63
|
+
? accentHex!
|
|
64
|
+
: "#91a6b0";
|
|
65
|
+
const cx = width / 2;
|
|
66
|
+
const cy = height * 0.46;
|
|
67
|
+
const size = Math.min(width, height) * 0.24;
|
|
68
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
69
|
+
<defs>
|
|
70
|
+
<linearGradient id="base" x2="1" y2="1">
|
|
71
|
+
<stop stop-color="#202a30"/><stop offset="1" stop-color="#10171c"/>
|
|
72
|
+
</linearGradient>
|
|
73
|
+
<radialGradient id="glow">
|
|
74
|
+
<stop stop-color="${accent}" stop-opacity=".24"/>
|
|
75
|
+
<stop offset="1" stop-color="${accent}" stop-opacity="0"/>
|
|
76
|
+
</radialGradient>
|
|
77
|
+
<radialGradient id="shadow">
|
|
78
|
+
<stop stop-color="#000" stop-opacity=".25"/>
|
|
79
|
+
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
|
80
|
+
</radialGradient>
|
|
81
|
+
</defs>
|
|
82
|
+
<path fill="url(#base)" d="M0 0H${width}V${height}H0z"/>
|
|
83
|
+
<ellipse cx="${cx}" cy="${cy}" rx="${height * 0.7}" ry="${height * 0.6}" fill="url(#glow)"/>
|
|
84
|
+
<g fill="none" stroke="${accent}">
|
|
85
|
+
<circle cx="${cx}" cy="${cy}" r="${size * 0.76}" stroke-opacity=".16"/>
|
|
86
|
+
<circle cx="${cx}" cy="${cy}" r="${size * 1.25}" stroke-opacity=".09"/>
|
|
87
|
+
<circle cx="${cx}" cy="${cy}" r="${size * 1.9}" stroke-opacity=".05"/>
|
|
88
|
+
</g>
|
|
89
|
+
${
|
|
90
|
+
avatar
|
|
91
|
+
? `<ellipse cx="${cx}" cy="${cy + size * 0.48}" rx="${size * 0.7}" ry="${size * 0.16}" fill="url(#shadow)"/>
|
|
92
|
+
<image x="${cx - size / 2}" y="${cy - size / 2}" width="${size}" height="${size}" href="data:image/png;base64,${avatar.toString("base64")}"/>`
|
|
93
|
+
: ""
|
|
94
|
+
}
|
|
95
|
+
${wallpaperWordmark(width, height, assistantName)}
|
|
96
|
+
</svg>`;
|
|
97
|
+
const Resvg = getResvg();
|
|
98
|
+
return Buffer.from(new Resvg(svg).render().asPng());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function renderCurrentDesktopWallpaper(
|
|
102
|
+
width: number,
|
|
103
|
+
height: number,
|
|
104
|
+
): Promise<Buffer | null> {
|
|
105
|
+
if (!isResvgAvailable()) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const state = readAvatarState();
|
|
109
|
+
const name = getAssistantName();
|
|
110
|
+
let raster = readContainedAvatarRaster(getAvatarImagePath());
|
|
111
|
+
if (!raster && state.kind === "character" && state.traits) {
|
|
112
|
+
try {
|
|
113
|
+
raster = renderCharacterPng(
|
|
114
|
+
state.traits.bodyShape,
|
|
115
|
+
state.traits.eyeStyle,
|
|
116
|
+
state.traits.color,
|
|
117
|
+
);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
getLogger("desktop-wallpaper").warn(
|
|
120
|
+
{ err },
|
|
121
|
+
"Could not render wallpaper avatar",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return renderDesktopWallpaper(
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
raster ? await renderNotificationAvatarPng(state, raster) : null,
|
|
129
|
+
resolveNotificationAccentHex(state),
|
|
130
|
+
name,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { renderCurrentDesktopWallpaper } from "./desktop-wallpaper-renderer.js";
|
|
4
|
+
|
|
5
|
+
const [width, height, output] = process.argv.slice(2);
|
|
6
|
+
if (
|
|
7
|
+
!output ||
|
|
8
|
+
!Number.isFinite(Number(width)) ||
|
|
9
|
+
!Number.isFinite(Number(height))
|
|
10
|
+
) {
|
|
11
|
+
throw new Error("Wallpaper dimensions and output path are required");
|
|
12
|
+
}
|
|
13
|
+
const png = await renderCurrentDesktopWallpaper(Number(width), Number(height));
|
|
14
|
+
if (png) {
|
|
15
|
+
await writeFile(output, png);
|
|
16
|
+
}
|
|
17
|
+
process.exit(0);
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
2
9
|
import { tmpdir } from "node:os";
|
|
3
10
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
afterEach,
|
|
13
|
+
beforeEach,
|
|
14
|
+
expect,
|
|
15
|
+
mock,
|
|
16
|
+
setDefaultTimeout,
|
|
17
|
+
test,
|
|
18
|
+
} from "bun:test";
|
|
5
19
|
|
|
6
20
|
import sharp from "sharp";
|
|
7
21
|
|
|
8
22
|
import {
|
|
9
23
|
__resetResvgCacheForTests,
|
|
10
24
|
__setResvgCacheForTests,
|
|
25
|
+
getResvg,
|
|
11
26
|
} from "../avatar/resvg-lazy.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
import { renderCurrentDesktopWallpaper } from "./desktop-wallpaper.js";
|
|
28
|
+
import { renderDesktopWallpaper } from "./desktop-wallpaper-renderer.js";
|
|
29
|
+
|
|
30
|
+
// Native resvg renders scan the system font database per instance, and on a
|
|
31
|
+
// loaded CI runner the file's first render runs past bun's 5s default. Each
|
|
32
|
+
// test file runs in its own process, so this doesn't leak.
|
|
33
|
+
setDefaultTimeout(15_000);
|
|
16
34
|
|
|
17
35
|
let workspace: string;
|
|
18
36
|
let previousWorkspace: string | undefined;
|
|
@@ -70,7 +88,7 @@ test("renders the current uploaded avatar into an opaque desktop wallpaper", asy
|
|
|
70
88
|
expect((await sharp(result).stats()).isOpaque).toBe(true);
|
|
71
89
|
});
|
|
72
90
|
|
|
73
|
-
test("
|
|
91
|
+
test("renders a missing character raster without changing persisted avatar files", async () => {
|
|
74
92
|
manifest({
|
|
75
93
|
kind: "character",
|
|
76
94
|
source: "pool",
|
|
@@ -78,9 +96,18 @@ test("regenerates a character from current traits when its raster is missing", a
|
|
|
78
96
|
image: null,
|
|
79
97
|
accent: null,
|
|
80
98
|
});
|
|
99
|
+
const avatarDir = join(workspace, "data/avatar");
|
|
100
|
+
writeFileSync(join(avatarDir, "character-traits.json"), "preserve traits");
|
|
101
|
+
writeFileSync(join(avatarDir, "character-ascii.txt"), "preserve ASCII");
|
|
102
|
+
const snapshot = () =>
|
|
103
|
+
readdirSync(avatarDir)
|
|
104
|
+
.sort()
|
|
105
|
+
.map((file) => [file, readFileSync(join(avatarDir, file))]);
|
|
106
|
+
const before = snapshot();
|
|
81
107
|
const result = (await renderCurrentDesktopWallpaper(480, 300))!;
|
|
82
108
|
const empty = renderDesktopWallpaper(480, 300, null, null);
|
|
83
109
|
expect(await centerPixel(result)).not.toEqual(await centerPixel(empty));
|
|
110
|
+
expect(snapshot()).toEqual(before);
|
|
84
111
|
});
|
|
85
112
|
|
|
86
113
|
test("missing and unreadable avatars produce the same neutral background", async () => {
|
|
@@ -189,3 +216,17 @@ test("names containing XML are rendered as text without injecting SVG shapes", a
|
|
|
189
216
|
);
|
|
190
217
|
expect((await wordmarkPixels(png)).length).toBeGreaterThan(0);
|
|
191
218
|
});
|
|
219
|
+
|
|
220
|
+
test("renders in a child without invoking the assistant's native rasterizer", async () => {
|
|
221
|
+
const render = mock(() => {
|
|
222
|
+
throw new Error("Rendering must not run on the assistant event loop");
|
|
223
|
+
});
|
|
224
|
+
__setResvgCacheForTests({
|
|
225
|
+
available: true,
|
|
226
|
+
Resvg: render as unknown as ReturnType<typeof getResvg>,
|
|
227
|
+
});
|
|
228
|
+
const png = await renderCurrentDesktopWallpaper(1600, 900);
|
|
229
|
+
expect(render).not.toHaveBeenCalled();
|
|
230
|
+
const metadata = await sharp(png!).metadata();
|
|
231
|
+
expect([metadata.width, metadata.height]).toEqual([1600, 900]);
|
|
232
|
+
});
|
|
@@ -1,98 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "
|
|
6
|
-
import { getResvg, isResvgAvailable } from "../avatar/resvg-lazy.js";
|
|
7
|
-
import { isTemplatePlaceholder } from "../daemon/handlers/identity.js";
|
|
8
|
-
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
9
|
-
import { escapeXmlContent } from "../util/xml.js";
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
height: number,
|
|
14
|
-
name: string | null,
|
|
15
|
-
): string {
|
|
16
|
-
const normalized = name?.replace(/\s+/g, " ").trim();
|
|
17
|
-
const label =
|
|
18
|
-
normalized && !isTemplatePlaceholder(normalized) ? normalized : "Vellum";
|
|
19
|
-
const text = `<text id="wordmark" font-family="Liberation Sans, Arial, sans-serif" font-size="64" font-weight="700" letter-spacing="1.5">${escapeXmlContent(`${label} OS`)}</text>`;
|
|
20
|
-
const Resvg = getResvg();
|
|
21
|
-
const bounds = new Resvg(
|
|
22
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${text}</svg>`,
|
|
23
|
-
).getBBox();
|
|
24
|
-
if (!bounds || bounds.width === 0) {
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
const depth = 6;
|
|
28
|
-
const scale = Math.min(
|
|
29
|
-
(height * 0.06) / 64,
|
|
30
|
-
(width * 0.72) / (bounds.width + depth),
|
|
31
|
-
);
|
|
32
|
-
const x = width / 2 - (bounds.x + (bounds.width + depth) / 2) * scale;
|
|
33
|
-
const layers = Array.from({ length: depth }, (_, index) => {
|
|
34
|
-
const offset = depth - index;
|
|
35
|
-
return `<use href="#wordmark" transform="translate(${offset} ${offset})" fill="#40535b"/>`;
|
|
36
|
-
}).join("");
|
|
37
|
-
return `<defs>
|
|
38
|
-
${text}
|
|
39
|
-
<linearGradient id="letter-face" x2="0" y2="1">
|
|
40
|
-
<stop stop-color="#ffffff"/><stop offset=".48" stop-color="#e0e9e7"/>
|
|
41
|
-
<stop offset="1" stop-color="#9eb4ba"/>
|
|
42
|
-
</linearGradient>
|
|
43
|
-
</defs>
|
|
44
|
-
<g transform="translate(${x} ${height * 0.69}) scale(${scale})">
|
|
45
|
-
<use href="#wordmark" transform="translate(9 12)" fill="#080f14" opacity=".35"/>
|
|
46
|
-
${layers}
|
|
47
|
-
<use href="#wordmark" fill="url(#letter-face)" stroke="#e8f1ed" stroke-opacity=".4" stroke-width=".5"/>
|
|
48
|
-
</g>`;
|
|
49
|
-
}
|
|
7
|
+
import { isResvgAvailable } from "../avatar/resvg-lazy.js";
|
|
8
|
+
import { resolveWorkerCommand } from "../util/worker-process.js";
|
|
50
9
|
|
|
51
|
-
|
|
52
|
-
width: number,
|
|
53
|
-
height: number,
|
|
54
|
-
avatar: Buffer | null,
|
|
55
|
-
accentHex: string | null,
|
|
56
|
-
assistantName: string | null = null,
|
|
57
|
-
): Buffer {
|
|
58
|
-
const accent = /^#[\da-f]{6}$/i.test(accentHex ?? "")
|
|
59
|
-
? accentHex!
|
|
60
|
-
: "#91a6b0";
|
|
61
|
-
const cx = width / 2;
|
|
62
|
-
const cy = height * 0.46;
|
|
63
|
-
const size = Math.min(width, height) * 0.24;
|
|
64
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
65
|
-
<defs>
|
|
66
|
-
<linearGradient id="base" x2="1" y2="1">
|
|
67
|
-
<stop stop-color="#202a30"/><stop offset="1" stop-color="#10171c"/>
|
|
68
|
-
</linearGradient>
|
|
69
|
-
<radialGradient id="glow">
|
|
70
|
-
<stop stop-color="${accent}" stop-opacity=".24"/>
|
|
71
|
-
<stop offset="1" stop-color="${accent}" stop-opacity="0"/>
|
|
72
|
-
</radialGradient>
|
|
73
|
-
<radialGradient id="shadow">
|
|
74
|
-
<stop stop-color="#000" stop-opacity=".25"/>
|
|
75
|
-
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
|
76
|
-
</radialGradient>
|
|
77
|
-
</defs>
|
|
78
|
-
<path fill="url(#base)" d="M0 0H${width}V${height}H0z"/>
|
|
79
|
-
<ellipse cx="${cx}" cy="${cy}" rx="${height * 0.7}" ry="${height * 0.6}" fill="url(#glow)"/>
|
|
80
|
-
<g fill="none" stroke="${accent}">
|
|
81
|
-
<circle cx="${cx}" cy="${cy}" r="${size * 0.76}" stroke-opacity=".16"/>
|
|
82
|
-
<circle cx="${cx}" cy="${cy}" r="${size * 1.25}" stroke-opacity=".09"/>
|
|
83
|
-
<circle cx="${cx}" cy="${cy}" r="${size * 1.9}" stroke-opacity=".05"/>
|
|
84
|
-
</g>
|
|
85
|
-
${
|
|
86
|
-
avatar
|
|
87
|
-
? `<ellipse cx="${cx}" cy="${cy + size * 0.48}" rx="${size * 0.7}" ry="${size * 0.16}" fill="url(#shadow)"/>
|
|
88
|
-
<image x="${cx - size / 2}" y="${cy - size / 2}" width="${size}" height="${size}" href="data:image/png;base64,${avatar.toString("base64")}"/>`
|
|
89
|
-
: ""
|
|
90
|
-
}
|
|
91
|
-
${wallpaperWordmark(width, height, assistantName)}
|
|
92
|
-
</svg>`;
|
|
93
|
-
const Resvg = getResvg();
|
|
94
|
-
return Buffer.from(new Resvg(svg).render().asPng());
|
|
95
|
-
}
|
|
10
|
+
const runFile = promisify(execFile);
|
|
96
11
|
|
|
97
12
|
export async function renderCurrentDesktopWallpaper(
|
|
98
13
|
width: number,
|
|
@@ -101,13 +16,30 @@ export async function renderCurrentDesktopWallpaper(
|
|
|
101
16
|
if (!isResvgAvailable()) {
|
|
102
17
|
return null;
|
|
103
18
|
}
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
19
|
+
const directory = await mkdtemp(join(tmpdir(), "vellum-wallpaper-"));
|
|
20
|
+
const output = join(directory, "wallpaper.png");
|
|
21
|
+
try {
|
|
22
|
+
const [executable, ...args] = resolveWorkerCommand(
|
|
23
|
+
new URL("./desktop-wallpaper-worker.ts", import.meta.url),
|
|
24
|
+
undefined,
|
|
25
|
+
);
|
|
26
|
+
await runFile(
|
|
27
|
+
executable!,
|
|
28
|
+
[...args, String(width), String(height), output],
|
|
29
|
+
{
|
|
30
|
+
timeout: 30_000,
|
|
31
|
+
killSignal: "SIGKILL",
|
|
32
|
+
maxBuffer: 64 * 1024,
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
return await readFile(output).catch((error: NodeJS.ErrnoException) => {
|
|
37
|
+
if (error.code === "ENOENT") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
});
|
|
42
|
+
} finally {
|
|
43
|
+
await rm(directory, { recursive: true, force: true });
|
|
44
|
+
}
|
|
113
45
|
}
|
|
@@ -6,7 +6,7 @@ const VIRTUAL_DESKTOP_FLAG = "assistant-desktop" as const;
|
|
|
6
6
|
|
|
7
7
|
/** Gates desktop streaming and control to enabled, platform-hosted containers. */
|
|
8
8
|
export function isVirtualDesktopEnabled(
|
|
9
|
-
config
|
|
9
|
+
config?: AssistantConfig,
|
|
10
10
|
containerized: boolean = getIsContainerized(),
|
|
11
11
|
platformHosted: boolean = getIsPlatform(),
|
|
12
12
|
): boolean {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpServerManager.start() applies the shared fair cap after every
|
|
3
|
+
* configured server has been attempted in parallel. A slow earlier
|
|
4
|
+
* server must not prevent later ones from being listed, and later
|
|
5
|
+
* servers must keep tools when the global cap binds.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import type { ResolvedMcpConfig } from "../../config/schemas/mcp.js";
|
|
11
|
+
|
|
12
|
+
const toolsByServer = new Map<string, Array<{ name: string }>>();
|
|
13
|
+
const connectDelays = new Map<string, number>();
|
|
14
|
+
|
|
15
|
+
mock.module("../client.js", () => ({
|
|
16
|
+
McpClient: class {
|
|
17
|
+
constructor(readonly serverId: string) {}
|
|
18
|
+
get isConnected() {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
get lastError() {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
async connect() {
|
|
25
|
+
const delay = connectDelays.get(this.serverId) ?? 0;
|
|
26
|
+
if (delay > 0) {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async listTools() {
|
|
31
|
+
return (toolsByServer.get(this.serverId) ?? []).map((tool) => ({
|
|
32
|
+
name: tool.name,
|
|
33
|
+
description: `${this.serverId} ${tool.name}`,
|
|
34
|
+
inputSchema: { type: "object", properties: {} },
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
async disconnect() {}
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { McpServerManager } = await import("../manager.js");
|
|
42
|
+
|
|
43
|
+
function httpServer(): ResolvedMcpConfig["servers"][string] {
|
|
44
|
+
return {
|
|
45
|
+
source: "workspace",
|
|
46
|
+
transport: {
|
|
47
|
+
type: "streamable-http",
|
|
48
|
+
url: "https://example.invalid/mcp",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function configWith(ids: string[]): ResolvedMcpConfig {
|
|
54
|
+
return {
|
|
55
|
+
servers: Object.fromEntries(ids.map((id) => [id, httpServer()])),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("McpServerManager tool selection", () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
toolsByServer.clear();
|
|
62
|
+
connectDelays.clear();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("later servers keep tools when eight servers exceed the global cap", async () => {
|
|
66
|
+
const ids = Array.from(
|
|
67
|
+
{ length: 8 },
|
|
68
|
+
(_, i) => `server-${String(i + 1).padStart(2, "0")}`,
|
|
69
|
+
);
|
|
70
|
+
for (const id of ids) {
|
|
71
|
+
toolsByServer.set(
|
|
72
|
+
id,
|
|
73
|
+
Array.from({ length: 10 }, (_, i) => ({ name: `tool_${i}` })),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const manager = new McpServerManager();
|
|
78
|
+
const started = await manager.start(configWith(ids));
|
|
79
|
+
|
|
80
|
+
expect(started.discoveredToolCount).toBe(80);
|
|
81
|
+
expect(started.keptToolCount).toBe(50);
|
|
82
|
+
expect(started.truncatedServerIds.length).toBeGreaterThan(0);
|
|
83
|
+
expect(
|
|
84
|
+
started.servers.every((server) => server.tools.length > 0),
|
|
85
|
+
"Fair selection must not empty servers 6-8 merely because they were last.",
|
|
86
|
+
).toBe(true);
|
|
87
|
+
await manager.stop();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a slow earlier server does not prevent later servers from connecting", async () => {
|
|
91
|
+
toolsByServer.set("slow", [{ name: "slow_tool" }]);
|
|
92
|
+
toolsByServer.set("fast", [{ name: "fast_tool" }]);
|
|
93
|
+
connectDelays.set("slow", 40);
|
|
94
|
+
|
|
95
|
+
const manager = new McpServerManager();
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
const started = await manager.start(configWith(["slow", "fast"]));
|
|
98
|
+
const elapsed = Date.now() - startedAt;
|
|
99
|
+
|
|
100
|
+
expect(started.connectedServerCount).toBe(2);
|
|
101
|
+
expect(started.servers.map((server) => server.serverId).sort()).toEqual([
|
|
102
|
+
"fast",
|
|
103
|
+
"slow",
|
|
104
|
+
]);
|
|
105
|
+
expect(
|
|
106
|
+
elapsed < 80,
|
|
107
|
+
"Servers connect in parallel, so one 40ms delay should not serialize both.",
|
|
108
|
+
).toBe(true);
|
|
109
|
+
await manager.stop();
|
|
110
|
+
});
|
|
111
|
+
});
|