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;