Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
140ee14
fix(server): Codex shadow homes replace stray sqlite maintenance lock…
juliusmarminge Oct 10, 2026
3857132
feat(desktop): T3 Code can be your default web browser on macOS (#17587)
juliusmarminge Oct 10, 2026
202b954
test(server): the ACP process-tree test no longer collides with the r…
yordis Oct 10, 2026
cc62349
fix(web): keep branch restore action inline in narrow composers (#14811)
Saikrishna1876 Oct 10, 2026
3e94afa
fix(web): composer banner actions stay inline whenever they fit (#17640)
maria-rcks Oct 10, 2026
b744bde
feat(web): draft screen project picker is searchable (#17664)
juliusmarminge Oct 10, 2026
b87b13d
fix(server): report incomplete transcript usage scans (#15661)
maria-rcks Oct 10, 2026
d970607
fix(web): every resize-driven layout commits in the same frame (#17656)
maria-rcks Oct 10, 2026
ca40de0
fix(web): right panel and terminal drawer follow the pointer while dr…
maria-rcks Oct 10, 2026
42e1a0f
fix(web): terminal drawer keeps its height after the window shrinks (…
maria-rcks Oct 10, 2026
033866c
perf(web): sidebar drags restyle only the sidebar (#17659)
maria-rcks Oct 10, 2026
eddc8fb
fix(web): server browser page resizes while the panel is dragged (#17…
maria-rcks Oct 10, 2026
0caa95d
fix(storage): make worktree cleanup work and show why it skipped (#17…
maria-rcks Oct 10, 2026
e858ddb
test(usage): usage service tests keep their state directory until cac…
tris203 Oct 10, 2026
f3a69c7
fix(checkpoint): pulls and rebases no longer flood a turn's changed f…
t3dotgg Oct 10, 2026
07b8080
fix(web): place notification icons after titles (#12209)
voltcrash Oct 10, 2026
0b9623f
fix(web): attachments on an open question are visible again (#15537)
tiliakoos Oct 10, 2026
f34adad
fix(web): scale Files tree with interface font size (#8011)
Umais-Adeed Oct 10, 2026
318201c
fix(server): probe only owned preview listeners (#16687)
maria-rcks Oct 10, 2026
ed4ea10
fix(chat): surface pending subagent questions on parents (#16634)
maria-rcks Oct 10, 2026
ee82f1c
feat(mobile): choose the language for iPhone voice input
roprgm Oct 6, 2026
891eb9e
fix(mobile): accented letters count as word boundaries in voice trans…
roprgm Oct 10, 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
60 changes: 59 additions & 1 deletion apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@ import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronShell from "../electron/ElectronShell.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopWebLinks from "./DesktopWebLinks.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopPreReadyFileSystem from "./DesktopPreReadyFileSystem.ts";

/** Clerk forwards web links; tests that are not about links ignore them. */
const ignoreWebLinks = DesktopWebLinks.DesktopWebLinks.of({
receive: () => Effect.void,
setRendererReady: () => Effect.void,
});

const layerDesktopClerk = (
isDevelopment = true,
events: string[] = [],
Expand Down Expand Up @@ -220,11 +227,12 @@ describe("DesktopClerk", () => {

assert.isTrue(Exit.isSuccess(exit));
assert.equal(quit.mock.calls.length, 0);
assert.deepEqual(registeredEvents, ["open-url", "second-instance"]);
assert.deepEqual(registeredEvents, ["open-url", "open-file", "second-instance"]);
}).pipe(
Effect.provide(layerDesktopClerk()),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
);
});

Expand Down Expand Up @@ -253,6 +261,7 @@ describe("DesktopClerk", () => {
Effect.provide(layerDesktopClerk()),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
);
});
});
Expand Down Expand Up @@ -299,6 +308,7 @@ it.effect(
Effect.provide(layerDesktopClerk()),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
);
},
);
Expand Down Expand Up @@ -381,6 +391,54 @@ it.effect.each(["startup", "open-url"] as const)(
ElectronWindow.ElectronWindow,
{} as ElectronWindow.ElectronWindow["Service"],
),
Effect.provideService(DesktopWebLinks.DesktopWebLinks, ignoreWebLinks),
);
}).pipe(Effect.scoped),
);

it.effect("hands a web link to the renderer and leaves other links alone", () =>
Effect.gen(function* () {
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
const listeners = new Map<string, (...args: unknown[]) => void>();
const electronApp = {
whenReady: Effect.void,
on: (name: string, listener: (...args: unknown[]) => void) =>
Effect.sync(() => {
listeners.set(name, listener);
}),
} as unknown as ElectronApp.ElectronApp["Service"];
const received: Array<string> = [];
yield* Effect.gen(function* () {
const clerk = yield* DesktopClerk.DesktopClerk;
yield* clerk.configure;
const open = (url: string) => {
const event = { preventDefault: vi.fn() };
listeners.get("open-url")!(event, url);
return event.preventDefault.mock.calls.length;
};
// macOS hands the default browser every web link.
assert.strictEqual(open("https://example.com/page"), 1);
assert.strictEqual(open("http://localhost:3000/"), 1);
// Anything else is not a web page; Electron keeps its own handling.
assert.strictEqual(open("mailto:hello@example.com"), 0);
yield* Effect.yieldNow;
assert.deepStrictEqual(received, ["https://example.com/page", "http://localhost:3000/"]);
}).pipe(
Effect.provide(layerDesktopClerk(true, [], "darwin")),
Effect.provideService(HostProcess.Arguments, ["t3"]),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(
ElectronWindow.ElectronWindow,
{} as ElectronWindow.ElectronWindow["Service"],
),
Effect.provideService(
DesktopWebLinks.DesktopWebLinks,
DesktopWebLinks.DesktopWebLinks.of({
receive: (url) => Effect.sync(() => void received.push(url)),
setRendererReady: () => Effect.void,
}),
),
);
}).pipe(Effect.scoped),
);
23 changes: 21 additions & 2 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createClerkBridge } from "@clerk/electron";
import * as NodeURL from "node:url";
import { storage } from "@clerk/electron/storage";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand All @@ -18,6 +19,7 @@ import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopUserData from "./DesktopUserData.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopWebLinks from "./DesktopWebLinks.ts";

declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;

Expand Down Expand Up @@ -53,7 +55,10 @@ export class DesktopClerk extends Context.Service<
readonly configure: Effect.Effect<
void,
never,
ElectronApp.ElectronApp | ElectronWindow.ElectronWindow | Scope.Scope
| ElectronApp.ElectronApp
| ElectronWindow.ElectronWindow
| DesktopWebLinks.DesktopWebLinks
| Scope.Scope
>;
}
>()("@t3tools/desktop/app/DesktopClerk") {}
Expand Down Expand Up @@ -125,6 +130,7 @@ export const make = Effect.gen(function* () {
configure: Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
const context = yield* Effect.context<ElectronWindow.ElectronWindow>();
const runPromise = Effect.runPromiseWith(context);

Expand Down Expand Up @@ -184,8 +190,21 @@ export const make = Effect.gen(function* () {
};
const args = yield* HostProcess.Arguments;
args.some((value) => startProviderAuthHandoff(value));
// As the default browser, macOS hands T3 Code every web link through the same event.
const openWebLink = (url: string) => {
if (!DesktopWebLinks.isWebLink(url)) return false;
void runPromise(webLinks.receive(url));
return true;
};
yield* electronApp.on("open-url", (event: { preventDefault: () => void }, url: string) => {
if (startProviderAuthHandoff(url) || resumeProviderAuth(url)) event.preventDefault();
if (startProviderAuthHandoff(url) || resumeProviderAuth(url) || openWebLink(url))
event.preventDefault();
});
// A browser opens HTML files too, which macOS hands over by path.
yield* electronApp.on("open-file", (event: { preventDefault: () => void }, path: string) => {
if (!DesktopWebLinks.isWebPageFile(path)) return;
event.preventDefault();
void runPromise(webLinks.receive(NodeURL.pathToFileURL(path).href));
});
yield* electronApp.on("second-instance", (_event: unknown, argv: readonly string[]) => {
if (argv?.some((value) => startProviderAuthHandoff(value) || resumeProviderAuth(value)))
Expand Down
93 changes: 93 additions & 0 deletions apps/desktop/src/app/DesktopWebLinks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// @effect-diagnostics nodeBuiltinImport:off - Stands in for an Electron webContents.
import { assert, describe, it } from "@effect/vitest";
import * as NodeEvents from "node:events";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";
import * as DesktopWebLinks from "./DesktopWebLinks.ts";

const makeWindow = () => {
const sent: Array<string> = [];
const revealed: Array<unknown> = [];
const webContents = Object.assign(new NodeEvents.EventEmitter(), {
isDestroyed: () => false,
send: (channel: string, url: string) => {
if (channel === WEB_LINK_OPEN_CHANNEL) sent.push(url);
},
});
const window = { webContents } as unknown as Electron.BrowserWindow;
/** The web app starts loading a new page, as a reload does. */
const reload = () =>
webContents.emit("did-start-navigation", { isMainFrame: true, isSameDocument: false });
const layer = DesktopWebLinks.layer.pipe(
Layer.provide(
Layer.succeed(
ElectronWindow.ElectronWindow,
ElectronWindow.ElectronWindow.of({
currentMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target: Electron.BrowserWindow) => Effect.sync(() => void revealed.push(target)),
} as unknown as ElectronWindow.ElectronWindow["Service"]),
),
),
);
return { sent, revealed, layer, reload };
};

describe("DesktopWebLinks", () => {
it("treats only http and https as web links", () => {
assert.isTrue(DesktopWebLinks.isWebLink("https://example.com/"));
assert.isTrue(DesktopWebLinks.isWebLink("http://localhost:3000/a?b=1"));
assert.isFalse(DesktopWebLinks.isWebLink("t3code://app/welcome"));
assert.isFalse(DesktopWebLinks.isWebLink("mailto:hello@example.com"));
assert.isFalse(DesktopWebLinks.isWebLink("not a url"));
});

it("opens HTML files the way a browser does, and nothing else by path", () => {
assert.isTrue(DesktopWebLinks.isWebPageFile("/Users/me/report.html"));
assert.isTrue(DesktopWebLinks.isWebPageFile("/tmp/page.HTM"));
assert.isTrue(DesktopWebLinks.isWebPageFile("/tmp/page.xhtml"));
assert.isFalse(DesktopWebLinks.isWebPageFile("/tmp/notes.md"));
assert.isFalse(DesktopWebLinks.isWebPageFile("/tmp/html"));
});

it.effect("holds links until the renderer listens, then delivers them in order", () => {
const { sent, revealed, layer } = makeWindow();
return Effect.gen(function* () {
const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
// The link that launched the app arrives before the web app has loaded.
yield* webLinks.receive("https://example.com/first");
yield* webLinks.receive("https://example.com/second");
assert.deepStrictEqual(sent, []);

yield* webLinks.setRendererReady(true);
assert.deepStrictEqual(sent, ["https://example.com/first", "https://example.com/second"]);
assert.strictEqual(revealed.length, 1);

yield* webLinks.receive("https://example.com/third");
assert.deepStrictEqual(sent.at(-1), "https://example.com/third");

// A reloading web app stops listening; links wait for it again.
yield* webLinks.setRendererReady(false);
yield* webLinks.receive("https://example.com/fourth");
assert.strictEqual(sent.length, 3);
}).pipe(Effect.provide(layer));
});

it.effect("holds links that arrive while the web app reloads until it listens again", () => {
const { sent, layer, reload } = makeWindow();
return Effect.gen(function* () {
const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
yield* webLinks.setRendererReady(true);
// The old page never says it stopped; the reload itself ends its listening.
reload();
yield* webLinks.receive("https://example.com/during-reload");
assert.deepStrictEqual(sent, []);

yield* webLinks.setRendererReady(true);
assert.deepStrictEqual(sent, ["https://example.com/during-reload"]);
}).pipe(Effect.provide(layer));
});
});
97 changes: 97 additions & 0 deletions apps/desktop/src/app/DesktopWebLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";

/** An http(s) link the operating system handed to T3 Code as the default browser. */
export const isWebLink = (value: string) => {
if (!URL.canParse(value)) return false;
const { protocol } = new URL(value);
return protocol === "http:" || protocol === "https:";
};

/** An HTML file the operating system asked T3 Code to open, as browsers are asked to. */
export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path);

/**
* Web links and HTML files macOS opens with T3 Code once it is the default
* browser. Files arrive as file:// URLs. They can
* arrive before the app is ready (the link that launched it) or while the web
* app reloads, so they wait until the renderer says it is listening.
*/
export class DesktopWebLinks extends Context.Service<
DesktopWebLinks,
{
/** Queues a link and delivers it as soon as the renderer is listening. */
readonly receive: (url: string) => Effect.Effect<void>;
/** The renderer started or stopped listening; listening flushes queued links. */
readonly setRendererReady: (ready: boolean) => Effect.Effect<void>;
}
>()("@t3tools/desktop/app/DesktopWebLinks") {}

const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const pending: Array<string> = [];
/** The page that said it is listening; a reload or crash replaces it, and it must say so again. */
let listening: Electron.WebContents | null = null;
let detachListening: (() => void) | null = null;

const stopListening = () => {
detachListening?.();
detachListening = null;
listening = null;
};

const flush = Effect.gen(function* () {
if (listening === null || pending.length === 0) return;
const window = yield* electronWindow.currentMainOrFirst;
if (Option.isNone(window) || window.value.webContents !== listening) return;
if (listening.isDestroyed()) {
stopListening();
return;
}
for (const url of pending.splice(0)) listening.send(WEB_LINK_OPEN_CHANNEL, url);
yield* electronWindow.reveal(window.value);
});

const startListening = Effect.gen(function* () {
const window = yield* electronWindow.currentMainOrFirst;
if (Option.isNone(window) || window.value.webContents.isDestroyed()) return;
const webContents = window.value.webContents;
if (listening === webContents) return;
stopListening();
listening = webContents;
// Links that arrive while a new page loads wait for it to listen again.
const onNavigation = (
event: Electron.Event<Electron.WebContentsDidStartNavigationEventParams>,
) => {
if (event.isMainFrame && !event.isSameDocument) stopListening();
};
webContents.on("did-start-navigation", onNavigation);
webContents.on("render-process-gone", stopListening);
webContents.once("destroyed", stopListening);
detachListening = () => {
webContents.removeListener("did-start-navigation", onNavigation);
webContents.removeListener("render-process-gone", stopListening);
webContents.removeListener("destroyed", stopListening);
};
});

return DesktopWebLinks.of({
receive: (url) =>
Effect.suspend(() => {
pending.push(url);
return flush;
}).pipe(Effect.withSpan("DesktopWebLinks.receive")),
setRendererReady: (ready) =>
(ready ? startListening : Effect.sync(stopListening)).pipe(
Effect.andThen(flush),
Effect.withSpan("DesktopWebLinks.setRendererReady"),
),
});
});

export const layer = Layer.effect(DesktopWebLinks, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
} from "./methods/snapShot.ts";
import * as PreviewIpc from "./methods/preview.ts";
import * as AppActivationIpc from "./methods/appActivation.ts";
import * as WebLinksIpc from "./methods/webLinks.ts";
import {
completeLegacyLocalStorage,
takeLegacyLocalStorage,
Expand All @@ -88,6 +89,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"

yield* ipc.handle(AppActivationIpc.setReady);
yield* ipc.handle(AppActivationIpc.complete);
yield* ipc.handle(WebLinksIpc.setReady);

yield* ipc.handleSync(takeLegacyLocalStorage);
yield* ipc.handle(completeLegacyLocalStorage);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const PROBE_REMOTE_EDITORS_CHANNEL = "desktop:probe-remote-editors";
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const PASTE_AS_TEXT_CHANNEL = "desktop:paste-as-text";
export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
export const WEB_LINK_OPEN_CHANNEL = "desktop:web-link-open";
export const WEB_LINK_READY_CHANNEL = "desktop:web-link-ready";
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
export const TRACKPAD_SCROLL_END_CHANNEL = "desktop:trackpad-scroll-end";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/ipc/methods/webLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";

import * as DesktopWebLinks from "../../app/DesktopWebLinks.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";

export const setReady = DesktopIpc.makeIpcMethod({
channel: IpcChannels.WEB_LINK_READY_CHANNEL,
payload: Schema.Boolean,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.webLinks.setReady")(function* (ready) {
const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
yield* webLinks.setRendererReady(ready);
}),
});
Loading