diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 5f6edbe42c1..433320f370e 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -10868,7 +10868,12 @@ ipcMain.handle('hermes:fs:openDir', async (_event, dirPath) => { // on-disk plugin door silently breaks (#66899). Electron owns this resolution // so it stays valid in every connection mode. Created on demand, like openDir. ipcMain.handle('hermes:fs:desktopPluginsRoot', async () => { - const dir = path.join(HERMES_HOME, 'desktop-plugins') + // Profile-aware: a named Desktop profile gets its own plugin root under + // profiles//, matching the profile-scoped hermes_home the backend + // reported before this resolver existed. 'default'/unset pins the global root. + const profile = readActiveDesktopProfile() + const base = profile && profile !== 'default' ? path.join(HERMES_HOME, 'profiles', profile) : HERMES_HOME + const dir = path.join(base, 'desktop-plugins') try { await fs.promises.mkdir(dir, { recursive: true }) diff --git a/apps/desktop/src/contrib/runtime-loader.test.ts b/apps/desktop/src/contrib/runtime-loader.test.ts index 253dc9e6822..cc1d4336c2d 100644 --- a/apps/desktop/src/contrib/runtime-loader.test.ts +++ b/apps/desktop/src/contrib/runtime-loader.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { HermesReadDirResult } from '@/global' import type * as HermesModule from '@/hermes' -import { discoverRuntimePlugins } from './runtime-loader' +import { discoverRuntimePlugins, watchRuntimePlugins } from './runtime-loader' // getStatus would supply the connected backend's hermes_home — a REMOTE path in // remote mode. The disk scanner must NOT derive the plugin root from it (#66899). @@ -16,12 +16,21 @@ vi.mock('@/hermes', async importActual => ({ const desktopPluginsRoot = vi.fn<() => Promise>() const readDir = vi.fn<(path: string) => Promise>() +const watchDirectory = vi.fn<(path: string) => Promise<{ id: string }>>() +const onPreviewFileChanged = vi.fn() beforeEach(() => { desktopPluginsRoot.mockReset() readDir.mockReset() + watchDirectory.mockReset() + onPreviewFileChanged.mockReset() getStatus.mockClear() - ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { desktopPluginsRoot, readDir } + ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { + desktopPluginsRoot, + onPreviewFileChanged, + readDir, + watchDirectory + } }) afterEach(() => { @@ -50,3 +59,19 @@ describe('scanDiskPlugins (#66899)', () => { expect(readDir).not.toHaveBeenCalled() }) }) + +describe('watchRuntimePlugins dir watch (#66899)', () => { + it('watches the Electron-resolved local root, never the backend hermes_home', async () => { + desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins') + readDir.mockResolvedValue({ entries: [] }) + watchDirectory.mockResolvedValue({ id: 'watch-1' }) + + watchRuntimePlugins() + // Drain the async scan + startDirWatch chains. + await vi.waitFor(() => expect(watchDirectory).toHaveBeenCalled()) + + expect(watchDirectory).toHaveBeenCalledWith('/local/.hermes/desktop-plugins') + expect(watchDirectory).not.toHaveBeenCalledWith('/remote/box/.hermes/desktop-plugins') + expect(getStatus).not.toHaveBeenCalled() + }) +}) diff --git a/apps/desktop/src/contrib/runtime-loader.ts b/apps/desktop/src/contrib/runtime-loader.ts index 1ae48fd0e16..27e6e40b95e 100644 --- a/apps/desktop/src/contrib/runtime-loader.ts +++ b/apps/desktop/src/contrib/runtime-loader.ts @@ -350,8 +350,15 @@ export function watchRuntimePlugins(): void { } try { - const { hermes_home } = await getStatus() - dirWatchId = (await desktop.watchDirectory(`${hermes_home}/desktop-plugins`)).id + // Same Electron-local root as the scanner — never the backend's + // hermes_home, which is a remote path in remote mode (#66899). + const root = await desktop.desktopPluginsRoot?.() + + if (!root) { + return false + } + + dirWatchId = (await desktop.watchDirectory(root)).id return true } catch {