Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -12,6 +12,7 @@ import * as ElectronProtocol from "../electron/ElectronProtocol.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 @@ -20,6 +21,7 @@ import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopObservability from "./DesktopObservability.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 @@ -63,6 +65,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 @@ -151,7 +171,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 @@ -162,6 +185,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 @@ -213,6 +243,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