Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
65600ed
fix(desktop): reuse remote environments over ssh
Aug 15, 2026
aa25bc2
fix(desktop): make SSH route switching reliable
Aug 15, 2026
24851ca
fix(connection): preserve non-relay routes on sign-out
Aug 22, 2026
d389717
fix(connection): address multiroute review findings
Aug 22, 2026
1343019
fix(ssh): harden runtime discovery handoff
Aug 22, 2026
369d5de
fix(web): keep route picker within narrow rows
Aug 22, 2026
c983f7a
fix(web): serialize route selection controls
Aug 22, 2026
d0733d5
fix(web): show pending route switches
Aug 22, 2026
236484a
fix(web): track pending route switches per environment
Aug 22, 2026
f43aaf8
feat(desktop): add remote port forwarding
Aug 13, 2026
3a14550
fix(desktop): make port forwards reliable
Aug 13, 2026
ff92fbe
fix(desktop): make port forwarding connections reliable
Aug 14, 2026
44e8409
fix(desktop): refresh port forwarding credentials
Aug 14, 2026
fb58842
fix(desktop): follow selected route for port forwards
Aug 16, 2026
432627b
fix(server): forward ports to IPv6 localhost
Aug 16, 2026
d577bdb
fix(desktop): harden port forward lifecycle
Aug 22, 2026
3c34d12
fix(desktop): address port forwarding review findings
Aug 22, 2026
77dee20
fix(desktop): close port forwarding handoff races
Aug 22, 2026
7894314
fix(desktop): serialize port forward state updates
Aug 22, 2026
2804ce0
refactor(web): export port forward error predicates
Aug 22, 2026
2999c92
fix(desktop): clean up failed port authorizations
Aug 22, 2026
15ef13b
fix(desktop): surface port forwarding validation
Aug 22, 2026
8c39d8e
fix(web): theme port forwarding toolbar control
Aug 22, 2026
ee9ea98
fix(web): keep port controls aligned with validation
Aug 22, 2026
255a9cf
fix(web): align port forwarding field semantics
Aug 22, 2026
986953d
fix(web): align port forward status text
Aug 22, 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
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ const migrateSavedEnvironmentRecords = Effect.fn(
return {
schemaVersion: 1,
targets,
routes: targets,
profiles,
credentials,
remoteDpopTokens: [],
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ import {
showContextMenu,
} from "./methods/window.ts";
import * as PreviewIpc from "./methods/preview.ts";
import * as PortForwardIpc from "./methods/portForward.ts";
import { getWslState, setWslBackendEnabled, setWslDistro, setWslOnly } from "./methods/wsl.ts";

export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers")(function* () {
const ipc = yield* DesktopIpc.DesktopIpc;
yield* PreviewIpc.installPreviewEventForwarding();
yield* PortForwardIpc.installPortForwardEventForwarding();

yield* ipc.handleSync(getAppBranding);
yield* ipc.handleSync(getSystemLocale);
Expand Down Expand Up @@ -89,6 +91,12 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(PortForwardIpc.createPortForward);
yield* ipc.handle(PortForwardIpc.listPortForwards);
yield* ipc.handle(PortForwardIpc.stopPortForward);
yield* ipc.handle(PortForwardIpc.stopEnvironmentPortForwards);
yield* ipc.handle(PortForwardIpc.resetEnvironmentPortForwardConnections);
yield* ipc.handle(PortForwardIpc.resolvePortForwardAuthorization);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ export const SET_WSL_BACKEND_ENABLED_CHANNEL = "desktop:set-wsl-backend-enabled"
export const SET_WSL_DISTRO_CHANNEL = "desktop:set-wsl-distro";
export const SET_WSL_ONLY_CHANNEL = "desktop:set-wsl-only";
export const SSH_PASSWORD_PROMPT_CANCELLED_RESULT = "ssh-password-prompt-cancelled";
export const PORT_FORWARD_CREATE_CHANNEL = "desktop:port-forward-create";
export const PORT_FORWARD_LIST_CHANNEL = "desktop:port-forward-list";
export const PORT_FORWARD_STOP_CHANNEL = "desktop:port-forward-stop";
export const PORT_FORWARD_STOP_ENVIRONMENT_CHANNEL = "desktop:port-forward-stop-environment";
export const PORT_FORWARD_RESET_ENVIRONMENT_CONNECTIONS_CHANNEL =
"desktop:port-forward-reset-environment-connections";
export const PORT_FORWARD_RESOLVE_AUTHORIZATION_CHANNEL =
"desktop:port-forward-resolve-authorization";
export const PORT_FORWARD_STATE_CHANNEL = "desktop:port-forward-state";
export const PORT_FORWARD_AUTHORIZATION_REQUEST_CHANNEL =
"desktop:port-forward-authorization-request";
export const PREVIEW_CREATE_TAB_CHANNEL = "desktop:preview-create-tab";
export const PREVIEW_CLOSE_TAB_CHANNEL = "desktop:preview-close-tab";
export const PREVIEW_REGISTER_WEBVIEW_CHANNEL = "desktop:preview-register-webview";
Expand Down
42 changes: 42 additions & 0 deletions apps/desktop/src/ipc/methods/portForward.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
DesktopPortForwardId,
EnvironmentId,
type DesktopPortForwardAuthorizationRequest,
} from "@t3tools/contracts";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import { vi } from "vite-plus/test";

import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as IpcChannels from "../channels.ts";
import { sendPortForwardAuthorizationRequest } from "./portForward.ts";

it.effect("sends port-forward authorization only to the main renderer", () => {
const send = vi.fn();
const sendAll = vi.fn(() => Effect.void);
const mainWindow = { webContents: { send } } as unknown as Electron.BrowserWindow;
const electronWindow = ElectronWindow.ElectronWindow.of({
currentMainOrFirst: Effect.succeed(Option.some(mainWindow)),
sendAll,
} as unknown as ElectronWindow.ElectronWindow["Service"]);
const request: DesktopPortForwardAuthorizationRequest = {
requestId: "request-1",
forwardId: DesktopPortForwardId.make("forward-1"),
environmentId: EnvironmentId.make("environment-1"),
remoteHost: "127.0.0.1",
remotePort: 5174,
};

return sendPortForwardAuthorizationRequest(electronWindow, request).pipe(
Effect.andThen(
Effect.sync(() => {
expect(send).toHaveBeenCalledWith(
IpcChannels.PORT_FORWARD_AUTHORIZATION_REQUEST_CHANNEL,
request,
);
expect(sendAll).not.toHaveBeenCalled();
}),
),
);
});
111 changes: 111 additions & 0 deletions apps/desktop/src/ipc/methods/portForward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
DesktopPortForwardAuthorizationResolution,
type DesktopPortForwardAuthorizationRequest,
DesktopPortForwardCreateInput,
DesktopPortForwardSnapshot,
DesktopPortForwardStopEnvironmentInput,
DesktopPortForwardStopInput,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopPortForwardManager from "../../portForward/DesktopPortForwardManager.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";

export const sendPortForwardAuthorizationRequest = Effect.fn(
"desktop.ipc.portForward.sendAuthorizationRequest",
)(function* (
electronWindow: ElectronWindow.ElectronWindow["Service"],
request: DesktopPortForwardAuthorizationRequest,
) {
const target = yield* electronWindow.currentMainOrFirst;
yield* Option.match(target, {
onNone: () => Effect.void,
onSome: (window) =>
Effect.sync(() => {
window.webContents.send(IpcChannels.PORT_FORWARD_AUTHORIZATION_REQUEST_CHANNEL, request);
}),
});
});

export const installPortForwardEventForwarding = Effect.fn(
"desktop.ipc.portForward.installEventForwarding",
)(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
yield* manager.subscribeStateChanges((snapshots) =>
electronWindow.sendAll(IpcChannels.PORT_FORWARD_STATE_CHANNEL, snapshots),
);
yield* manager.subscribeAuthorizationRequests((request) =>
sendPortForwardAuthorizationRequest(electronWindow, request),
);
});

