@vellumai/assistant 0.12.2-staging.2 → 0.12.2-staging.3
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/docs/desktop-browser-cli.md +33 -0
- package/openapi.yaml +17 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +6 -4
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +116 -18
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +7 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +62 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +34 -4
- package/src/calls/voice-session-bridge.ts +23 -0
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +30 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +2 -2
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/daemon/conversation-tool-setup.ts +31 -29
- package/src/daemon/host-cu-proxy.ts +51 -26
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +22 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +3 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +11 -3
- package/src/live-voice/__tests__/session-controls.test.ts +34 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +3 -0
- package/src/live-voice/protocol.ts +10 -1
- package/src/live-voice/session-controls.ts +28 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/seed-providers.ts +11 -0
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +2 -3
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -12,16 +12,30 @@ import { join } from "node:path";
|
|
|
12
12
|
import { readAvatarState } from "../avatar/avatar-manifest.js";
|
|
13
13
|
import { resolveNotificationAccentHex } from "../avatar/notification-avatar.js";
|
|
14
14
|
import { getIsContainerized } from "../config/env-registry.js";
|
|
15
|
+
import { connectCdpWsTransport } from "../tools/browser/cdp-client/cdp-inspect/ws-transport.js";
|
|
15
16
|
import { terminateProcessTree } from "../util/host-process.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
18
|
import { getDataDir } from "../util/platform.js";
|
|
18
19
|
import { sleep } from "../util/retry.js";
|
|
20
|
+
import { DesktopBrowserClient } from "./desktop-browser-client.js";
|
|
21
|
+
import {
|
|
22
|
+
allocateDesktopDebugPort,
|
|
23
|
+
desktopChromeArguments,
|
|
24
|
+
discoverDesktopBrowser,
|
|
25
|
+
findDesktopBrowserPid,
|
|
26
|
+
} from "./desktop-browser-endpoint.js";
|
|
19
27
|
import { writeDesktopChromePolicy } from "./desktop-chrome-policy.js";
|
|
20
|
-
import {
|
|
28
|
+
import { configureDesktopChromeFrame } from "./desktop-chrome-session.js";
|
|
21
29
|
import {
|
|
22
30
|
desktopChromePath,
|
|
23
31
|
resolveDesktopBinaries,
|
|
24
32
|
} from "./desktop-dependencies.js";
|
|
33
|
+
import {
|
|
34
|
+
DESKTOP_DISPLAY,
|
|
35
|
+
DESKTOP_HEIGHT,
|
|
36
|
+
DESKTOP_OVERRIDABLE_PARAMETERS,
|
|
37
|
+
DESKTOP_WIDTH,
|
|
38
|
+
} from "./desktop-display.js";
|
|
25
39
|
import { writeDesktopPanelConfig } from "./desktop-panel-config.js";
|
|
26
40
|
import { renderCurrentDesktopWallpaper } from "./desktop-wallpaper.js";
|
|
27
41
|
import { DESKTOP_WINDOW_DRAG_SCRIPT } from "./desktop-window-drag.js";
|
|
@@ -30,10 +44,7 @@ import { writeDesktopWindowTheme } from "./desktop-window-theme.js";
|
|
|
30
44
|
|
|
31
45
|
const log = getLogger("desktop-session");
|
|
32
46
|
|
|
33
|
-
const DESKTOP_DISPLAY = ":99";
|
|
34
47
|
export const DESKTOP_VNC_PORT = 5999;
|
|
35
|
-
const DESKTOP_WIDTH = 1600;
|
|
36
|
-
const DESKTOP_HEIGHT = 900;
|
|
37
48
|
const DESKTOP_GEOMETRY = `${DESKTOP_WIDTH}x${DESKTOP_HEIGHT}`;
|
|
38
49
|
const DESKTOP_LINGER_MS = 5 * 60_000;
|
|
39
50
|
const VNC_READY_DEADLINE_MS = 10_000;
|
|
@@ -153,6 +164,7 @@ type ViewerSlotResult =
|
|
|
153
164
|
| { readonly ok: false; readonly loss: DesktopLoss };
|
|
154
165
|
|
|
155
166
|
interface DesktopSessionManagerOptions {
|
|
167
|
+
readonly allocateDebugPort?: () => Promise<number>;
|
|
156
168
|
readonly spawn?: (
|
|
157
169
|
role: DesktopChildRole,
|
|
158
170
|
request: DesktopSpawnRequest,
|
|
@@ -194,6 +206,7 @@ export class DesktopSessionManager {
|
|
|
194
206
|
/** Bumped on every teardown so an in-flight start notices it lost its tree. */
|
|
195
207
|
private generation = 0;
|
|
196
208
|
private viewer: DesktopViewer | null = null;
|
|
209
|
+
private automation: DesktopViewer | null = null;
|
|
197
210
|
private lingerTimer: ReturnType<typeof setTimeout> | null = null;
|
|
198
211
|
private ingressClosed = false;
|
|
199
212
|
/** Resolved for the current tree, and read again when the dock comes up. */
|
|
@@ -211,6 +224,70 @@ export class DesktopSessionManager {
|
|
|
211
224
|
refreshQueued: boolean;
|
|
212
225
|
} | null = null;
|
|
213
226
|
|
|
227
|
+
private debugPort?: number;
|
|
228
|
+
private browserStarting: Promise<void> | null = null;
|
|
229
|
+
private readonly allocateDebugPort: () => Promise<number>;
|
|
230
|
+
readonly browser = new DesktopBrowserClient(async (signal) => {
|
|
231
|
+
if (!this.automation) {
|
|
232
|
+
throw new Error("Desktop browser requires an automation slot");
|
|
233
|
+
}
|
|
234
|
+
const generation = this.generation;
|
|
235
|
+
const owner = this.automation;
|
|
236
|
+
let child = this.children.get("browser");
|
|
237
|
+
let pid = child?.pid;
|
|
238
|
+
if (!pid && this.debugPort) {
|
|
239
|
+
pid = await findDesktopBrowserPid(
|
|
240
|
+
await this.resolveChromePath(),
|
|
241
|
+
this.profileDir,
|
|
242
|
+
this.debugPort,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (this.generation !== generation || this.automation !== owner) {
|
|
246
|
+
throw new Error("Desktop Chrome session changed. Observe again.");
|
|
247
|
+
}
|
|
248
|
+
signal.throwIfAborted();
|
|
249
|
+
if (!pid) {
|
|
250
|
+
await this.ensureBrowser(this.childEnv(), generation);
|
|
251
|
+
child = this.children.get("browser");
|
|
252
|
+
pid = child?.pid;
|
|
253
|
+
}
|
|
254
|
+
const port = this.debugPort;
|
|
255
|
+
const isCurrent = () =>
|
|
256
|
+
this.generation === generation &&
|
|
257
|
+
this.automation === owner &&
|
|
258
|
+
(!child || this.children.get("browser") === child);
|
|
259
|
+
if (!pid || !port) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"Desktop Chrome is unavailable. Open the Desktop modal to inspect it, then retry the browser command.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
signal = AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
|
|
265
|
+
const deadline = Date.now() + 5_000;
|
|
266
|
+
while (true) {
|
|
267
|
+
signal.throwIfAborted();
|
|
268
|
+
if (!isCurrent()) {
|
|
269
|
+
throw new Error("Desktop Chrome session changed. Observe again.");
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const url = await discoverDesktopBrowser(pid, port, signal);
|
|
273
|
+
const transport = await connectCdpWsTransport(url, {
|
|
274
|
+
signal,
|
|
275
|
+
connectTimeoutMs: 3000,
|
|
276
|
+
});
|
|
277
|
+
if (!isCurrent() || signal.aborted) {
|
|
278
|
+
transport.dispose();
|
|
279
|
+
throw new Error("Desktop Chrome session changed. Observe again.");
|
|
280
|
+
}
|
|
281
|
+
return transport;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (Date.now() >= deadline || signal.aborted) {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
await sleep(100);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
214
291
|
private readonly spawn: NonNullable<DesktopSessionManagerOptions["spawn"]>;
|
|
215
292
|
private readonly which: NonNullable<DesktopSessionManagerOptions["which"]>;
|
|
216
293
|
private readonly probeVncPort: NonNullable<
|
|
@@ -235,6 +312,8 @@ export class DesktopSessionManager {
|
|
|
235
312
|
private readonly writeWindowManagerConfig: (configDir: string) => string;
|
|
236
313
|
|
|
237
314
|
constructor(options: DesktopSessionManagerOptions = {}) {
|
|
315
|
+
this.allocateDebugPort =
|
|
316
|
+
options.allocateDebugPort ?? allocateDesktopDebugPort;
|
|
238
317
|
this.spawn = options.spawn ?? spawnDetached;
|
|
239
318
|
this.which = options.which ?? Bun.which;
|
|
240
319
|
this.probeVncPort = options.probeVncPort ?? probeLoopbackPort;
|
|
@@ -308,7 +387,29 @@ export class DesktopSessionManager {
|
|
|
308
387
|
return;
|
|
309
388
|
}
|
|
310
389
|
this.viewer = null;
|
|
311
|
-
if (this.running || this.starting) {
|
|
390
|
+
if (!this.automation && (this.running || this.starting)) {
|
|
391
|
+
this.armLinger();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
acquireAutomationSlot(owner: DesktopViewer): ViewerSlotResult {
|
|
396
|
+
if (this.ingressClosed) {
|
|
397
|
+
return { ok: false, loss: SHUTTING_DOWN_LOSS };
|
|
398
|
+
}
|
|
399
|
+
if (this.automation) {
|
|
400
|
+
return { ok: false, loss: BUSY_LOSS };
|
|
401
|
+
}
|
|
402
|
+
this.automation = owner;
|
|
403
|
+
this.clearLinger();
|
|
404
|
+
return { ok: true };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
releaseAutomationSlot(owner: DesktopViewer): void {
|
|
408
|
+
if (this.automation !== owner) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.automation = null;
|
|
412
|
+
if (!this.viewer && (this.running || this.starting)) {
|
|
312
413
|
this.armLinger();
|
|
313
414
|
}
|
|
314
415
|
}
|
|
@@ -393,7 +494,7 @@ export class DesktopSessionManager {
|
|
|
393
494
|
this.running = true;
|
|
394
495
|
log.info({ display: DESKTOP_DISPLAY }, "Desktop started");
|
|
395
496
|
void this.refreshWallpaper(env, generation);
|
|
396
|
-
void this.
|
|
497
|
+
void this.ensureBrowser(env, generation);
|
|
397
498
|
}
|
|
398
499
|
|
|
399
500
|
private async refreshWallpaper(
|
|
@@ -455,18 +556,42 @@ export class DesktopSessionManager {
|
|
|
455
556
|
}
|
|
456
557
|
|
|
457
558
|
/** Launch Chrome and its dock once the X server is ready. */
|
|
559
|
+
private ensureBrowser(
|
|
560
|
+
env: Record<string, string>,
|
|
561
|
+
generation: number,
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
if (this.children.has("browser")) {
|
|
564
|
+
return Promise.resolve();
|
|
565
|
+
}
|
|
566
|
+
this.browserStarting ??= this.startBrowser(env, generation).finally(() => {
|
|
567
|
+
this.browserStarting = null;
|
|
568
|
+
});
|
|
569
|
+
return this.browserStarting;
|
|
570
|
+
}
|
|
571
|
+
|
|
458
572
|
private async startBrowser(
|
|
459
573
|
env: Record<string, string>,
|
|
460
574
|
generation: number,
|
|
461
575
|
): Promise<void> {
|
|
462
576
|
try {
|
|
463
577
|
const executable = await this.resolveChromePath();
|
|
464
|
-
|
|
578
|
+
const debugPort = this.debugPort ?? (await this.allocateDebugPort());
|
|
579
|
+
if (this.generation !== generation || this.children.has("browser")) {
|
|
465
580
|
return;
|
|
466
581
|
}
|
|
467
582
|
mkdirSync(this.profileDir, { recursive: true });
|
|
583
|
+
this.debugPort = debugPort;
|
|
584
|
+
try {
|
|
585
|
+
configureDesktopChromeFrame(this.profileDir);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
log.warn({ err }, "Desktop Chrome frame could not be configured");
|
|
588
|
+
}
|
|
468
589
|
this.startPanel(executable, env);
|
|
469
|
-
this.launch(
|
|
590
|
+
this.launch(
|
|
591
|
+
"browser",
|
|
592
|
+
[executable, ...desktopChromeArguments(this.profileDir, debugPort)],
|
|
593
|
+
env,
|
|
594
|
+
);
|
|
470
595
|
} catch (err) {
|
|
471
596
|
log.warn({ err }, "Desktop browser failed to launch");
|
|
472
597
|
if (this.generation === generation) {
|
|
@@ -491,6 +616,7 @@ export class DesktopSessionManager {
|
|
|
491
616
|
configDir: this.panelConfigDir,
|
|
492
617
|
chromiumPath,
|
|
493
618
|
chromiumProfileDir: this.profileDir,
|
|
619
|
+
debugPort: this.debugPort,
|
|
494
620
|
terminalPath: binaries.terminal,
|
|
495
621
|
});
|
|
496
622
|
this.launch("panel", [binaries.panelSession, "--", binaries.panel], {
|
|
@@ -577,6 +703,7 @@ export class DesktopSessionManager {
|
|
|
577
703
|
return;
|
|
578
704
|
}
|
|
579
705
|
if (role === "browser") {
|
|
706
|
+
this.browser.dispose();
|
|
580
707
|
log.info({ outcome }, "Desktop browser exited");
|
|
581
708
|
return;
|
|
582
709
|
}
|
|
@@ -599,6 +726,8 @@ export class DesktopSessionManager {
|
|
|
599
726
|
private teardown(loss?: DesktopLoss): Promise<void> {
|
|
600
727
|
this.clearLinger();
|
|
601
728
|
this.generation += 1;
|
|
729
|
+
this.browser.dispose();
|
|
730
|
+
this.debugPort = undefined;
|
|
602
731
|
this.running = false;
|
|
603
732
|
this.binaries = null;
|
|
604
733
|
this.panelStarted = false;
|
|
@@ -613,7 +742,12 @@ export class DesktopSessionManager {
|
|
|
613
742
|
const children = new Map(this.children);
|
|
614
743
|
this.children.clear();
|
|
615
744
|
const viewer = this.viewer;
|
|
745
|
+
const automation = this.automation;
|
|
616
746
|
this.viewer = null;
|
|
747
|
+
this.automation = null;
|
|
748
|
+
if (automation && loss) {
|
|
749
|
+
automation.onDesktopLost(loss);
|
|
750
|
+
}
|
|
617
751
|
if (viewer && loss) {
|
|
618
752
|
viewer.onDesktopLost(loss);
|
|
619
753
|
}
|
|
@@ -733,6 +867,8 @@ function xServerCommand(executable: string): string[] {
|
|
|
733
867
|
"None",
|
|
734
868
|
"-rfbport",
|
|
735
869
|
String(DESKTOP_VNC_PORT),
|
|
870
|
+
"-AllowOverride",
|
|
871
|
+
DESKTOP_OVERRIDABLE_PARAMETERS.join(","),
|
|
736
872
|
"-geometry",
|
|
737
873
|
DESKTOP_GEOMETRY,
|
|
738
874
|
// Keep the dock and wallpaper anchored to a stable display.
|
|
@@ -744,22 +880,6 @@ function xServerCommand(executable: string): string[] {
|
|
|
744
880
|
];
|
|
745
881
|
}
|
|
746
882
|
|
|
747
|
-
function browserCommand(executable: string, profileDir: string): string[] {
|
|
748
|
-
// Explicit window bounds override Chrome's maximized startup state.
|
|
749
|
-
return [
|
|
750
|
-
executable,
|
|
751
|
-
"--no-sandbox",
|
|
752
|
-
"--no-first-run",
|
|
753
|
-
"--disable-dev-shm-usage",
|
|
754
|
-
// Crash recovery requires both flags; clean exits use normal startup.
|
|
755
|
-
...(shouldRestoreDesktopChromeSession(profileDir)
|
|
756
|
-
? ["--restore-last-session", "--hide-crash-restore-bubble"]
|
|
757
|
-
: []),
|
|
758
|
-
"--start-maximized",
|
|
759
|
-
`--user-data-dir=${profileDir}`,
|
|
760
|
-
];
|
|
761
|
-
}
|
|
762
|
-
|
|
763
883
|
// ---------------------------------------------------------------------------
|
|
764
884
|
// Default collaborators
|
|
765
885
|
// ---------------------------------------------------------------------------
|
|
@@ -178,7 +178,7 @@ export class DesktopStreamBridge {
|
|
|
178
178
|
if (!this.isEnabled()) {
|
|
179
179
|
this.fail(
|
|
180
180
|
DESKTOP_CLOSE.unavailable,
|
|
181
|
-
"
|
|
181
|
+
"Virtual desktop is available only on enabled platform-hosted assistants",
|
|
182
182
|
);
|
|
183
183
|
return false;
|
|
184
184
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
2
|
+
import { getIsContainerized, getIsPlatform } from "../config/env-registry.js";
|
|
3
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_DESKTOP_FLAG = "assistant-desktop" as const;
|
|
6
|
+
|
|
7
|
+
/** Gates desktop streaming and control to enabled, platform-hosted containers. */
|
|
8
|
+
export function isVirtualDesktopEnabled(
|
|
9
|
+
config: AssistantConfig,
|
|
10
|
+
containerized: boolean = getIsContainerized(),
|
|
11
|
+
platformHosted: boolean = getIsPlatform(),
|
|
12
|
+
): boolean {
|
|
13
|
+
return (
|
|
14
|
+
platformHosted &&
|
|
15
|
+
containerized &&
|
|
16
|
+
isAssistantFeatureFlagEnabled(VIRTUAL_DESKTOP_FLAG, config)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
|
|
4
|
+
import { shouldUseVirtualDesktopBrowser } from "../browser/virtual-desktop-target.js";
|
|
5
|
+
import { executeDesktopBrowserOperation } from "./desktop-browser-operations.js";
|
|
6
|
+
import { desktopDependencyInstaller } from "./desktop-dependencies.js";
|
|
7
|
+
|
|
8
|
+
const originalPlatform = process.env.IS_PLATFORM;
|
|
9
|
+
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
10
|
+
const context = {
|
|
11
|
+
workingDir: "/tmp",
|
|
12
|
+
conversationId: "conv-platform-gate",
|
|
13
|
+
sourceActorPrincipalId: "user-123",
|
|
14
|
+
trustClass: "guardian" as const,
|
|
15
|
+
transportInterface: "web" as const,
|
|
16
|
+
clientOs: "web" as const,
|
|
17
|
+
};
|
|
18
|
+
let status: ReturnType<
|
|
19
|
+
typeof spyOn<typeof desktopDependencyInstaller, "getStatus">
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
24
|
+
process.env.IS_PLATFORM = "false";
|
|
25
|
+
setOverridesForTesting({ "assistant-desktop": true });
|
|
26
|
+
status = spyOn(desktopDependencyInstaller, "getStatus").mockReturnValue({
|
|
27
|
+
state: "ready",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
status.mockRestore();
|
|
33
|
+
setOverridesForTesting({});
|
|
34
|
+
if (originalPlatform === undefined) {
|
|
35
|
+
delete process.env.IS_PLATFORM;
|
|
36
|
+
} else {
|
|
37
|
+
process.env.IS_PLATFORM = originalPlatform;
|
|
38
|
+
}
|
|
39
|
+
if (originalContainerized === undefined) {
|
|
40
|
+
delete process.env.IS_CONTAINERIZED;
|
|
41
|
+
} else {
|
|
42
|
+
process.env.IS_CONTAINERIZED = originalContainerized;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("self-hosted Docker cannot automatically select or explicitly control virtual Chrome", async () => {
|
|
47
|
+
expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(false);
|
|
48
|
+
await expect(
|
|
49
|
+
executeDesktopBrowserOperation("snapshot", {}, context),
|
|
50
|
+
).rejects.toThrow("only on enabled platform-hosted assistants");
|
|
51
|
+
expect(status).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("platform web conversations select virtual Chrome before and during installation", () => {
|
|
55
|
+
process.env.IS_PLATFORM = "true";
|
|
56
|
+
expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
|
|
57
|
+
status.mockReturnValue({ state: "installing" });
|
|
58
|
+
expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
|
|
59
|
+
status.mockReturnValue({ state: "required" });
|
|
60
|
+
expect(shouldUseVirtualDesktopBrowser(undefined, {}, context)).toBe(true);
|
|
61
|
+
});
|
|
@@ -35,9 +35,7 @@ describe("negotiateLocale", () => {
|
|
|
35
35
|
|
|
36
36
|
describe("localeFromAcceptLanguage", () => {
|
|
37
37
|
test("honors quality values", () => {
|
|
38
|
-
expect(localeFromAcceptLanguage("fr;q=0.8, es;q=0.9, en;q=0.5")).toBe(
|
|
39
|
-
"es",
|
|
40
|
-
);
|
|
38
|
+
expect(localeFromAcceptLanguage("fr;q=0.8, es;q=0.9, en;q=0.5")).toBe("es");
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
test("defaults on missing or empty", () => {
|
|
@@ -58,7 +56,7 @@ describe("catalog completeness", () => {
|
|
|
58
56
|
});
|
|
59
57
|
|
|
60
58
|
describe("t", () => {
|
|
61
|
-
test("resolves
|
|
59
|
+
test("resolves message keys per locale", () => {
|
|
62
60
|
expect(t(MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING, "en")).toBe(
|
|
63
61
|
"Generating title...",
|
|
64
62
|
);
|
|
@@ -68,13 +66,19 @@ describe("t", () => {
|
|
|
68
66
|
expect(t(MESSAGE_KEYS.CONVERSATION_TITLE_UNTITLED, "es")).toBe(
|
|
69
67
|
"Sin título",
|
|
70
68
|
);
|
|
69
|
+
expect(
|
|
70
|
+
t(MESSAGE_KEYS.PLUGIN_MCP_OAUTH_CREDENTIALS_UNCHECKED, "es"),
|
|
71
|
+
).toContain("almacenamiento de credenciales");
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
describe("resolveConversationTitle", () => {
|
|
75
76
|
test("resolves a stored key and an empty title", () => {
|
|
76
77
|
expect(
|
|
77
|
-
resolveConversationTitle(
|
|
78
|
+
resolveConversationTitle(
|
|
79
|
+
MESSAGE_KEYS.CONVERSATION_TITLE_GENERATING,
|
|
80
|
+
"zh",
|
|
81
|
+
),
|
|
78
82
|
).toBe("标题生成中...");
|
|
79
83
|
expect(resolveConversationTitle(null, "es")).toBe("Sin título");
|
|
80
84
|
expect(resolveConversationTitle("")).toBe("Untitled Conversation");
|
package/src/i18n/messages.ts
CHANGED
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
* a user title may be in any language.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_LOCALE,
|
|
16
|
-
type SupportedLocale,
|
|
17
|
-
} from "./locales.js";
|
|
14
|
+
import { DEFAULT_LOCALE, type SupportedLocale } from "./locales.js";
|
|
18
15
|
|
|
19
16
|
export const MESSAGE_KEYS = {
|
|
20
17
|
CONVERSATION_TITLE_GENERATING: "conversation.title.generating",
|
|
21
18
|
CONVERSATION_TITLE_UNTITLED: "conversation.title.untitled",
|
|
19
|
+
PLUGIN_MCP_OAUTH_CREDENTIALS_UNCHECKED:
|
|
20
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked",
|
|
22
21
|
} as const;
|
|
23
22
|
|
|
24
23
|
export type MessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
|
|
@@ -30,22 +29,32 @@ export const MESSAGE_CATALOGS: Record<
|
|
|
30
29
|
en: {
|
|
31
30
|
"conversation.title.generating": "Generating title...",
|
|
32
31
|
"conversation.title.untitled": "Untitled Conversation",
|
|
32
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
33
|
+
"Credential storage is unavailable, so historical plugin MCP OAuth credentials could not be checked.",
|
|
33
34
|
},
|
|
34
35
|
es: {
|
|
35
36
|
"conversation.title.generating": "Generando título...",
|
|
36
37
|
"conversation.title.untitled": "Sin título",
|
|
38
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
39
|
+
"El almacenamiento de credenciales no está disponible, por lo que no se pudieron comprobar las credenciales históricas de OAuth de MCP del plugin.",
|
|
37
40
|
},
|
|
38
41
|
ru: {
|
|
39
42
|
"conversation.title.generating": "Создание названия...",
|
|
40
43
|
"conversation.title.untitled": "Без названия",
|
|
44
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
45
|
+
"Хранилище учетных данных недоступно, поэтому не удалось проверить сохраненные учетные данные OAuth MCP плагина.",
|
|
41
46
|
},
|
|
42
47
|
zh: {
|
|
43
48
|
"conversation.title.generating": "标题生成中...",
|
|
44
49
|
"conversation.title.untitled": "无标题",
|
|
50
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
51
|
+
"凭据存储不可用,因此无法检查该插件之前的 MCP OAuth 凭据。",
|
|
45
52
|
},
|
|
46
53
|
"zh-TW": {
|
|
47
54
|
"conversation.title.generating": "標題產生中...",
|
|
48
55
|
"conversation.title.untitled": "未命名",
|
|
56
|
+
"plugin.uninstall.mcp_oauth_credentials_unchecked":
|
|
57
|
+
"憑證儲存空間無法使用,因此無法檢查此外掛程式先前的 MCP OAuth 憑證。",
|
|
49
58
|
},
|
|
50
59
|
};
|
|
51
60
|
|