From 59c643845fc45ba9d565b717e057b2cf08faff7a Mon Sep 17 00:00:00 2001 From: wenshao Date: Sat, 29 Aug 2026 14:20:38 +0800 Subject: [PATCH] fix(serve): advertise the native directory picker during bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9406 made `native_directory_picker` host-conditional but wired the probe only into the runtime capability path (`createApp` -> `serve-features.ts`). The fast-path bootstrap envelopes, which answer `/capabilities` and `/daemon/status` until the runtime mounts, never set the toggle, so they omitted the tag even on a host whose GUI probe returns true. Two consequences: a client that reads `/capabilities` inside that window hides the workspace Browse affordance on a machine that can open the native picker, and the capabilities-envelope E2E — which lands inside the bootstrap window — fails wherever the probe is true. macOS is the only CI lane where it is, which is why `E2E Test - macOS - shard 2/2` has been red on main since #9406 landed. Probe once while the bootstrap app is built and feed the result to both bootstrap envelopes, mirroring what the runtime path already does at `createApp` time. Probing at app-build time also keeps the `/dev/console` stat and the `PATH` scan for `zenity` off the per-request path. --- packages/cli/src/serve/run-qwen-serve.test.ts | 56 +++++++++++++++++++ packages/cli/src/serve/run-qwen-serve.ts | 17 +++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/serve/run-qwen-serve.test.ts b/packages/cli/src/serve/run-qwen-serve.test.ts index 9a40063fda1..906891dea3f 100644 --- a/packages/cli/src/serve/run-qwen-serve.test.ts +++ b/packages/cli/src/serve/run-qwen-serve.test.ts @@ -34,6 +34,7 @@ import { waitForRuntimeStartingForShutdown, } from './run-qwen-serve.js'; import { isBrowserAutomationMcpAvailable } from './cdp-mcp-command.js'; +import * as nativeDirectoryPicker from './native-directory-picker.js'; import { loadServeFastPathEnvironment } from './fast-path-settings.js'; import { loadEnvironment } from '../config/environment.js'; import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js'; @@ -10615,6 +10616,61 @@ describe('runQwenServe runtime startup failures', () => { } }); + it.each([true, false])( + 'mirrors the native directory picker probe on the bootstrap envelopes (available: %s)', + async (available) => { + tmpDir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'qws-bootstrap-picker-')), + ); + // Keep the runtime from mounting so the bootstrap `/capabilities` and + // `/daemon/status` envelopes stay the ones being served. + vi.spyOn(acpBridge, 'createAcpSessionBridge').mockImplementation(() => { + throw new Error('runtime boom'); + }); + const probe = vi + .spyOn(nativeDirectoryPicker, 'isNativeDirectoryPickerAvailable') + .mockReturnValue(available); + const handle = await runQwenServe( + { + port: 0, + hostname: '127.0.0.1', + mode: 'http-bridge', + workspace: tmpDir, + maxSessions: 1, + serveWebShell: false, + }, + { resolveOnListen: true }, + ); + try { + await expect(handle.runtimeReady).rejects.toThrow('runtime boom'); + const probeCallsAfterBoot = probe.mock.calls.length; + const capabilities = (await ( + await fetch(`${handle.url}/capabilities`) + ).json()) as { features: string[] }; + const status = (await ( + await fetch(`${handle.url}/daemon/status`) + ).json()) as { capabilities: { features: string[] } }; + if (available) { + expect(capabilities.features).toContain('native_directory_picker'); + expect(status.capabilities.features).toContain( + 'native_directory_picker', + ); + } else { + expect(capabilities.features).not.toContain( + 'native_directory_picker', + ); + expect(status.capabilities.features).not.toContain( + 'native_directory_picker', + ); + } + // Probed once while the bootstrap app was built, not per request. + expect(probe.mock.calls.length).toBe(probeCallsAfterBoot); + } finally { + await handle.close(); + } + }, + ); + it('shuts down a bridge when runtime mounting fails after bridge creation', async () => { tmpDir = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'qws-runtime-partial-fail-')), diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index f84fb6021fc..ac96b3d5257 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -112,6 +112,7 @@ import { getServeProtocolVersions, SERVE_CAPABILITY_REGISTRY, } from './capabilities.js'; +import { isNativeDirectoryPickerAvailable } from './native-directory-picker.js'; import { EXTERNAL_TOOL_GUARD_PROVIDER_ATTACHED_VALUE, EXTERNAL_TOOL_GUARD_REQUIRED_VALUE, @@ -2289,6 +2290,7 @@ function currentServeFeaturesForRunQwenServe( sessionArtifactsPersistenceAvailable: boolean, currentSessionSchedulingAvailable: boolean, env: Readonly>, + nativeDirectoryPickerAvailable: boolean, ): string[] { return getAdvertisedServeFeatures(undefined, { requireAuth: opts.requireAuth === true, @@ -2315,8 +2317,10 @@ function currentServeFeaturesForRunQwenServe( channelManagementAvailable: true, persistentWorkspaceRegistrationAvailable: true, workspaceRuntimeRemovalAvailable: true, - // Advertise the same WS feature flags as the runtime path (serve-features.ts) - // so the bootstrap `/capabilities` window doesn't briefly under-report them. + // Advertise the same host-conditional and WS feature flags as the runtime + // path (serve-features.ts) so the bootstrap `/capabilities` window doesn't + // briefly under-report them. + nativeDirectoryPickerAvailable, clientMcpOverWsEnabled: opts.clientMcpOverWs === true, cdpTunnelOverWsEnabled: opts.cdpTunnelOverWs === true, browserAutomationMcpAvailable: isBrowserAutomationMcpAvailable(opts, env), @@ -2332,6 +2336,7 @@ function createBootstrapCapabilities(input: { currentSessionSchedulingAvailable: boolean; permissionPolicy: PermissionPolicy | undefined; env: Readonly>; + nativeDirectoryPickerAvailable: boolean; }): CapabilitiesEnvelope { return { v: CAPABILITIES_SCHEMA_VERSION, @@ -2346,6 +2351,7 @@ function createBootstrapCapabilities(input: { input.sessionArtifactsPersistenceAvailable, input.currentSessionSchedulingAvailable, input.env, + input.nativeDirectoryPickerAvailable, ), modelServices: [], workspaceCwd: input.boundWorkspace, @@ -2566,6 +2572,11 @@ function createBootstrapServeApp(input: { onHealthServed, } = input; const app = express(); + // The probe stats `/dev/console` (macOS) or scans `PATH` for `zenity` + // (Linux), and both bootstrap endpoints below rebuild their envelope per + // request, so evaluate it once here — the runtime path likewise probes once, + // at `createApp` time (server.ts). + const nativeDirectoryPickerAvailable = isNativeDirectoryPickerAvailable(); installSameOriginOriginStrip(app, getPort); if (opts.allowOrigins && opts.allowOrigins.length > 0) { @@ -2626,6 +2637,7 @@ function createBootstrapServeApp(input: { currentSessionSchedulingAvailable, permissionPolicy, env: process.env, + nativeDirectoryPickerAvailable, }), ); }); @@ -2765,6 +2777,7 @@ function createBootstrapServeApp(input: { sessionArtifactsPersistenceAvailable, currentSessionSchedulingAvailable, process.env, + nativeDirectoryPickerAvailable, ), }, runtime: {