Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ea80cb5
fix: coordinate desktop update entrypoints
Trahloc Nov 7, 2026
ff55e2f
fix: preserve coordinated update retries and feedback
Trahloc Nov 7, 2026
a7b504f
fix(desktop): recover coordinated updates after renderer reload
Trahloc Nov 7, 2026
994a0b7
fix(desktop): start update control with local environment off
Trahloc Nov 7, 2026
729c148
fix(desktop): retain coordinated update ownership across reloads
Trahloc Nov 8, 2026
54ecd47
test(web): keep update regression compatible with ES2022
Trahloc Nov 8, 2026
a42ae4a
fix(desktop): wait for background checks during remote preparation
Trahloc Nov 8, 2026
eae7e28
fix(server): remove duplicate service install guidance
Trahloc Nov 8, 2026
c226590
Merge remote-tracking branch 'github/main' into trahloc/fix/coordinat…
maria-rcks Nov 10, 2026
7558356
fix(desktop): coordinated update requests also check provider updates
maria-rcks Nov 10, 2026
71ec628
Merge remote-tracking branch 'origin/main' into trahloc/fix/coordinat…
juliusmarminge Nov 11, 2026
ec682dd
refactor: trim coordinated update tests and simplify track switch rel…
juliusmarminge Nov 11, 2026
3ea389b
fix(web): keep track switch release compilable by React Compiler
juliusmarminge Nov 11, 2026
df0cce4
fix(web): coordinated update requests offer provider updates found by…
juliusmarminge Nov 11, 2026
fb32df1
fix: decide provider offers from the refresh and keep service update …
juliusmarminge Nov 11, 2026
a6c27f9
test(web): refresh mock returns the provider list the contract promises
juliusmarminge Nov 11, 2026
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
103 changes: 70 additions & 33 deletions apps/desktop/src/app/DesktopApp.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as DesktopUpdateCommand from "../updates/DesktopUpdateCommand.ts";
import { assert, it } from "@effect/vitest";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as Deferred from "effect/Deferred";
Expand Down Expand Up @@ -70,9 +71,13 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as MacPermissions from "../permissions/MacPermissions.ts";
import * as PreviewManager from "../preview/Manager.ts";