export const createPortForward = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_CREATE_CHANNEL,
payload: DesktopPortForwardCreateInput,
result: DesktopPortForwardSnapshot,
handler: Effect.fn("desktop.ipc.portForward.create")(function* (input) {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
return yield* manager.create(input);
}),
});

export const listPortForwards = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_LIST_CHANNEL,
payload: Schema.Void,
result: Schema.Array(DesktopPortForwardSnapshot),
handler: Effect.fn("desktop.ipc.portForward.list")(function* () {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
return yield* manager.list;
}),
});

export const stopPortForward = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_STOP_CHANNEL,
payload: DesktopPortForwardStopInput,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.portForward.stop")(function* ({ id }) {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
yield* manager.stop(id);
}),
});

export const stopEnvironmentPortForwards = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_STOP_ENVIRONMENT_CHANNEL,
payload: DesktopPortForwardStopEnvironmentInput,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.portForward.stopEnvironment")(function* ({ environmentId }) {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
yield* manager.stopEnvironment(environmentId);
}),
});

export const resetEnvironmentPortForwardConnections = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_RESET_ENVIRONMENT_CONNECTIONS_CHANNEL,
payload: DesktopPortForwardStopEnvironmentInput,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.portForward.resetEnvironmentConnections")(function* ({
environmentId,
}) {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
yield* manager.resetEnvironmentConnections(environmentId);
}),
});

export const resolvePortForwardAuthorization = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PORT_FORWARD_RESOLVE_AUTHORIZATION_CHANNEL,
payload: DesktopPortForwardAuthorizationResolution,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.portForward.resolveAuthorization")(function* ({
requestId,
socketUrl,
error,
}) {
const manager = yield* DesktopPortForwardManager.DesktopPortForwardManager;
yield* manager.resolveAuthorization(requestId, socketUrl, error);
}),
});
2 changes: 2 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import * as DesktopWindow from "./window/DesktopWindow.ts";
import * as DesktopWslBackend from "./wsl/DesktopWslBackend.ts";
import * as DesktopWslEnvironment from "./wsl/DesktopWslEnvironment.ts";
import * as DesktopWslServerTree from "./wsl/DesktopWslServerTree.ts";
import * as DesktopPortForwardManager from "./portForward/DesktopPortForwardManager.ts";

const desktopEnvironmentLayer = Layer.unwrap(
Effect.gen(function* () {
Expand Down Expand Up @@ -188,6 +189,7 @@ const desktopApplicationLayer = Layer.mergeAll(
DesktopLinuxUrlHandler.layer,
DesktopShellEnvironment.layer,
desktopSshLayer,
DesktopPortForwardManager.layer,
).pipe(
Layer.provideMerge(DesktopUpdates.layer),
Layer.provideMerge(desktopWslBackendLayer),
Expand Down
Loading
Loading