Skip to content
Closed
2 changes: 1 addition & 1 deletion .agents/skills/test-t3-mobile/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Use these client origins:
- Android Emulator: `http://10.0.2.2:<server-port>`
- Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin

Enter the complete `http://` origin to make the test transport explicit. Bare IP addresses default to HTTP, while bare hostnames default to HTTPS. When testing web and mobile together, run `vp run dev --home-dir <base-dir> --host 127.0.0.1` instead and do not launch a second backend over the same base directory.
Always enter the complete `http://` origin; the mobile host field otherwise assumes HTTPS. When testing web and mobile together, run `vp run dev --home-dir <base-dir> --host 127.0.0.1` instead and do not launch a second backend over the same base directory.

## Start or reuse Metro safely

Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/app/DesktopApp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vite-plus/test";
import * as Option from "effect/Option";

import { resolveDesktopBackendPortHint } from "./DesktopApp.ts";

describe("resolveDesktopBackendPortHint", () => {
it("keeps the renderer protocol aligned with a reused live backend", () => {
expect(resolveDesktopBackendPortHint("http://127.0.0.1:8080/", Option.some(3773))).toEqual(
Option.some(8080),
);
});

it("uses the configured port when no live backend exists", () => {
expect(resolveDesktopBackendPortHint(undefined, Option.some(4949))).toEqual(Option.some(4949));
});

it("ignores a malformed live backend marker", () => {
expect(resolveDesktopBackendPortHint("not a URL", Option.some(4949))).toEqual(
Option.some(4949),
);
});
});
38 changes: 37 additions & 1 deletion apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopDeepLinks from "./DesktopDeepLinks.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
Expand All @@ -23,6 +24,7 @@ import * as DesktopObservability from "./DesktopObservability.ts";
import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
import { readLiveExistingBackend } from "../backend/DesktopExistingBackend.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
Expand Down Expand Up @@ -66,6 +68,24 @@ const { logInfo: logBootstrapInfo, logWarning: logBootstrapWarning } =
const { logInfo: logStartupInfo, logError: logStartupError } =
DesktopObservability.makeComponentLogger("desktop-startup");

export function resolveDesktopBackendPortHint(
existingBackendHttpBaseUrl: string | undefined,
configuredPort: Option.Option<number>,
): Option.Option<number> {
if (existingBackendHttpBaseUrl !== undefined) {
try {
const port = Number.parseInt(new URL(existingBackendHttpBaseUrl).port, 10);
if (Number.isSafeInteger(port) && port > 0 && port <= MAX_TCP_PORT) {
return Option.some(port);
}
} catch {
// Fall through to the configured port when the live marker is malformed.
}
}

return configuredPort;
}

const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* (
configuredPort: Option.Option<number>,
) {
Expand Down Expand Up @@ -154,7 +174,10 @@ const bootstrap = Effect.gen(function* () {
return yield* new DesktopDevelopmentBackendPortRequiredError();
}

const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);
const existingBackend = readLiveExistingBackend(environment.stateDir);
const backendPortSelection = yield* resolveDesktopBackendPort(
resolveDesktopBackendPortHint(existingBackend?.httpBaseUrl, environment.configuredBackendPort),
);
const backendPort = backendPortSelection.port;
yield* logBootstrapInfo(
backendPortSelection.selectedByScan
Expand All @@ -165,6 +188,13 @@ const bootstrap = Effect.gen(function* () {
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
},
);
if (existingBackend) {
yield* logBootstrapInfo("reusing existing backend for shared T3 home", {
pid: existingBackend.pid,
httpBaseUrl: existingBackend.httpBaseUrl,
stateDir: existingBackend.stateDir,
});
}

const settings = yield* desktopSettings.get;
if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {
Expand Down Expand Up @@ -216,6 +246,12 @@ const bootstrap = Effect.gen(function* () {
// slow first wsl.exe spawn.
yield* Effect.forkScoped(wslBackend.reconcile);
}

// Catalog + window services are usable; flush any deep link captured from
// initial argv / open-url during single-instance setup.
const deepLinks = yield* DesktopDeepLinks.DesktopDeepLinks;
yield* deepLinks.start;
yield* logBootstrapInfo("bootstrap deep links ready");
}).pipe(Effect.withSpan("desktop.bootstrap"));

const startup = Effect.gen(function* () {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>
calls.setAboutPanelOptions.push(options);
}),
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
Expand Down
46 changes: 19 additions & 27 deletions apps/desktop/src/app/DesktopAppIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,6 @@ const normalizeCommitHash = (value: string): Option.Option<string> => {
: Option.none();
};

export const resolveUserDataPath = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const legacyPath = environment.path.join(
environment.appDataDirectory,
environment.legacyUserDataDirName,
);
const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
Effect.mapError(
(cause) =>
new DesktopUserDataPathResolutionError({
legacyPath,
cause,
}),
),
);
return legacyPathExists
? legacyPath
: environment.path.join(environment.appDataDirectory, environment.userDataDirName);
}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath"));

export const make = Effect.gen(function* () {
const assets = yield* DesktopAssets.DesktopAssets;
const electronApp = yield* ElectronApp.ElectronApp;
Expand Down Expand Up @@ -111,11 +90,24 @@ export const make = Effect.gen(function* () {
return commitHash;
});

const userDataPath = resolveUserDataPath.pipe(
Effect.provide(
yield* Effect.context<DesktopEnvironment.DesktopEnvironment | FileSystem.FileSystem>(),
),
);
const resolveUserDataPath = Effect.gen(function* () {
const legacyPath = environment.path.join(
environment.appDataDirectory,
environment.legacyUserDataDirName,
);
const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe(
Effect.mapError(
(cause) =>
new DesktopUserDataPathResolutionError({
legacyPath,
cause,
}),
),
);
return legacyPathExists
? legacyPath
: environment.path.join(environment.appDataDirectory, environment.userDataDirName);
}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath"));

const configure = Effect.gen(function* () {
const commitHash = yield* resolveAboutCommitHash;
Expand Down Expand Up @@ -144,7 +136,7 @@ export const make = Effect.gen(function* () {
}).pipe(Effect.withSpan("desktop.appIdentity.configure"));

return DesktopAppIdentity.of({
resolveUserDataPath: userDataPath,
resolveUserDataPath,
configure,
});
});
Expand Down
Loading
Loading