From 3390fd53d6a15fbe1767ece473c5d475de6b4a1a Mon Sep 17 00:00:00 2001 From: JD Date: Mon, 9 Nov 2026 07:26:39 -0500 Subject: [PATCH] fix(web): reuse host identity for paired service editor opening Use the existing getHostEnvironmentId bridge and remove the duplicate desktop IPC and contract. Exercise the selected workspace editor shortcut with the details panel open and closed. Verification: check:changed against the PR base passed 129 tests and six compiler checks. Disabling both shortcut handlers makes the strengthened browser test fail. --- apps/desktop/src/ipc/DesktopIpcHandlers.ts | 2 - apps/desktop/src/ipc/channels.ts | 1 - apps/desktop/src/ipc/methods/window.test.ts | 19 -------- apps/desktop/src/ipc/methods/window.ts | 16 ------- apps/desktop/src/preload.ts | 2 - .../ChatView.sameHome.browser.test.tsx | 44 +++++++++++++++++-- apps/web/src/remoteOpen.browser.test.tsx | 8 ++-- apps/web/src/remoteOpen.test.ts | 14 +++--- apps/web/src/remoteOpen.ts | 2 +- packages/contracts/src/ipc.ts | 2 - 10 files changed, 52 insertions(+), 58 deletions(-) diff --git a/apps/desktop/src/ipc/DesktopIpcHandlers.ts b/apps/desktop/src/ipc/DesktopIpcHandlers.ts index 599a041ccaa71..db26c525c0015 100644 --- a/apps/desktop/src/ipc/DesktopIpcHandlers.ts +++ b/apps/desktop/src/ipc/DesktopIpcHandlers.ts @@ -38,7 +38,6 @@ import { getLocalEnvironmentBootstraps, getLocalEnvironmentBearerToken, installRelayTelemetryEnabled, - getHomeEnvironmentId, getSystemLocale, getPreferredSystemLanguages, getWindowFullscreenState, @@ -155,7 +154,6 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers" yield* ipc.handle(pasteAsText); yield* ipc.handle(revealWindow); yield* ipc.handle(probeRemoteEditors); - yield* ipc.handle(getHomeEnvironmentId); yield* ipc.handle(getUpdateState); yield* ipc.handle(setUpdateChannel); yield* ipc.handle(downloadUpdate); diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts index 93d81b48db59b..6a149e24ae695 100644 --- a/apps/desktop/src/ipc/channels.ts +++ b/apps/desktop/src/ipc/channels.ts @@ -34,7 +34,6 @@ export const CLI_COMMAND_UNINSTALL_CHANNEL = "desktop:cli-command-uninstall"; export const GET_APP_BRANDING_CHANNEL = "desktop:get-app-branding"; export const GET_RELAY_TELEMETRY_ENABLED_CHANNEL = "desktop:get-relay-telemetry-enabled"; export const GET_SYSTEM_LOCALE_CHANNEL = "desktop:get-system-locale"; -export const GET_HOME_ENVIRONMENT_ID_CHANNEL = "desktop:get-home-environment-id"; export const GET_PREFERRED_SYSTEM_LANGUAGES_CHANNEL = "desktop:get-preferred-system-languages"; export const GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL = "desktop:get-local-environment-bootstraps"; export const GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL = "desktop:get-local-environment-enabled"; diff --git a/apps/desktop/src/ipc/methods/window.test.ts b/apps/desktop/src/ipc/methods/window.test.ts index e0b247e62eba1..2ddc672891755 100644 --- a/apps/desktop/src/ipc/methods/window.test.ts +++ b/apps/desktop/src/ipc/methods/window.test.ts @@ -20,7 +20,6 @@ vi.mock("electron", () => ({ BrowserWindow: { fromWebContents: ownerWindow }, })); -import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts"; import * as DesktopBackendConfiguration from "../../backend/DesktopBackendConfiguration.ts"; import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts"; import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts"; @@ -34,7 +33,6 @@ import { getLocalEnvironmentBootstraps, installRelayTelemetryEnabled, pickProjectFavicon, - getHomeEnvironmentId, probeRemoteEditors, readMedia, } from "./window.ts"; @@ -388,20 +386,3 @@ it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")( assert.include(editors, "webstorm"); }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); - -it.effect("reads the home's environment id and treats an unwritten home as none", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const stateDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-home-environment-" }); - const read = getHomeEnvironmentId.handler(undefined).pipe( - Effect.provideService(DesktopEnvironment.DesktopEnvironment, { - path, - stateDir, - } as DesktopEnvironment.DesktopEnvironment["Service"]), - ); - assert.isNull(yield* read); - yield* fs.writeFileString(path.join(stateDir, "environment-id"), "environment-1\n"); - assert.equal(yield* read, "environment-1"); - }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), -); diff --git a/apps/desktop/src/ipc/methods/window.ts b/apps/desktop/src/ipc/methods/window.ts index b9a54aa04c124..9580b29b94bba 100644 --- a/apps/desktop/src/ipc/methods/window.ts +++ b/apps/desktop/src/ipc/methods/window.ts @@ -390,22 +390,6 @@ export const openSystemSettings = DesktopIpc.makeIpcMethod({ }), }); -export const getHomeEnvironmentId = DesktopIpc.makeIpcMethod({ - channel: IpcChannels.GET_HOME_ENVIRONMENT_ID_CHANNEL, - payload: Schema.Undefined, - result: Schema.NullOr(Schema.String), - handler: Effect.fn("desktop.ipc.window.getHomeEnvironmentId")(function* () { - const environment = yield* DesktopEnvironment.DesktopEnvironment; - const fileSystem = yield* FileSystem.FileSystem; - return yield* fileSystem - .readFileString(environment.path.join(environment.stateDir, "environment-id")) - .pipe( - Effect.map((contents) => contents.trim() || null), - Effect.orElseSucceed(() => null), - ); - }), -}); - export const probeRemoteEditors = DesktopIpc.makeIpcMethod({ channel: IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, payload: Schema.Undefined, diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 7d6475039c91f..7c44ec5133785 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -216,8 +216,6 @@ contextBridge.exposeInMainWorld("desktopBridge", { openSystemSettings: (pane: string) => ipcRenderer.invoke(IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, pane), probeRemoteEditors: () => ipcRenderer.invoke(IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, undefined), - getHomeEnvironmentId: () => - ipcRenderer.invoke(IpcChannels.GET_HOME_ENVIRONMENT_ID_CHANNEL, undefined), pasteAsText: () => ipcRenderer.invoke(IpcChannels.PASTE_AS_TEXT_CHANNEL, undefined), onMenuAction: (listener) => { const wrappedListener = (_event: Electron.IpcRendererEvent, action: unknown) => { diff --git a/apps/web/src/components/ChatView.sameHome.browser.test.tsx b/apps/web/src/components/ChatView.sameHome.browser.test.tsx index 7baf604f82f24..f9725c00c5d08 100644 --- a/apps/web/src/components/ChatView.sameHome.browser.test.tsx +++ b/apps/web/src/components/ChatView.sameHome.browser.test.tsx @@ -1,10 +1,11 @@ import { createMemoryHistory } from "@tanstack/react-router"; import { scopeProjectRef } from "@t3tools/client-runtime/environment"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { AsyncResult } from "effect/reactivity"; import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import type * as Context from "effect/Context"; import { expect, it, vi } from "vite-plus/test"; -import { page } from "vite-plus/test/browser"; +import { page, userEvent } from "vite-plus/test/browser"; import { AppRoot } from "~/AppRoot"; import "~/index.css"; @@ -22,7 +23,24 @@ import { } from "../../test/fakeEnvironment"; import { render } from "../../test/render"; -const pickerState = vi.hoisted(() => ({ sameHome: false })); +const pickerState = vi.hoisted(() => ({ sameHome: false, openEditor: vi.fn() })); +vi.mock(import("~/state/server"), async (original) => { + const actual = await original(); + const { Atom } = await import("effect/reactivity"); + const editors = Atom.make(["vscode"] as const); + return { ...actual, serverAvailableEditorsAtom: () => editors }; +}); +vi.mock(import("~/state/use-atom-command"), async (original) => { + const actual = await original(); + const { shellEnvironment } = await import("~/state/shell"); + return { + ...actual, + useAtomCommand: ((command, options) => { + const run = actual.useAtomCommand(command, options); + return Object.is(command, shellEnvironment.openInEditor) ? pickerState.openEditor : run; + }) as typeof actual.useAtomCommand, + }; +}); vi.mock(import("~/state/environments"), async (original) => { const actual = await original(); return { @@ -63,9 +81,10 @@ vi.mock(import("~/rpc/requestLatencyState"), async (original) => ({ trackRpcRequestSent: () => {}, })); -it("keeps workspace editor controls and its shortcut target for a same-home service with another primary", async () => { +it("opens the selected same-home workspace by shortcut with another primary", async () => { await page.viewport(1400, 900); pickerState.sameHome = true; + pickerState.openEditor.mockReset().mockResolvedValue(AsyncResult.success(undefined)); using _reset = { [Symbol.dispose]() { pickerState.sameHome = false; @@ -93,5 +112,22 @@ it("keeps workspace editor controls and its shortcut target for a same-home serv const toggle = page.getByRole("button", { name: "Toggle thread details panel", exact: true }); if (toggle.element().getAttribute("aria-pressed") === "false") await toggle.click(); await expect.element(page.getByRole("group", { name: "Open in editor" })).toBeVisible(); - expect(document.querySelector("[data-open-favorite-editor]")).not.toBeNull(); + await expect + .element(page.getByRole("button", { name: "Open in VS Code", exact: true })) + .toBeEnabled(); + await userEvent.keyboard("{Control>}o{/Control}"); + await vi.waitFor(() => + expect(pickerState.openEditor).toHaveBeenCalledExactlyOnceWith({ + environmentId: ENVIRONMENT_ID, + input: { cwd: "/workspace/render", editor: "vscode" }, + }), + ); + await toggle.click(); + await expect.element(page.getByRole("group", { name: "Open in editor" })).not.toBeInTheDocument(); + await userEvent.keyboard("{Control>}o{/Control}"); + await vi.waitFor(() => expect(pickerState.openEditor).toHaveBeenCalledTimes(2)); + expect(pickerState.openEditor).toHaveBeenLastCalledWith({ + environmentId: ENVIRONMENT_ID, + input: { cwd: "/workspace/render", editor: "vscode" }, + }); }); diff --git a/apps/web/src/remoteOpen.browser.test.tsx b/apps/web/src/remoteOpen.browser.test.tsx index 8ffba1d8e6da6..f3dba8581a470 100644 --- a/apps/web/src/remoteOpen.browser.test.tsx +++ b/apps/web/src/remoteOpen.browser.test.tsx @@ -31,8 +31,8 @@ function Resolution() { } it("marks only matching-home bearer execution and updates provenance when the mode stays local", async () => { - const getHomeEnvironmentId = vi.fn<() => Promise>().mockResolvedValue(null); - vi.stubGlobal("desktopBridge", { getHomeEnvironmentId }); + const getHostEnvironmentId = vi.fn<() => Promise>().mockResolvedValue(null); + vi.stubGlobal("desktopBridge", { getHostEnvironmentId }); using _cleanup = { [Symbol.dispose]() { vi.unstubAllGlobals(); @@ -55,9 +55,9 @@ it("marks only matching-home bearer execution and updates provenance when the mo , ); await expect.element(screen.getByRole("status")).toHaveTextContent("local-exec/false"); - await vi.waitFor(() => expect(getHomeEnvironmentId).toHaveResolvedWith(null)); + await vi.waitFor(() => expect(getHostEnvironmentId).toHaveResolvedWith(null)); - getHomeEnvironmentId.mockResolvedValue(environmentId); + getHostEnvironmentId.mockResolvedValue(environmentId); appAtomRegistry.set(state.presentationAtom!, presentation("paired-service")); await expect.element(screen.getByRole("status")).toHaveTextContent("local-exec/true"); diff --git a/apps/web/src/remoteOpen.test.ts b/apps/web/src/remoteOpen.test.ts index ea93cb6af0e08..6e8c2aec6cd79 100644 --- a/apps/web/src/remoteOpen.test.ts +++ b/apps/web/src/remoteOpen.test.ts @@ -242,13 +242,13 @@ it("recognizes a same-home service after a missing ID without reloading the page using _presentation = vi .spyOn(environmentPresentations, "presentationAtom") .mockReturnValue(presentationAtom); - const getHomeEnvironmentId = vi.fn<() => Promise>().mockResolvedValue(null); - vi.stubGlobal("window", { desktopBridge: { getHomeEnvironmentId } }); + const getHostEnvironmentId = vi.fn<() => Promise>().mockResolvedValue(null); + vi.stubGlobal("window", { desktopBridge: { getHostEnvironmentId } }); try { expect(readDesktopHomeEnvironmentId(environmentId)).toBeNull(); - await vi.waitFor(() => expect(getHomeEnvironmentId).toHaveResolvedWith(null)); + await vi.waitFor(() => expect(getHostEnvironmentId).toHaveResolvedWith(null)); - getHomeEnvironmentId.mockResolvedValue(environmentId); + getHostEnvironmentId.mockResolvedValue(environmentId); appAtomRegistry.set(presentationAtom, { ...presentation, connection: { phase: "connected", error: null, traceId: null }, @@ -257,10 +257,10 @@ it("recognizes a same-home service after a missing ID without reloading the page expect(readDesktopHomeEnvironmentId(environmentId)).toBe(environmentId); }); - const callsAfterConnecting = getHomeEnvironmentId.mock.calls.length; + const callsAfterConnecting = getHostEnvironmentId.mock.calls.length; appAtomRegistry.set(presentationAtom, { ...presentation, serverConfig: null }); expect(readDesktopHomeEnvironmentId(environmentId)).toBe(environmentId); - expect(getHomeEnvironmentId).toHaveBeenCalledTimes(callsAfterConnecting); + expect(getHostEnvironmentId).toHaveBeenCalledTimes(callsAfterConnecting); appAtomRegistry.set(presentationAtom, { ...presentation, @@ -270,7 +270,7 @@ it("recognizes a same-home service after a missing ID without reloading the page }, }); expect(readDesktopHomeEnvironmentId(environmentId)).toBeNull(); - expect(getHomeEnvironmentId).toHaveBeenCalledTimes(callsAfterConnecting); + expect(getHostEnvironmentId).toHaveBeenCalledTimes(callsAfterConnecting); } finally { appAtomRegistry.dispose(); vi.unstubAllGlobals(); diff --git a/apps/web/src/remoteOpen.ts b/apps/web/src/remoteOpen.ts index d41985e50b0d0..5ab361ed8d1f9 100644 --- a/apps/web/src/remoteOpen.ts +++ b/apps/web/src/remoteOpen.ts @@ -117,7 +117,7 @@ const desktopHomeEnvironmentIdAtom = Atom.family((environmentId: EnvironmentId) get.addFinalizer(() => { active = false; }); - const read = window.desktopBridge?.getHomeEnvironmentId; + const read = window.desktopBridge?.getHostEnvironmentId; read?.().then( (id) => { if (active && id !== null) get.setSelf(id); diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 2eaf161468c65..afb87b14bc91f 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -1120,8 +1120,6 @@ export interface DesktopBridge { openSystemSettings: (pane: SystemSettingsPane) => Promise; checkSystemPermission: (pane: SystemSettingsPane) => Promise; probeRemoteEditors: () => Promise; - /** The environment id stored in this desktop's T3 home; null before any server has written it. */ - getHomeEnvironmentId: () => Promise; pasteAsText: () => Promise; onMenuAction: (listener: (action: string) => void) => () => void; onSnapShotEvent: (listener: (event: DesktopSnapShotEvent) => void) => () => void;