fix(desktop): scope WSL bridge state by profile
This commit is contained in:
@@ -353,7 +353,7 @@ import { installWindowsSystemCaTrust } from './windows-system-ca'
|
||||
import { readWindowsUserEnvVar } from './windows-user-env'
|
||||
import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd'
|
||||
import { readWslWindowsClipboardImage } from './wsl-clipboard-image'
|
||||
import { resolvePickerDefaultPath, setWslBridgeActive } from './wsl-path-bridge'
|
||||
import { resolvePickerDefaultPath, setActiveGatewayProfile, setWslBridgeProfileState } from './wsl-path-bridge'
|
||||
|
||||
const USER_DATA_OVERRIDE = process.env.HERMES_DESKTOP_USER_DATA_DIR
|
||||
|
||||
@@ -9898,6 +9898,7 @@ async function ensureBackend(profile) {
|
||||
|
||||
if (route.backend === 'primary') {
|
||||
const connection = await startHermes()
|
||||
setWslBridgeProfileState(key, connection.mode !== 'remote')
|
||||
|
||||
// A shared backend still owes the caller its profile scope, so renderer-side
|
||||
// WebSocket, filesystem, and cache routing target the selected profile.
|
||||
@@ -9921,8 +9922,10 @@ async function ensureBackend(profile) {
|
||||
|
||||
if (existing) {
|
||||
existing.lastActiveAt = Date.now()
|
||||
const connection = await existing.connectionPromise
|
||||
setWslBridgeProfileState(key, connection.mode !== 'remote')
|
||||
|
||||
return existing.connectionPromise
|
||||
return connection
|
||||
}
|
||||
|
||||
evictLruPoolBackends(POOL_MAX_BACKENDS - 1)
|
||||
@@ -9955,7 +9958,10 @@ async function ensureBackend(profile) {
|
||||
backendPool.set(key, entry)
|
||||
startPoolIdleReaper()
|
||||
|
||||
return entry.connectionPromise
|
||||
const connection = await entry.connectionPromise
|
||||
setWslBridgeProfileState(key, connection.mode !== 'remote')
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
// ── Registry-scoped backends (multi-connection, PR 2 of the campaign) ──────
|
||||
@@ -10579,6 +10585,11 @@ async function startHermes() {
|
||||
}
|
||||
|
||||
const connectionAttempt = backendConnectionState.startAttempt()
|
||||
const primaryProfile = primaryProfileKey()
|
||||
|
||||
// Legacy path callers without an explicit profile belong to the primary
|
||||
// window backend. Profile-scoped callers still pass their key directly.
|
||||
setActiveGatewayProfile(primaryProfile)
|
||||
|
||||
// Classify this boot BEFORE the throwing resolve/mint runs: a remote failure
|
||||
// must NOT latch (it's transient — see shouldLatchBackendStartFailure), while
|
||||
@@ -10660,7 +10671,7 @@ async function startHermes() {
|
||||
// both for an already-saved remote and after first-run remote Apply.
|
||||
attemptedRemote = primaryBackendIsRemote()
|
||||
|
||||
return resolveRemoteBackend(primaryProfileKey())
|
||||
return resolveRemoteBackend(primaryProfile)
|
||||
},
|
||||
waitForDecision: waitForFirstRunSetupChoice,
|
||||
// Mutual exclusion with an in-app update (#50238). Remote connections
|
||||
@@ -10673,13 +10684,13 @@ async function startHermes() {
|
||||
// cannot open via wsl.exe — disable WSL path bridging so native dialogs
|
||||
// and file panels don't spawn wsl.exe (or the interactive install prompt
|
||||
// on WSL-less machines) for unresolvable paths. (#66433)
|
||||
setWslBridgeActive(false)
|
||||
setWslBridgeProfileState(primaryProfile, false)
|
||||
|
||||
return setup.connection
|
||||
}
|
||||
|
||||
// Local WSL backend — paths are bridgeable.
|
||||
setWslBridgeActive(true)
|
||||
setWslBridgeProfileState(primaryProfile, true)
|
||||
|
||||
const backend = setup.backend
|
||||
// Route old runtimes (no `serve`) through the legacy `dashboard --no-open`.
|
||||
@@ -13949,7 +13960,10 @@ ipcMain.handle('hermes:selectPaths', async (_event, options: any = {}) => {
|
||||
try {
|
||||
// On a Windows host with a WSL backend the cwd may be a POSIX/WSL path;
|
||||
// bridge it to a UNC/drive form the native dialog can actually open.
|
||||
const bridged = IS_WINDOWS ? resolvePickerDefaultPath(String(options.defaultPath)) : String(options.defaultPath)
|
||||
const bridged = IS_WINDOWS
|
||||
? resolvePickerDefaultPath(String(options.defaultPath), undefined, options?.profile)
|
||||
: String(options.defaultPath)
|
||||
|
||||
resolvedDefaultPath = bridged ? path.resolve(bridged) : undefined
|
||||
} catch {
|
||||
resolvedDefaultPath = undefined
|
||||
@@ -15041,7 +15055,10 @@ app.whenReady().then(() => {
|
||||
f12Blocked = readPersistedDisableF12()
|
||||
// Seed this before the first window exists: a picker can open before
|
||||
// startHermes() finishes resolving the configured backend.
|
||||
setWslBridgeActive(!primaryBackendIsRemote())
|
||||
const primaryProfile = primaryProfileKey()
|
||||
|
||||
setActiveGatewayProfile(primaryProfile)
|
||||
setWslBridgeProfileState(primaryProfile, !primaryBackendIsRemote())
|
||||
// Quick Entry's global chord — registered on ready so a cold launch restores
|
||||
// it without the renderer visiting Settings. A failed registration is logged
|
||||
// here and surfaced in Settings via the IPC state (never silent).
|
||||
|
||||
242
apps/desktop/electron/wsl-path-bridge-profile.test.ts
Normal file
242
apps/desktop/electron/wsl-path-bridge-profile.test.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Profile-scoped eligibility for the WSL path bridge (#66447).
|
||||
*
|
||||
* The single-profile tests in wsl-path-bridge.test.ts and the Windows-platform
|
||||
* gate tests in wsl-path-bridge-gate.test.ts cover the *what* (paths pass
|
||||
* through unchanged when bridging is disabled) but not the *which profile*. The
|
||||
* desktop is multi-profile: the renderer can swap the live gateway onto any
|
||||
* profile (primary or pool) without reloading the window — so the bridge
|
||||
* eligibility MUST be keyed off the **currently active profile's** backend
|
||||
* configuration, not a process-global boolean. This file proves the
|
||||
* per-profile contract:
|
||||
*
|
||||
* 1. local primary profile → bridge ON (preserved)
|
||||
* 2. remote primary profile → bridge OFF (preserved)
|
||||
* 3. local primary + remote non-primary → bridge OFF when the non-primary
|
||||
* is active; bridge ON when the primary is active again — **no bleed**.
|
||||
* 4. remote primary + local non-primary → bridge ON when the non-primary
|
||||
* is active; bridge OFF when the primary is active again — **no bleed**.
|
||||
* 5. profile-scoped calls accept a profile argument; absent it falls back
|
||||
* to the live gateway profile that the renderer announced.
|
||||
* 6. afterEach resets state so tests don't bleed into each other.
|
||||
*
|
||||
* These tests are pure behavior: they assert the eligibility function's output
|
||||
* and the public selectors' output against observable calls. They do NOT read
|
||||
* the implementation source — only the public surface exported from
|
||||
* `./wsl-path-bridge`.
|
||||
*/
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { afterEach, describe, test } from 'vitest'
|
||||
|
||||
import {
|
||||
isWslBridgeActive,
|
||||
resolveLocalReadPath,
|
||||
resolvePickerDefaultPath,
|
||||
setActiveGatewayProfile,
|
||||
setWslBridgeActive,
|
||||
setWslBridgeProfileState,
|
||||
wslPosixToWindowsAccessible
|
||||
} from './wsl-path-bridge'
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
const PROFILE_PRIMARY = 'default'
|
||||
const PROFILE_LOCAL = 'team-local'
|
||||
const PROFILE_REMOTE = 'team-remote'
|
||||
|
||||
/** Reset every profile key the bridge knows about to a clean state. */
|
||||
afterEach(() => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_LOCAL, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
setWslBridgeActive(true)
|
||||
})
|
||||
|
||||
// ── single-profile contract (preserved behaviour) ────────────────────
|
||||
|
||||
describe('WSL bridge profile eligibility — single-profile contract preserved', () => {
|
||||
test('primary local → bridge ON (preserved)', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
assert.equal(isWslBridgeActive(), true)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
})
|
||||
|
||||
test('primary remote → bridge OFF (preserved)', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, false)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
assert.equal(isWslBridgeActive(), false)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY), '/home/alex')
|
||||
assert.equal(resolveLocalReadPath('/home/alex/proj', undefined, PROFILE_PRIMARY), '/home/alex/proj')
|
||||
})
|
||||
})
|
||||
|
||||
// ── multi-profile regression (the gap) ────────────────────────────────
|
||||
|
||||
describe('WSL bridge profile eligibility — multi-profile (no cross-profile bleed)', () => {
|
||||
test('local primary + remote non-primary → non-primary OFF, primary ON', () => {
|
||||
// Primary is a local backend (WSL on this Windows host). A second profile
|
||||
// points at a remote host whose POSIX paths the Windows host CANNOT open
|
||||
// via wsl.exe — bridging must be OFF for it.
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
|
||||
// Non-primary remote is foregrounded — bridge must be OFF for it.
|
||||
setActiveGatewayProfile(PROFILE_REMOTE)
|
||||
assert.equal(isWslBridgeActive(), false)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_REMOTE), '/home/alex')
|
||||
assert.equal(resolveLocalReadPath('/home/alex/proj', undefined, PROFILE_REMOTE), '/home/alex/proj')
|
||||
|
||||
// Swap back to local primary — bridge must be ON again, no bleed.
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
assert.equal(isWslBridgeActive(), true)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
})
|
||||
|
||||
test('remote primary + local non-primary → non-primary ON, primary OFF', () => {
|
||||
// Primary is remote (no local WSL paths). A second profile is local —
|
||||
// bridging should be ON for it because its paths CAN be opened locally.
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, false)
|
||||
setWslBridgeProfileState(PROFILE_LOCAL, true)
|
||||
|
||||
// Local non-primary foregrounded — bridge ON for it.
|
||||
setActiveGatewayProfile(PROFILE_LOCAL)
|
||||
assert.equal(isWslBridgeActive(), true)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_LOCAL),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
|
||||
// Swap back to remote primary — bridge OFF for it, no bleed.
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
assert.equal(isWslBridgeActive(), false)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY), '/home/alex')
|
||||
})
|
||||
|
||||
test('three profiles: each profile behaves independently', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_LOCAL, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
|
||||
// Same path, different profiles, different outcomes.
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_LOCAL),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_REMOTE), '/home/alex')
|
||||
})
|
||||
})
|
||||
|
||||
// ── profile-argument contract ────────────────────────────────────────
|
||||
|
||||
describe('WSL bridge profile eligibility — selector argument contract', () => {
|
||||
test("selector with explicit profile → that profile's bridge state", () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
|
||||
// Explicit profile wins over the live gateway.
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_REMOTE), '/home/alex')
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
})
|
||||
|
||||
test("selector with no profile → live gateway profile's bridge state", () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
setActiveGatewayProfile(PROFILE_REMOTE)
|
||||
|
||||
// No profile argument → fallback to active gateway profile (remote → OFF).
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu'), '/home/alex')
|
||||
assert.equal(resolveLocalReadPath('/home/alex/proj'), '/home/alex/proj')
|
||||
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu'), '\\\\wsl.localhost\\Ubuntu\\home\\alex')
|
||||
})
|
||||
|
||||
test('selector with unknown profile → bridge ON (defaults to active for new profiles)', () => {
|
||||
// A profile that has never been seeded should default to the safe
|
||||
// "bridge ON" behaviour so a brand-new local profile isn't accidentally
|
||||
// disabled. The renderer seeds the state at boot; an unknown key here is
|
||||
// either a renderer race or a profile created mid-session.
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, false)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', 'unknown-profile'),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ── legacy back-compat: setWslBridgeActive targets the active profile ─
|
||||
|
||||
describe('WSL bridge profile eligibility — legacy toggle targets active profile', () => {
|
||||
test('setWslBridgeActive(false) flips the active profile, not a process-global', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, true)
|
||||
setActiveGatewayProfile(PROFILE_REMOTE)
|
||||
|
||||
setWslBridgeActive(false)
|
||||
|
||||
// Active profile (REMOTE) flipped to OFF; primary untouched.
|
||||
assert.equal(isWslBridgeActive(), false)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
assert.equal(resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_REMOTE), '/home/alex')
|
||||
})
|
||||
|
||||
test('setWslBridgeActive(true) restores the active profile only', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, true)
|
||||
setWslBridgeProfileState(PROFILE_REMOTE, false)
|
||||
setActiveGatewayProfile(PROFILE_REMOTE)
|
||||
|
||||
setWslBridgeActive(true)
|
||||
|
||||
// Active (REMOTE) restored; primary unchanged.
|
||||
assert.equal(isWslBridgeActive(), true)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_REMOTE),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
assert.equal(
|
||||
resolvePickerDefaultPath('/home/alex', 'Ubuntu', PROFILE_PRIMARY),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ── wslPosixToWindowsAccessible stays pure / unchanged ───────────────
|
||||
|
||||
describe('WSL bridge profile eligibility — POSIX translation stays pure', () => {
|
||||
test('wslPosixToWindowsAccessible ignores bridge state (pure translation)', () => {
|
||||
setWslBridgeProfileState(PROFILE_PRIMARY, false)
|
||||
setActiveGatewayProfile(PROFILE_PRIMARY)
|
||||
|
||||
// The translator does NOT consult the bridge state — it's pure POSIX → UNC.
|
||||
// Tests elsewhere assert that the bridge gate short-circuits BEFORE this
|
||||
// function is reached. A regression here would mean coupling leaked into
|
||||
// a helper that should stay side-effect-free.
|
||||
assert.equal(
|
||||
wslPosixToWindowsAccessible('/home/alex/proj', 'Ubuntu'),
|
||||
'\\\\wsl.localhost\\Ubuntu\\home\\alex\\proj'
|
||||
)
|
||||
assert.equal(wslPosixToWindowsAccessible('/mnt/c/Users/alex', 'Ubuntu'), 'C:\\Users\\alex')
|
||||
})
|
||||
})
|
||||
@@ -17,25 +17,39 @@ let cachedDistro: null | string = null
|
||||
let cachedUncBase: null | string = null
|
||||
|
||||
/**
|
||||
* Whether WSL path bridging is active. The bridge only makes sense when the
|
||||
* desktop runs on Windows AND the gateway is a *local* backend (e.g. running
|
||||
* inside WSL on the same machine). When the gateway is a remote host, the
|
||||
* POSIX paths it reports belong to a machine the Windows host cannot open via
|
||||
* `wsl.exe` — bridging them only spawns `wsl.exe` (and on WSL-less machines,
|
||||
* the interactive "Install WSL" console prompt) for paths that can never be
|
||||
* resolved locally. main.ts toggles this off once it resolves a remote
|
||||
* backend. Defaults to active so a local Windows+WSL boot is unaffected.
|
||||
* WSL path eligibility belongs to the backend profile that produced the path.
|
||||
* A single desktop process can keep a local primary backend and a remote pool
|
||||
* backend alive simultaneously, so a process-global boolean can bleed between
|
||||
* them. Unknown profiles retain the historical local default until Electron
|
||||
* resolves and records their actual backend mode.
|
||||
*/
|
||||
let wslBridgeActive = true
|
||||
const DEFAULT_WSL_BRIDGE_PROFILE = 'default'
|
||||
const wslBridgeProfiles = new Map<string, boolean>()
|
||||
let activeWslBridgeProfile = DEFAULT_WSL_BRIDGE_PROFILE
|
||||
|
||||
/** Enable/disable WSL path bridging at runtime (called by main.ts). */
|
||||
export function setWslBridgeActive(active: boolean): void {
|
||||
wslBridgeActive = active
|
||||
function normalizeWslBridgeProfile(profile?: null | string): string {
|
||||
return String(profile || '').trim() || DEFAULT_WSL_BRIDGE_PROFILE
|
||||
}
|
||||
|
||||
/** Test seam: is the bridge currently active? */
|
||||
export function isWslBridgeActive(): boolean {
|
||||
return wslBridgeActive
|
||||
/** Select the profile used by legacy callers that cannot pass one explicitly. */
|
||||
export function setActiveGatewayProfile(profile?: null | string): void {
|
||||
activeWslBridgeProfile = normalizeWslBridgeProfile(profile)
|
||||
}
|
||||
|
||||
/** Record whether paths returned by one profile belong to this host's WSL. */
|
||||
export function setWslBridgeProfileState(profile: null | string, active: boolean): void {
|
||||
wslBridgeProfiles.set(normalizeWslBridgeProfile(profile), Boolean(active))
|
||||
}
|
||||
|
||||
/** Backward-compatible toggle: update only the current fallback profile. */
|
||||
export function setWslBridgeActive(active: boolean): void {
|
||||
setWslBridgeProfileState(activeWslBridgeProfile, active)
|
||||
}
|
||||
|
||||
export function isWslBridgeActive(profile?: null | string): boolean {
|
||||
const key = profile == null ? activeWslBridgeProfile : normalizeWslBridgeProfile(profile)
|
||||
|
||||
return wslBridgeProfiles.get(key) ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +152,8 @@ export function wslPosixToWindowsAccessible(posixPath: string, distro: string =
|
||||
/** Native folder dialog `defaultPath`: open a WSL cwd in the Windows picker. */
|
||||
export function resolvePickerDefaultPath(
|
||||
defaultPath: string | undefined,
|
||||
distro?: string
|
||||
distro?: string,
|
||||
profile?: null | string
|
||||
): string | undefined {
|
||||
if (!defaultPath) {
|
||||
return undefined
|
||||
@@ -147,7 +162,7 @@ export function resolvePickerDefaultPath(
|
||||
// Remote-gateway POSIX paths can't be opened via wsl.exe — no-op the bridge
|
||||
// so the native dialog gets the raw path (it falls back gracefully) instead
|
||||
// of triggering a wsl.exe spawn / install prompt. (#66433)
|
||||
if (!wslBridgeActive) {
|
||||
if (!isWslBridgeActive(profile)) {
|
||||
return defaultPath
|
||||
}
|
||||
|
||||
@@ -159,14 +174,14 @@ export function resolvePickerDefaultPath(
|
||||
}
|
||||
|
||||
/** fs read path: on Windows, make a WSL cwd readable via its UNC / drive form. */
|
||||
export function resolveLocalReadPath(dirPath: string, distro?: string): string {
|
||||
export function resolveLocalReadPath(dirPath: string, distro?: string, profile?: null | string): string {
|
||||
const value = String(dirPath || '').trim()
|
||||
|
||||
// In remote-gateway mode the POSIX paths belong to a host the Windows
|
||||
// desktop cannot open locally — skip the WSL bridge entirely (no distro
|
||||
// probe, no wsl.exe) so the file panel never spawns the install prompt on
|
||||
// WSL-less machines. (#66433)
|
||||
if (!wslBridgeActive) {
|
||||
if (!isWslBridgeActive(profile)) {
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
2
apps/desktop/src/global.d.ts
vendored
2
apps/desktop/src/global.d.ts
vendored
@@ -1311,6 +1311,8 @@ export interface HermesSelectPathsOptions {
|
||||
defaultPath?: string
|
||||
directories?: boolean
|
||||
multiple?: boolean
|
||||
/** Backend profile that produced defaultPath; Electron uses it for WSL gating. */
|
||||
profile?: string
|
||||
filters?: Array<{ name: string; extensions: string[] }>
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('desktop filesystem facade', () => {
|
||||
})
|
||||
|
||||
it('uses local Electron filesystem methods in local mode', async () => {
|
||||
$connection.set({ mode: 'local' } as never)
|
||||
$connection.set({ mode: 'local', profile: 'team-local' } as never)
|
||||
|
||||
await expect(readDesktopDir('/work')).resolves.toEqual({
|
||||
entries: [{ name: 'local', path: '/local', isDirectory: true }]
|
||||
@@ -90,7 +90,7 @@ describe('desktop filesystem facade', () => {
|
||||
expect(readFileText).toHaveBeenCalledWith('/work/file.txt')
|
||||
expect(readFileDataUrl).toHaveBeenCalledWith('/work/file.txt')
|
||||
expect(gitRoot).toHaveBeenCalledWith('/work')
|
||||
expect(selectPaths).toHaveBeenCalledWith({ directories: true })
|
||||
expect(selectPaths).toHaveBeenCalledWith({ directories: true, profile: 'team-local' })
|
||||
expect(api).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -216,12 +216,12 @@ describe('desktop filesystem facade', () => {
|
||||
|
||||
it('uses the local Electron picker for remote file selection', async () => {
|
||||
const remoteSelect = vi.fn(async () => ['/remote/project'])
|
||||
$connection.set({ mode: 'remote' } as never)
|
||||
$connection.set({ mode: 'remote', profile: 'team-remote' } as never)
|
||||
setDesktopFsRemotePicker({ selectPaths: remoteSelect })
|
||||
|
||||
await expect(selectDesktopPaths({ directories: false, multiple: false })).resolves.toEqual(['/local'])
|
||||
|
||||
expect(selectPaths).toHaveBeenCalledWith({ directories: false, multiple: false })
|
||||
expect(selectPaths).toHaveBeenCalledWith({ directories: false, multiple: false, profile: 'team-remote' })
|
||||
expect(remoteSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -201,13 +201,15 @@ export async function desktopFileDiff(repoRoot: string, filePath: string): Promi
|
||||
|
||||
export async function selectDesktopPaths(options?: HermesSelectPathsOptions): Promise<string[]> {
|
||||
const desktop = bridge()
|
||||
const profile = desktopFsProfile()
|
||||
const localOptions = profile ? { ...options, profile } : options
|
||||
|
||||
if (!isDesktopFsRemoteMode()) {
|
||||
return desktop.selectPaths(options)
|
||||
return desktop.selectPaths(localOptions)
|
||||
}
|
||||
|
||||
if (!options?.directories) {
|
||||
return desktop.selectPaths(options)
|
||||
return desktop.selectPaths(localOptions)
|
||||
}
|
||||
|
||||
return remotePicker ? remotePicker.selectPaths({ ...options, multiple: false }) : []
|
||||
|
||||
Reference in New Issue
Block a user