Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
5e22256
feat(web): Add provider button sits with the provider list (#17152)
t3dotgg Oct 8, 2026
a4c9494
fix(web): link pull requests to threads in folders that aren't Git re…
shivamhwp Oct 8, 2026
12069ee
feat(web): find messages and plans in the current thread (#10439)
Noojuno Oct 8, 2026
9a3070b
feat(web): improve terminal scrollback navigation and snapshots (#17091)
StiensWout Oct 8, 2026
a65b438
docs(internals): add a checklist for adding a provider (#17229)
juliusmarminge Oct 8, 2026
a6ec88f
fix(mobile): keep native screens ordered during stack pops (#17231)
juliusmarminge Oct 8, 2026
4daec10
fix(server): pairing tokens work on Node versions that cannot bind bo…
chisewaguri Oct 8, 2026
61b9790
fix(mobile): HTML pages in a thread no longer trap scrolling on Andro…
SunkenInTime Oct 8, 2026
07a8b9e
fix(web): centered scrollers no longer shift when the scrollbar appea…
maria-rcks Oct 8, 2026
62a5d37
fix(web): distinguish thread search matches from code tints (#17263)
Yash-Singh1 Oct 8, 2026
f999eb2
fix(server): Pi extension wakes get an owned continuation turn (#17214)
StiensWout Oct 8, 2026
de72286
fix(server): Pi discovers optional T3 tools on demand (#17220)
StiensWout Oct 8, 2026
eb1cf7b
fix(web): stack merge dialog closes as soon as you confirm (#17116)
flamboh Oct 8, 2026
e8f5850
fix(server): Pi editor dialogs prefill the answer composer (#17206)
StiensWout Oct 8, 2026
a0066d5
fix(desktop): generate valid User-Agent that follows RFC 9110 guideli…
jztmanyl Oct 8, 2026
0b6ec43
fix(server): Pi discovers workspace skills and commands (#17190)
StiensWout Oct 8, 2026
805967a
fix(mobile): preserve navigation after native swipe back (#17268)
juliusmarminge Oct 8, 2026
fd25c42
fix(server): keep newly discovered models out of legacy groups (#14314)
Bil0000 Oct 8, 2026
df616cc
feat(editors): open remote projects in JetBrains IDEs over SSH (#17271)
juliusmarminge Oct 8, 2026
740f591
test(desktop): expect JetBrains IDEs among remote editors (#17291)
juliusmarminge Oct 8, 2026
cdd331b
fix(server): recognize authenticated GitHub Enterprise hosts (#11059)
alimek Oct 8, 2026
2a93885
fix(connect): relay client updates itself and skips incompatible clou…
juliusmarminge Oct 8, 2026
bd565bd
fix(shared): relay client install waits out a brief Windows file lock…
ScottN-PV Oct 8, 2026
c377d1c
fix(shared): release relay install locks on cancellation (#10585)
yashranaway Oct 8, 2026
af3d842
chore(shared): bump managed cloudflared to 2026.10.0 (#11184)
bompus Oct 8, 2026
5809487
fix(shared): bound cloudflared download with 10-minute timeout (#14139)
kvnloo Oct 8, 2026
9eea28b
refactor(provider-core): add provider-core and provider-testing packa…
juliusmarminge Oct 8, 2026
2c11607
refactor(settings): drop the legacy per-driver providers map (#17300)
juliusmarminge Oct 8, 2026
9b0df13
refactor(provider-pi): move Pi into its own provider package (#17302)
juliusmarminge Oct 8, 2026
7b57b72
feat(models): tell users when a CLI update unlocks a new model (#17307)
juliusmarminge Oct 8, 2026
48b71f0
fix(web): collapsed composer reserves room for wide send actions (#17…
maria-rcks Oct 8, 2026
d81afa0
fix(muse): workflow subagents no longer stall on hidden approvals (#1…
t3dotgg Oct 8, 2026
e38b6a1
refactor(provider-core): share attachment prompts, notifications, and…
juliusmarminge Oct 8, 2026
741377c
fix(web): file previews handle downloads, in-page links, and repo pat…
juliusmarminge Oct 8, 2026
5754121
fix(server): environment-hosted browser tabs behave like a normal bro…
juliusmarminge Oct 8, 2026
d9f7c0c
fix(desktop): browser tab fixes for fullscreen, shortcuts, links, rel…
juliusmarminge Oct 8, 2026
66298aa
fix(web): desktop opens remote environments' browser tabs locally (#1…
juliusmarminge Oct 8, 2026
3bfbc37
fix(desktop): the t3 command warns instead of installing behind anoth…
juliusmarminge Oct 8, 2026
e0c80c1
fix(web): images, video, HTML and PDF preview in a thread before its …
juliusmarminge Oct 8, 2026
5b78b34
refactor(provider-muse): move Muse Code into its own provider package…
juliusmarminge Oct 8, 2026
6308db4
fix(web): semantic branch naming hint lines up with its setting (#16972)
limineol Oct 8, 2026
393ff4c
fix(mobile): restore chat image previews in the v5 stack (#17361)
juliusmarminge Oct 8, 2026
c724c7c
feat(mobile): fade working threads and match web's status labels (#17…
juliusmarminge Oct 9, 2026
dbd8343
fix(server): agent browser tools stop bloating history, fall back sen…
juliusmarminge Oct 9, 2026
29980a3
fix(web): add room for thread timeline markers (#17372)
Yash-Singh1 Oct 9, 2026
b0ee7dd
fix(web): drop sidebar context before cancelling pointer drag (#17373)
Yash-Singh1 Oct 9, 2026
2c7a446
refactor(providers): namespace-import service modules in core, Muse, …
juliusmarminge Oct 9, 2026
456930a
fix(auth): show connection permissions and enforce session lifetime (…
juliusmarminge Oct 9, 2026
e62161b
refactor(provider-opencode): move OpenCode into its own provider pack…
juliusmarminge Oct 9, 2026
b5e4f1e
refactor(provider-cursor): move Cursor into its own provider package …
juliusmarminge Oct 9, 2026
664bd5e
refactor(provider-acp): move the shared ACP adapter into its own pack…
juliusmarminge Oct 9, 2026
b707eeb
refactor(provider-grok): move Grok into its own provider package (#17…
juliusmarminge Oct 9, 2026
42c6623
fix(server): speed up long thread message sync (#17387)
Yash-Singh1 Oct 9, 2026
c9fa136
fix(desktop): cancel backend pipe reads to avoid slow shutdown (#17386)
Yash-Singh1 Oct 9, 2026
b000eac
refactor(providers): adapter factories yield their services (#17381)
juliusmarminge Oct 9, 2026
5785df1
fix(web): show a row spinner instead of a banner when expanding a fol…
juliusmarminge Oct 9, 2026
8f76037
fix(server): a timed-out browser drag no longer exits the server (#17…
ScottN-PV Oct 9, 2026
563645c
fix(server): a logged-out Claude CLI no longer reports as authenticat…
yordis Oct 9, 2026
3b6af0b
fix(server): Pi loads every selected skill without losing prompt text…
StiensWout Oct 9, 2026
c908cea
fix(server): keep the Claude MCP token out of process arguments (#17408)
juliusmarminge Oct 9, 2026
7a2b1c7
fix(server): reconcile Pi native session rewinds (#13839)
StiensWout Oct 9, 2026
cb46c62
test(provider-pi): cover continuation offers through the driver (#17407)
juliusmarminge Oct 9, 2026
43f8a8d
refactor(provider-acp-registry): move the ACP Registry into its own p…
juliusmarminge Oct 9, 2026
101f8b2
fix(server): relay client updates no longer drop the host off T3 Conn…
t3dotgg Oct 9, 2026
ec80933
fix(connect): Cloudflare and other VPN addresses no longer show as Ta…
shivamhwp Oct 9, 2026
43f6f3a
chore: sync upstream/main at ec80933ac
Fryuni Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@t3tools/tailscale": "workspace:*",
"dbus-next": "0.10.2",
"effect": "catalog:",
"electron": "44.4.2",
"electron": "44.4.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.8.9",
"ffi-rs": "1.3.2",
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe("DesktopAppIdentity", () => {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
yield* identity.configure;

assert.deepEqual(calls.setName, ["T3 Code (Alpha)"]);
assert.deepEqual(calls.setName, ["T3 Code Alpha"]);
assert.equal(calls.setAboutPanelOptions[0]?.applicationName, "T3 Code (Alpha)");
assert.equal(calls.setAboutPanelOptions[0]?.applicationVersion, "1.2.3");
assert.equal(calls.setAboutPanelOptions[0]?.version, "0123456789ab");
Expand All @@ -237,6 +237,39 @@ describe("DesktopAppIdentity", () => {
);
});

it.effect.each([
{ stage: "Alpha", environment: {} },
{
stage: "Nightly",
environment: { appVersion: "0.0.43-nightly.20260929.2428" },
},
{
stage: "Dev",
environment: { env: { VITE_DEV_SERVER_URL: "http://localhost:5173" } },
},
])("uses a valid native User-Agent product name for $stage", ({ stage, environment }) => {
const calls: ElectronAppCalls = {
setAboutPanelOptions: [],
setDockIcon: [],
setName: [],
};

return withIdentity(
Effect.gen(function* () {
const identity = yield* DesktopAppIdentity.DesktopAppIdentity;
yield* identity.configure;

const runtimeName = calls.setName[0];
assert.isDefined(runtimeName);
assert.equal(runtimeName, `T3 Code ${stage}`);
// RFC 9110's token grammar, after Electron removes ASCII spaces.
assert.match(runtimeName.replaceAll(" ", ""), /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/);
assert.equal(calls.setAboutPanelOptions[0]?.applicationName, `T3 Code (${stage})`);
}),
{ calls, environment },
);
});

it.effect("sets the dock icon only when running unpackaged", () => {
const calls: ElectronAppCalls = {
setAboutPanelOptions: [],
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/app/DesktopAppIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ export const make = Effect.gen(function* () {

const configure = Effect.gen(function* () {
const commitHash = yield* resolveAboutCommitHash;
yield* electronApp.setName(environment.displayName);
// Electron removes spaces from this name to build the native User-Agent
// product token, but leaves parentheses intact. Keep the runtime name valid
// without rewriting preview sessions (which breaks Turnstile, #7110).
yield* electronApp.setName(
`${environment.branding.baseName} ${environment.branding.stageLabel}`,
);
yield* electronApp.setAboutPanelOptions({
applicationName: environment.displayName,
applicationVersion: environment.appVersion,
Expand Down
32 changes: 24 additions & 8 deletions apps/desktop/src/app/DesktopCliCommand.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import { afterEach, beforeEach, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Option from "effect/Option";
Expand Down Expand Up @@ -85,6 +85,15 @@ const fakePowerShell = (initial: string) => {
return { registry, spawner };
};

// The machine's own PATH may already hold a t3; each test sets the PATH it means.
const machinePath = process.env.PATH;
beforeEach(() => {
process.env.PATH = "";
});
afterEach(() => {
process.env.PATH = machinePath;
});

it.layer(NodeServices.layer)("DesktopCliCommand", (it) => {
it.effect("links the launcher onto PATH and removes only that link", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -165,24 +174,31 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => {
}).pipe(Effect.scoped),
);

it.effect("reports when another t3 earlier on PATH would run instead", () =>
it.effect("refuses to install behind another t3 that runs first", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped();
const shadow = path.join(home, "shadow");
yield* fs.makeDirectory(shadow);
yield* fs.writeFileString(path.join(shadow, "t3"), "#!/bin/sh\n", { mode: 0o755 });
const previous = process.env.PATH;
process.env.PATH = [shadow, path.join(home, ".local", "bin")].join(":");
yield* Effect.addFinalizer(() => Effect.sync(() => (process.env.PATH = previous)));

const command = yield* commandIn({ home });
const theirs = path.join(shadow, "t3");
expect((yield* command.state).shadowedBy).toBe(theirs);
// A link behind it would never run, so nothing is created.
const error = yield* Effect.flip(command.install);
expect(error.message).toContain(theirs);
expect(yield* fs.exists(path.join(home, ".local", "bin", "t3"))).toBe(false);

yield* fs.remove(theirs);
const installed = yield* command.install;
expect(installed.installedPath).toBe(path.join(home, ".local", "bin", "t3"));
expect(installed.onPath).toBe(false);
yield* fs.remove(path.join(shadow, "t3"));
expect((yield* command.state).onPath).toBe(true);
expect(installed).toMatchObject({
onPath: true,
installedPath: path.join(home, ".local", "bin", "t3"),
});
expect(installed.shadowedBy).toBeUndefined();
}).pipe(Effect.scoped),
);

Expand Down
23 changes: 21 additions & 2 deletions apps/desktop/src/app/DesktopCliCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ export const make = Effect.gen(function* () {
return Option.none<string>();
});

/** The `t3` a new shell runs when it is not this app's, on Unix. */
const foreignFirstOnPath = Effect.gen(function* () {
if (windows) return Option.none<string>();
const first = yield* firstOnPath;
if (Option.isNone(first) || (yield* isOurLink(first.value))) return Option.none<string>();
return first;
});

/** Where this app's command is installed now, if anywhere. */
const installedAt = Effect.gen(function* () {
if (windows) {
Expand All @@ -167,12 +175,16 @@ export const make = Effect.gen(function* () {
return { supported: false, installedPath: null, onPath: false } as const;
}
const installed = yield* installedAt;
if (Option.isNone(installed)) return { supported: true, installedPath: null, onPath: false };
const shadowedBy = yield* foreignFirstOnPath;
const shadow = Option.isSome(shadowedBy) ? { shadowedBy: shadowedBy.value } : {};
if (Option.isNone(installed)) {
return { supported: true, installedPath: null, onPath: false, ...shadow };
}
// On Windows only terminals opened after the change see it. On Unix the
// first `t3` on PATH must be ours; a `t3` earlier on PATH would shadow it.
const first = yield* firstOnPath;
const onPath = windows || (Option.isSome(first) && (yield* isOurLink(first.value)));
return { supported: true, installedPath: installed.value, onPath };
return { supported: true, installedPath: installed.value, onPath, ...shadow };
}).pipe(Effect.orElseSucceed(() => ({ supported: false, installedPath: null, onPath: false })));

/** Writes the launcher if the app has not yet, e.g. when no local backend runs. */
Expand Down Expand Up @@ -209,6 +221,13 @@ export const make = Effect.gen(function* () {
.remove(existing.value)
.pipe(Effect.mapError(() => fail(`Could not replace ${existing.value}.`)));
}
// A link behind another `t3` never runs, so installing one would only hide the problem.
const shadowedBy = yield* foreignFirstOnPath;
if (Option.isSome(shadowedBy)) {
return yield* fail(
`Another t3 at ${shadowedBy.value} runs first in a new terminal. Remove it, or run the launcher directly at ${launcher}.`,
);
}
const onPath = pathEntries(process.env.PATH, ":");
const candidates = unixCandidates(environment.homeDirectory, environment.platform);
// Prefer a folder already on PATH that the user can write to without admin rights.
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function layerDesktopWindow(
dispatchMenuAction: () => Effect.void,
dispatchSnapShotEvent: () => Effect.void,
zoomMain: () => Effect.void,
runMainContentsCommand: () => Effect.void,
syncAppearance: Effect.void,
});
}
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function layerPool(labelRef: Ref.Ref<string>): Layer.Layer<DesktopBackendPool.De
dispatchMenuAction: () => Effect.die("unexpected menu action"),
dispatchSnapShotEvent: () => Effect.void,
zoomMain: () => Effect.die("unexpected zoom"),
runMainContentsCommand: () => Effect.die("unexpected main contents command"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
),
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/electron/ElectronDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ export class ElectronDialog extends Context.Service<
readonly pickFiles: (
input: ElectronDialogPickFilesInput,
) => Effect.Effect<readonly string[], ElectronDialogPickFilesError>;
/** Pass `owner` to attach the box to that window so it cannot fall behind it. */
readonly showMessageBox: (
options: Electron.MessageBoxOptions,
owner?: Electron.BrowserWindow,
) => Effect.Effect<Electron.MessageBoxReturnValue, ElectronDialogShowMessageBoxError>;
readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>;
}
Expand Down Expand Up @@ -164,9 +166,12 @@ export const make = ElectronDialog.of({
});
return result.canceled ? [] : result.filePaths;
}),
showMessageBox: (options) =>
showMessageBox: (options, owner) =>
Effect.tryPromise({
try: () => Electron.dialog.showMessageBox(options),
try: () =>
owner === undefined
? Electron.dialog.showMessageBox(options)
: Electron.dialog.showMessageBox(owner, options),
catch: (cause) =>
new ElectronDialogShowMessageBoxError({
type: options.type ?? null,
Expand Down
22 changes: 20 additions & 2 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ describe("ElectronShell", () => {
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("opens the JetBrains Toolbox ssh deep link", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const url =
"jetbrains://gateway/ssh/environment?h=example.com&launchIde=true&ideHint=IU&projectHint=%2Fhome%2Fuser%2Fproject";
const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal(url);

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [[url]]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open editor URLs that mix up link shapes", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);
Expand All @@ -114,9 +128,12 @@ describe("ElectronShell", () => {
const results = yield* Effect.all([
electronShell.openExternal("zed://extension/attacker"),
electronShell.openExternal("vscode://ssh/example.com/home/user/project"),
electronShell.openExternal("jetbrains://idea/navigate/reference?project=x&path=y"),
electronShell.openExternal("jetbrains://gateway/ssh/environment?launchIde=true"),
electronShell.openExternal("jetbrains://gateway/com.example.provider/connect?h=x"),
]);

assert.deepEqual(results, [false, false]);
assert.deepEqual(results, [false, false, false, false, false]);
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);
Expand All @@ -134,9 +151,10 @@ describe("ElectronShell", () => {
"vscode://:secret@vscode-remote/ssh-remote+example.com/home/user/project",
),
electronShell.openExternal("zed://ssh/user@example.com/home/user/project"),
electronShell.openExternal("jetbrains://user@gateway/ssh/environment?h=example.com"),
]);

assert.deepEqual(results, [false, false, false]);
assert.deepEqual(results, [false, false, false, false]);
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);
Expand Down
14 changes: 9 additions & 5 deletions apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import * as Option from "effect/Option";
import * as Electron from "electron";

// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`,
// `zed://ssh/<host>/<path>`) must reach the OS handler; every other non-web
// scheme stays blocked.
// `zed://ssh/<host>/<path>`, `jetbrains://gateway/ssh/environment?h=…`) must
// reach the OS handler; every other non-web scheme stays blocked.
const SAFE_WEB_PROTOCOLS = new Set(["http:", "https:"]);
const REMOTE_EDITOR_PROTOCOLS = new Set(
REMOTE_CAPABLE_EDITOR_IDS.flatMap((id) => {
Expand All @@ -31,9 +31,13 @@ const isRemoteEditorUrl = (url: URL) =>
url.password.length === 0 &&
(url.protocol === "zed:"
? url.host === "ssh" && ZED_SSH_PATHNAME.test(url.pathname)
: url.host === "vscode-remote" &&
url.pathname.startsWith("/ssh-remote+") &&
url.pathname.length > "/ssh-remote+".length);
: url.protocol === "jetbrains:"
? url.host === "gateway" &&
url.pathname === "/ssh/environment" &&
(url.searchParams.get("h") ?? "").length > 0
: url.host === "vscode-remote" &&
url.pathname.startsWith("/ssh-remote+") &&
url.pathname.length > "/ssh-remote+".length);

export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
if (typeof rawUrl !== "string") {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const PREVIEW_RECORDING_SAVE_CHANNEL = "desktop:preview-recording-save";
export const PREVIEW_RECORDING_FRAME_CHANNEL = "desktop:preview-recording-frame";
export const PREVIEW_STATE_CHANGE_CHANNEL = "desktop:preview-state-change";
export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event";
export const PREVIEW_OPEN_LINK_CHANNEL = "desktop:preview-open-link";

export const MAC_PERMISSION_HELPER_CHANNEL = "desktop:mac-permission-helper";

Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/ipc/methods/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export const installPreviewEventForwarding = Effect.fn(
yield* manager.subscribePointerEvents((event) =>
electronWindow.sendAll(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, event),
);
yield* manager.subscribeOpenLinks((event) =>
electronWindow.sendAll(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, event),
);
});

export const setForwardedShortcuts = DesktopIpc.makeIpcMethod({
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,6 @@ it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
);
assert.include(editors, "cursor");
assert.include(editors, "vscode");
assert.notInclude(editors, "webstorm");
assert.include(editors, "webstorm");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
10 changes: 10 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
DesktopBridge,
DesktopPreviewOpenLinkEvent,
DesktopPreviewPointerEvent,
DesktopPreviewRecordingInputEvent,
DesktopPreviewRecordingFrame,
Expand Down Expand Up @@ -416,5 +417,14 @@ contextBridge.exposeInMainWorld("desktopBridge", {
return () =>
ipcRenderer.removeListener(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, wrappedListener);
},
onOpenLink: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, linkEvent: unknown) => {
if (typeof linkEvent !== "object" || linkEvent === null) return;
listener(linkEvent as DesktopPreviewOpenLinkEvent);
};
ipcRenderer.on(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener);
return () =>
ipcRenderer.removeListener(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener);
},
},
} satisfies DesktopBridge);
Loading
Loading