it.effect.each([false, true])(
"stops backends together before flushing history and cancels Linux registration, stop defect=%s",
(stopDefect) =>
it.effect.each([
{ localEnvironmentEnabled: true, stopDefect: false },
{ localEnvironmentEnabled: true, stopDefect: true },
{ localEnvironmentEnabled: false, stopDefect: false },
])(
"owns controls through shutdown; local=$localEnvironmentEnabled, stop defect=$stopDefect",
({ localEnvironmentEnabled, stopDefect }) =>
Effect.gen(function* () {
const registrationStarted = yield* Deferred.make<void>();
const releaseRegistration = yield* Deferred.make<void>();
Expand All @@ -84,6 +89,8 @@ it.effect.each([false, true])(
const historyStarted = yield* Deferred.make<void>();
const releaseHistory = yield* Deferred.make<void>();
const quitting = yield* Ref.make(false);
const bootstrapped = yield* Deferred.make<void>();
const shutdown = yield* DesktopShutdown.DesktopShutdown;
const events: string[] = [];
const record = (event: string) =>
Effect.sync(() => {
Expand All @@ -92,6 +99,12 @@ it.effect.each([false, true])(
const runApp = DesktopApp.program
.pipe(
Effect.provide(NodeCrypto.layer),
Effect.provideService(DesktopShutdown.DesktopShutdown, {
...shutdown,
awaitRequest: Deferred.succeed(bootstrapped, undefined).pipe(
Effect.andThen(shutdown.awaitRequest),
),
}),
Effect.provideService(DesktopAppIdentity.DesktopAppIdentity, {
resolveUserDataPath: Effect.succeed("/isolated/userdata"),
configure: record("identity"),
Expand Down Expand Up @@ -123,7 +136,7 @@ it.effect.each([false, true])(
Effect.provideService(DesktopAppSettings.DesktopAppSettings, {
load: Effect.void,
get: Effect.succeed({
localEnvironmentEnabled: true,
localEnvironmentEnabled,
serverExposureMode: "local-only",
}),
} as unknown as DesktopAppSettings.DesktopAppSettings["Service"]),
Expand Down Expand Up @@ -167,40 +180,52 @@ it.effect.each([false, true])(
quitting,
} as DesktopState.DesktopState["Service"]),
Effect.provideService(DesktopWindow.DesktopWindow, {
ensureMain: record("window").pipe(
Effect.andThen(Deferred.succeed(windowStarted, undefined)),
),
ensureMain: localEnvironmentEnabled
? record("window").pipe(Effect.andThen(Deferred.succeed(windowStarted, undefined)))
: Effect.die("disabled local backend must not ensure a backend window"),
createMainIfBackendReady: !localEnvironmentEnabled
? record("window").pipe(Effect.andThen(Deferred.succeed(windowStarted, undefined)))
: Effect.die("enabled startup must retain its backend-first ordering"),
} as unknown as DesktopWindow.DesktopWindow["Service"]),
Effect.provideService(DesktopBackendPool.DesktopBackendPool, {
primary: Effect.succeed({
start: record("backend"),
currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }),
}),
list: Effect.succeed([
{
stop: () =>
Deferred.succeed(firstStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
Effect.andThen(stopDefect ? Effect.die("stop defect") : Effect.void),
),
},
{
stop: () =>
Deferred.succeed(secondStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
),
},
]),
primary: localEnvironmentEnabled
? Effect.succeed({
start: record("backend"),
currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }),
})
: Effect.die("disabled local environment must not access the primary backend"),
list: Effect.succeed(
!localEnvironmentEnabled
? []
: [
{
stop: () =>
Deferred.succeed(firstStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
Effect.andThen(stopDefect ? Effect.die("stop defect") : Effect.void),
),
},
{
stop: () =>
Deferred.succeed(secondStopStarted, undefined).pipe(
Effect.andThen(Deferred.await(releaseStops)),
),
},
],
),
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]),
Effect.provideService(DesktopServerExposure.DesktopServerExposure, {
configureFromSettings: () => Effect.succeed({ mode: "local-only" }),
configureFromSettings: () =>
localEnvironmentEnabled
? Effect.succeed({ mode: "local-only" })
: Effect.die("disabled local environment must not configure a server"),
backendConfig: Effect.succeed({ httpBaseUrl: new URL("http://127.0.0.1:3773") }),
} as unknown as DesktopServerExposure.DesktopServerExposure["Service"]),
Effect.provideService(DesktopWslBackend.DesktopWslBackend, {
reconcile: Effect.void,
} as DesktopWslBackend.DesktopWslBackend["Service"]),
Effect.provideService(DesktopAppActivation.DesktopAppActivation, {
start: Effect.void,
start: Effect.acquireRelease(record("control"), () => record("control-closed")),
} as unknown as DesktopAppActivation.DesktopAppActivation["Service"]),
Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, {
shutdown: Deferred.succeed(historyStarted, undefined).pipe(
Expand All @@ -213,6 +238,7 @@ it.effect.each([false, true])(
Effect.provide(
Layer.mergeAll(
Layer.mock(BrowserImport.BrowserImport, {}),
Layer.mock(DesktopUpdateCommand.DesktopUpdateCommand, {}),
Layer.mock(DesktopBackendConfiguration.DesktopBackendConfiguration, {}),
Layer.mock(DesktopCliCommand.DesktopCliCommand, {}),
Layer.mock(DesktopClientSettings.DesktopClientSettings, {}),
Expand Down Expand Up @@ -248,13 +274,16 @@ it.effect.each([false, true])(
);
yield* Deferred.await(registrationStarted);
yield* Deferred.await(windowStarted);
assert.deepEqual(events, ["identity", "ready", "identity", "backend", "window"]);
yield* Deferred.await(bootstrapped);
const startupEvents = [...events];

assert.isFalse(yield* Deferred.isDone(registrationStopped));
const shutdown = yield* DesktopShutdown.DesktopShutdown;
yield* shutdown.request;
yield* Deferred.await(firstStopStarted);
yield* Deferred.await(secondStopStarted);
assert.isFalse(yield* Deferred.isDone(historyStarted));
if (localEnvironmentEnabled) {
yield* Deferred.await(firstStopStarted);
yield* Deferred.await(secondStopStarted);
assert.isFalse(yield* Deferred.isDone(historyStarted));
}
assert.isFalse(yield* shutdown.isComplete);
yield* Deferred.succeed(releaseStops, undefined);
yield* Deferred.await(historyStarted);
Expand All @@ -264,5 +293,13 @@ it.effect.each([false, true])(
assert.equal(Exit.isFailure(result), stopDefect);
assert.isTrue(yield* shutdown.isComplete);
assert.isTrue(yield* Deferred.isDone(registrationStopped));
assert.deepEqual(
startupEvents,
localEnvironmentEnabled
? ["identity", "ready", "identity", "backend", "window", "control"]
: ["identity", "ready", "identity", "window", "control"],
);

assert.equal(events.filter((event) => event === "control-closed").length, 1);
}).pipe(Effect.provide(DesktopShutdown.layer)),
);
10 changes: 6 additions & 4 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ const bootstrap = Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const snapShot = yield* DesktopSnapShot.DesktopSnapShot;
const appActivation = yield* DesktopAppActivation.DesktopAppActivation;
const startAppControl = appActivation.start.pipe(
Effect.tap(() => logBootstrapInfo("desktop app control socket ready")),
Effect.catch((error) => logStartupError("desktop app control socket unavailable", { error })),
);
yield* logBootstrapInfo("bootstrap start");

const settings = yield* desktopSettings.get;
Expand Down Expand Up @@ -210,6 +214,7 @@ const bootstrap = Effect.gen(function* () {
yield* logBootstrapInfo("bootstrap skipping local environment (disabled in settings)");
if (!(yield* Ref.get(state.quitting))) {
yield* desktopWindow.createMainIfBackendReady;
yield* startAppControl;
}
return;
}
Expand Down Expand Up @@ -274,10 +279,7 @@ const bootstrap = Effect.gen(function* () {
) {
yield* desktopWindow.ensureMain;
}
yield* appActivation.start.pipe(
Effect.tap(() => logBootstrapInfo("desktop app control socket ready")),
Effect.catch((error) => logStartupError("desktop app control socket unavailable", { error })),
);
yield* startAppControl;
yield* Effect.forkScoped(wslBackend.reconcile);
}
}).pipe(Effect.withSpan("desktop.bootstrap"));
Expand Down
49 changes: 45 additions & 4 deletions apps/desktop/src/app/DesktopAppActivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ProjectId,
ThreadId,
type DesktopAppActivationRequest,
type DesktopAppActivationResponse,
type DesktopAppControlResponse,
} from "@t3tools/contracts";
import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
import * as HostProcess from "@t3tools/shared/HostProcess";
Expand Down Expand Up @@ -63,8 +63,8 @@ function startOkServer(target: ReturnType<typeof makeTarget>, userId: number | u
});
}

function exchange(address: string, payload: DesktopAppActivationRequest) {
return new Promise<DesktopAppActivationResponse>((resolve, reject) => {
function exchange(address: string, payload: unknown) {
return new Promise<DesktopAppControlResponse>((resolve, reject) => {
const socket = NodeNet.createConnection(address);
socket.setEncoding("utf8");
let buffer = "";
Expand All @@ -75,12 +75,53 @@ function exchange(address: string, payload: DesktopAppActivationRequest) {
const newline = buffer.indexOf("\n");
if (newline === -1) return;
socket.destroy();
resolve(JSON.parse(buffer.slice(0, newline)) as DesktopAppActivationResponse);
resolve(JSON.parse(buffer.slice(0, newline)) as DesktopAppControlResponse);
});
});
}

describe("desktop app control server", () => {
it.effect("routes a read-only update check without activating a workspace", () =>
Effect.gen(function* () {
const platform = yield* HostProcess.Platform;
const userId = yield* HostProcess.UserId;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-update-check-"));
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
const server = await startDesktopAppControlServer({
...target,
userId,
handle: async () => {
throw new Error("A check must not activate a workspace");
},
handleUpdate: async (input) => ({
version: 1,
requestId: input.requestId,
ok: true,
check: {
schema: "t3.update-check.v1",
coordinated: true,
owner: "desktop",
pathway: "desktop-coordinated",
currentVersion: "0.1.7",
targetVersion: "0.1.8",
state: "ready",
},
}),
cancel: () => undefined,
onReclaimError: () => undefined,
});
openServers.push(server);
const response = await exchange(target.address, {
version: 1,
requestId: "update-check",
type: "update-check",
});
expect(response).toMatchObject({ ok: true, check: { coordinated: true, state: "ready" } });
await NodeFSP.rm(root, { recursive: true, force: true });
});
}),
);
it.effect("cancels a queued request when the client disconnects", () =>
Effect.gen(function* () {
const platform = yield* HostProcess.Platform;
Expand Down
30 changes: 28 additions & 2 deletions apps/desktop/src/app/DesktopAppActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import * as NodePath from "node:path";
import {
DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,
DesktopAppActivationRequest,
DesktopUpdateCommandRequest,
type DesktopUpdateCommandResponse,
type DesktopAppControlResponse,
type DesktopAppActivationResponse,
} from "@t3tools/contracts";
import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
Expand All @@ -26,6 +29,7 @@ import type * as Electron from "electron";

import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL } from "../ipc/channels.ts";
import * as DesktopUpdateCommand from "../updates/DesktopUpdateCommand.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import { DesktopAppActivationBroker } from "./DesktopAppActivationBroker.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
Expand All @@ -34,6 +38,7 @@ import { makeComponentLogger } from "./DesktopObservability.ts";
const MAX_REQUEST_BYTES = 64 * 1024;
const REQUEST_TIMEOUT_MS = 15_000;
const isDesktopAppActivationRequest = Schema.is(DesktopAppActivationRequest);
const isDesktopUpdateCommandRequest = Schema.is(DesktopUpdateCommandRequest);

export class DesktopAppActivationStartError extends Schema.TaggedError<DesktopAppActivationStartError>()(
"DesktopAppActivationStartError",
Expand Down Expand Up @@ -110,6 +115,10 @@ export async function startDesktopAppControlServer(input: {
readonly directory: string | null;
readonly userId: number | undefined;
readonly handle: (request: DesktopAppActivationRequest) => Promise<DesktopAppActivationResponse>;
readonly cancelUpdate?: (requestId: string) => void;
readonly handleUpdate?: (
request: DesktopUpdateCommandRequest,
) => Promise<DesktopUpdateCommandResponse>;
readonly cancel: (requestId: string) => void;
readonly onReclaimError: (error: unknown) => void;
}): Promise<RunningControlServer> {
Expand All @@ -124,7 +133,7 @@ export async function startDesktopAppControlServer(input: {

socket.setTimeout(5_000, () => socket.destroy());

const finish = (response: DesktopAppActivationResponse) => {
const finish = (response: DesktopAppControlResponse) => {
responseSent = true;
if (!socket.destroyed) socket.end(`${JSON.stringify(response)}\n`);
};
Expand All @@ -151,6 +160,15 @@ export async function startDesktopAppControlServer(input: {
return;
}

if (isDesktopUpdateCommandRequest(parsed) && input.handleUpdate) {
activeRequestId = parsed.requestId;
void input
.handleUpdate(parsed)
.then(finish, () =>
finish(invalidResponse(parsed.requestId, "The desktop update request failed.")),
);
return;
}
if (!isDesktopAppActivationRequest(parsed)) {
pipe(
requestIdFromUnknown(parsed),
Expand All @@ -169,7 +187,10 @@ export async function startDesktopAppControlServer(input: {
socket.on("error", () => socket.destroy());
socket.on("close", () => {
sockets.delete(socket);
if (!responseSent && activeRequestId !== null) input.cancel(activeRequestId);
if (!responseSent && activeRequestId !== null) {
input.cancel(activeRequestId);
input.cancelUpdate?.(activeRequestId);
}
});
};

Expand Down Expand Up @@ -298,6 +319,7 @@ const { logWarning } = makeComponentLogger("desktop-app-activation");
export const make = Effect.gen(function* () {
const desktopEnvironment = yield* DesktopEnvironment.DesktopEnvironment;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const updateCommand = yield* DesktopUpdateCommand.DesktopUpdateCommand;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const path = yield* Path.Path;
const userId = yield* HostProcess.UserId;
Expand Down Expand Up @@ -338,6 +360,10 @@ export const make = Effect.gen(function* () {
...address,
userId,
handle: (request) => broker.request(request),
handleUpdate: (request) => runPromise(updateCommand.request(request)),
cancelUpdate: (requestId) => {
void runPromise(updateCommand.cancel(requestId));
},
cancel: (requestId) => broker.cancel(requestId),
onReclaimError: (cause) =>
void runPromise(
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopCliShim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const writeShim = (root: string, target: CliShimTarget) => {
const run = (shim: string, args: ReadonlyArray<string>, env: NodeJS.ProcessEnv = {}) =>
NodeChildProcess.spawnSync(shim, args, {
encoding: "utf8",
env: { PATH: process.env.PATH, ...env },
env: { PATH: process.env.PATH, TMPDIR: NodePath.dirname(shim), ...env },
});

const REPORTER =
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/app/DesktopCliShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const renderCliShim = (input: {
]
: []),
`set "T3CODE_CLI_PATH=${cmdText(input.shimPath)}"`,
'set "T3CODE_DESKTOP_CLI=1"',
`if not defined T3CODE_HOME set "T3CODE_HOME=${cmdText(input.t3Home)}"`,
'set "ELECTRON_RUN_AS_NODE=1"',
// Electron's Windows executable starts without stdin; this tells the CLI to reopen the console.
Expand All @@ -72,6 +73,7 @@ export const renderCliShim = (input: {
"#!/bin/sh",
`# ${MARKER}`,
`export T3CODE_CLI_PATH=${shellWord(input.shimPath)}`,
"export T3CODE_DESKTOP_CLI=1",
`home=${shellWord(input.t3Home)}`,
'export T3CODE_HOME="${T3CODE_HOME:-$home}"',
"export ELECTRON_RUN_AS_NODE=1",
Expand Down
Loading
Loading