Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
aa8c666
fix(web): pr merge actions stay visible while the stack refreshes (#1…
maria-rcks Oct 9, 2026
91a6646
chore(deps): upgrade Effect to 4.0.2 (#17571)
juliusmarminge Oct 9, 2026
ecfb734
fix(devices): recover stalled video without losing simulator input (#…
juliusmarminge Oct 9, 2026
7856908
fix(web): keep checkout stable while pr actions load (#16625)
maria-rcks Oct 9, 2026
1fd558d
fix(mobile): show waiting thread status (#16693)
maria-rcks Oct 9, 2026
5722496
feat(web): add parent thread breadcrumb navigation (#16666)
maria-rcks Oct 9, 2026
0a4d789
fix(server): restart inactivity after snoozed threads wake (#16674)
maria-rcks Oct 9, 2026
454b94a
feat(desktop): passkeys in the in-app browser on macOS (#16952)
juliusmarminge Oct 9, 2026
a1db449
fix(client): load earlier turns works for MCP threads over T3 Connect…
juliusmarminge Oct 9, 2026
0e7abea
refactor(client): sign relay request URLs built from the HttpApi cont…
juliusmarminge Oct 9, 2026
28f11ed
refactor(source-control): add @t3tools/source-control-core (#17573)
juliusmarminge Oct 9, 2026
f6e4502
refactor(source-control): Forgejo lives in @t3tools/source-control-fo…
juliusmarminge Oct 9, 2026
d01febb
refactor(source-control): Azure DevOps lives in @t3tools/source-contr…
juliusmarminge Oct 9, 2026
eb459b5
refactor(source-control): GitLab lives in @t3tools/source-control-git…
juliusmarminge Oct 9, 2026
8d1858d
refactor(source-control): Bitbucket lives in @t3tools/source-control-…
juliusmarminge Oct 9, 2026
507361b
refactor(source-control): GitHub lives in @t3tools/source-control-git…
juliusmarminge Oct 9, 2026
e5dca23
refactor(usage): transcript readers come from their drivers (#17576)
juliusmarminge Oct 9, 2026
23957c7
refactor(usage): OpenCode usage comes from provider-opencode (#17577)
juliusmarminge Oct 9, 2026
5a9c664
refactor(usage): Cursor account usage comes from provider-cursor (#17…
juliusmarminge Oct 9, 2026
9a0766b
refactor(usage): Antigravity usage is a reader on its driver (#17579)
juliusmarminge Oct 9, 2026
b1ec4b3
refactor(usage): usage readers use Effect FileSystem and SqlClient (#…
juliusmarminge Oct 9, 2026
6586a01
fix(web): composer context strip pads both edges evenly (#17562)
limineol Oct 9, 2026
a0a0e94
test(usage): v4 cache upgrade test waits for the migrated cache write…
Mnigos Oct 9, 2026
c85dc64
feat(mobile): support Duo in the shared iOS app (#12648)
juliusmarminge Oct 9, 2026
49f849e
refactor(source-control): GitManager reads provider resolvers, not ho…
juliusmarminge Oct 9, 2026
2ce3e4a
refactor(source-control): PullRequestService reads GitHub resolvers, …
juliusmarminge Oct 9, 2026
51347ac
refactor(source-control): Forgejo identity and Azure DevOps addressin…
juliusmarminge Oct 9, 2026
58916b6
refactor: home directory comes from a HostProcessHomeDirectory refere…
juliusmarminge Oct 9, 2026
e308522
refactor(shared): host process references live in a HostProcess modul…
juliusmarminge Oct 9, 2026
6cd02e9
feat(web): filter PR comments by bots and resolved threads (#17645)
juliusmarminge Oct 9, 2026
0c40120
fix(clients): remove redundant prefix from PR watch status (#17635)
extoci Oct 9, 2026
99d2651
fix(web): pending requests wait until you stop typing (#17637)
maria-rcks Oct 9, 2026
08e18b7
fix(models): remove new badges from Claude Opus and Sonnet 5.5 (#17646)
extoci Oct 9, 2026
5dcb59e
fix(ui): keep focus and selection borders visible across the app (#16…
maria-rcks Oct 9, 2026
5fe9d02
fix(mobile): prevent row presses during native back swipes (#17648)
juliusmarminge Oct 9, 2026
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
c1f21e5
fix(web): section header chevrons point up when collapsed (#14273)
ZenderGoD Oct 10, 2026
e08c31e
fix(web): timeline divider pill shows a pointer, visible hover and fo…
jonesfionn101-dotcom Oct 10, 2026
6e4ca6c
fix(git): allow creating prs from dirty worktrees (#15625)
maria-rcks Oct 10, 2026
b822420
docs(install): polish binary install destination phrasing (#15732)
ege-arhan Oct 10, 2026
0981e6b
perf(server): keep passive terminal output flowing (#17178)
StiensWout Oct 10, 2026
b6aa268
fix(web): stop button icon no longer shifts on hover (#16012)
NK-Works Oct 10, 2026
9bbbbba
fix(web): add bottom padding to expanded tool panels (#16525)
12ya Oct 10, 2026
2def3de
fix(web): keep incremental highlighter return type portable (#17259)
luke2x Oct 10, 2026
391f813
fix(web): sidebar "Code" label no longer clips its letter tops (#16134)
akbarakma Oct 10, 2026
699da95
fix(tests): use POSIX paths for the simulated macOS device host (#17241)
Quicksaver Oct 10, 2026
88edfac
fix(mobile): Android composer keeps the caret in view on AOSP-based k…
bitmvk Oct 10, 2026
467fea9
test(web): allow cold timeline imports on CI (#16608)
lastobelus Oct 10, 2026
aa165e9
fix(mobile): pinch zooms chat images on Android (#15047)
AKolenda Oct 10, 2026
d1e56e7
fix(web): selected provider ring no longer clipped during panel resiz…
jfortez Oct 10, 2026
d50a2a6
docs(usage): OpenCode Go limits need a Go API key (#15664)
nexxeln Oct 10, 2026
c116dac
fix(web): improve usage scanning indicator alignment (#15498)
AksharP5 Oct 10, 2026
97009b0
fix(server): print pairing credential expiry as ISO timestamp (#14128)
kvnloo Oct 10, 2026
2cc55f2
chore(ci): use GPT 6.1 Sol Max for check agents (#14312)
ishaanko Oct 10, 2026
8b81a88
fix(mobile): honor requested terminal native architectures (#10709)
bompus Oct 10, 2026
5c715c6
fix(server): keep preview browser connected after operation timeouts …
juliusmarminge Oct 10, 2026
aacefcd
fix(web): timeline divider focus ring stays inside the pill (#17702)
t3dotgg Oct 10, 2026
03bd839
perf(desktop): reuse the prepared shell environment in the local back…
Yash-Singh1 Oct 10, 2026
f02002a
fix(web): align settings page widths (#12158)
diegoarff Oct 10, 2026
a3d1505
test(server): resolve the temp dir before matching the symlinked entr…
ylcn91 Oct 10, 2026
6e83160
fix(web): keep inline code pills intact when they wrap (#12038)
satyalyadav Oct 10, 2026
2aa033b
Revert "chore(ci): use GPT 6.1 Sol Max for check agents" (#17698)
maria-rcks Oct 10, 2026
f55c66b
fix(web): show the correct new thread shortcut in command palette (#8…
vaishnavsm Oct 10, 2026
c1ade7d
fix(desktop): declare macOS local network usage (#11922)
jsilets Oct 10, 2026
968ac2e
docs: add Scoop as Windows installation method (#10509)
Mostafa-Ben-Git Oct 10, 2026
a0a601c
fix(server): agents run in their own systemd scopes so an OOM kill sp…
t3dotgg Oct 10, 2026
4ea7356
fix(web): welcome wizard says where imported projects come from (#14584)
UzEE Oct 10, 2026
d02e292
fix(web): cite works on responses that end before a tool call (#17713)
maria-rcks Oct 10, 2026
fa9f936
fix(desktop): sign Windows native addons (#8206)
Lumbreras2306 Oct 10, 2026
c572cb7
fix(server): track resumed subagent follow-ups as separate tasks (#17…
Yash-Singh1 Oct 10, 2026
c2a301b
fix(web): nested corners follow their container's radius (#17695)
maria-rcks Oct 10, 2026
1f093d1
feat(web): pr panel actions confirm in place (#17710)
maria-rcks Oct 10, 2026
4107930
feat(web): reorder right panel tabs by dragging (#17730)
eimexdev Oct 10, 2026
c899a0c
fix(azure-devops): list pull requests with token sign-in and check ou…
maria-rcks Oct 10, 2026
0be20c8
fix(usage): keep one email in two workspaces as two accounts (#17711)
maria-rcks Oct 10, 2026
c3243f0
feat(pull-requests): hosts can report edit and resolve permissions pe…
juliusmarminge Oct 10, 2026
9cc5768
perf(server): run Git for Windows' real git.exe, not its launcher (#1…
SunkenInTime Oct 10, 2026
a7223f3
fix(clients): restart continuations show as a T3 Code notice, not ano…
juliusmarminge Oct 10, 2026
57b3780
fix(mobile): browser picture in picture opens from the header button …
juliusmarminge Oct 10, 2026
65d3317
feat(source-control): GitCafe lives in @t3tools/source-control-gitcaf…
juliusmarminge Oct 10, 2026
500cb42
fix(web): add provider wizard no longer shifts sideways while it grow…
flamboh Oct 10, 2026
42f71df
fix(web): PR search keeps the caret where you type (#17675)
flamboh Oct 10, 2026
6d5ea19
fix(server): thread PR badges catch up when another environment reads…
flamboh Oct 10, 2026
bd2346e
fix(server): refuse editor paths with line breaks or quotes when the …
juliusmarminge Oct 10, 2026
c77a7b7
fix(web): align compact button touch targets (#17748)
Yash-Singh1 Oct 10, 2026
6266af3
fix(server): t3_thread_launch refuses a new worktree whose base ref h…
tris203 Oct 10, 2026
d6f3af7
fix(web): omit underlines on markdown image links (#17728)
Saikrishna1876 Oct 10, 2026
4606f9d
fix(server): restore auto resume for wrapped Claude gateway rate limi…
tzachbon Oct 10, 2026
e1ad595
fix(opencode): name OpenCode 2 sessions after their thread (#17414)
nkoynov Oct 10, 2026
472ea7e
fix(web): find update settings from the command palette (#17396)
sergical Oct 10, 2026
7b09ac7
fix(provider-opencode): tell OpenCode Zen and Go models apart (#17424)
mr-karan Oct 10, 2026
adbecda
fix(server): a pull that fast-forwards no longer fails on large Git o…
ScottN-PV Oct 10, 2026
91474e5
fix(web): cancel question auto-advance after navigation (#17364)
maxwellyoung Oct 10, 2026
33c1e36
fix(server): a bare repository name resolves to the signed-in account…
ScottN-PV Oct 10, 2026
71f6f1d
fix(web): a maximized right panel stays maximized when you return to …
jamesvillarrubia Oct 10, 2026
02612c2
fix(mobile): allow starting a task with only an image (#17409)
Claudesaul Oct 10, 2026
a418075
fix(server): PR watch no longer reports passed while a second run of …
ScottN-PV Oct 10, 2026
a5b4824
fix(server): say why a thread can't be settled (#17258)
DylanTX Oct 10, 2026
03b2105
fix(server): prevent busy terminals from starving history persistence…
StiensWout Oct 10, 2026
de49d63
feat(server): use macOS .icns app icons as project icons (#17149)
psv2522 Oct 10, 2026
a1dee8b
fix(mobile): usage reset icon lines up with its row (#17175)
Aforno Oct 10, 2026
ee7924f
fix(web): paths pasted after @ keep their underscores (#16619)
derektrimm Oct 10, 2026
dd4549e
fix(server): settle every OpenCode subagent call one report answers (…
nkoynov Oct 10, 2026
8bf0a38
perf(web): switching project keeps Diagnostics and Providers mounted …
flamboh Oct 10, 2026
f7664fd
perf(web): Open Source Licenses downloads its manifest once per sessi…
flamboh Oct 10, 2026
94d1a8e
fix(server): restore OpenCode adapter test typecheck (#17810)
Yash-Singh1 Oct 10, 2026
98beed1
fix(server): Claude subagents show the reasoning effort they run at (…
RakshithBhat03 Oct 10, 2026
8c777fb
feat(server): stop Claude subagents without stopping their owner (#17…
Yash-Singh1 Oct 10, 2026
afd98d7
fix(server): stopped native subagents no longer read as Running (#17223)
im-kvijay Oct 10, 2026
a11f464
perf(server): t3_thread_list reads only the listed project's threads …
only21mil Oct 10, 2026
cfbd145
fix(server): show diffs for projects outside the server cwd (#17724)
maria-rcks Oct 10, 2026
50647de
fix(server): check the specific scope for scripts, preview input, and…
juliusmarminge Oct 10, 2026
dacd2cb
fix(source-control): stop Forgejo status refresh from scanning every …
loispostula Oct 10, 2026
d6f7c48
Merge pingdotgg/main into the fork (10 Oct)
Tevin2119 Oct 10, 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
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ Install the latest version of the desktop app from [GitHub Releases](https://git
winget install T3Tools.T3Code
```

#### Windows (`scoop`)

```bash
scoop install extras/t3code
```

#### macOS (Homebrew)

```bash
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"electron": "44.4.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.8.9",
"electron-webauthn": "catalog:",
"ffi-rs": "1.3.2",
"playwright-core": "1.60.0",
"react-grab": "^0.1.32"
"react-grab": "^0.1.32",
"tldts": "catalog:"
},
"devDependencies": {
"@effect/vitest": "catalog:",
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as PreviewPasskeys from "../preview/Passkeys.ts";
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
Expand All @@ -34,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
import * as HostProcess from "@t3tools/shared/HostProcess";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
Expand Down Expand Up @@ -276,13 +278,16 @@ const startup = Effect.gen(function* () {
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
const updates = yield* DesktopUpdates.DesktopUpdates;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const previewPasskeys = yield* PreviewPasskeys.PreviewPasskeys;

yield* shellEnvironment.installIntoProcess;
const hasCommandLinePasswordStore =
preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;
const linuxElectronOptions =
environment.platform === "linux" && !hasCommandLinePasswordStore
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess(
yield* HostProcess.HomeDirectory,
)
: preReadyElectronOptions.linux;
if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {
if (
Expand Down Expand Up @@ -329,6 +334,7 @@ const startup = Effect.gen(function* () {
});
}
yield* appIdentity.configure;
yield* previewPasskeys.configure;
yield* applicationMenu.configure;
yield* updates.configure;
yield* DesktopRemoteUpdates.listen;
Expand Down
18 changes: 9 additions & 9 deletions apps/desktop/src/app/DesktopAppActivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type DesktopAppActivationResponse,
} from "@t3tools/contracts";
import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
import { HostProcessPlatform, HostProcessUserId } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import { afterEach, describe, expect } from "vite-plus/test";
Expand Down Expand Up @@ -83,8 +83,8 @@ function exchange(address: string, payload: DesktopAppActivationRequest) {
describe("desktop app control server", () => {
it.effect("roundtrips a request and removes its socket on shutdown", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
const platform = yield* HostProcess.Platform;
const userId = yield* HostProcess.UserId;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-control-test-"));
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
Expand Down Expand Up @@ -123,8 +123,8 @@ describe("desktop app control server", () => {

it.effect("cancels a queued request when the client disconnects", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
const platform = yield* HostProcess.Platform;
const userId = yield* HostProcess.UserId;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-cancel-test-"));
const target = makeTarget(NodePath.join(root, "userdata"), platform, userId);
Expand Down Expand Up @@ -162,8 +162,8 @@ describe("desktop app control server", () => {
// Two desktop apps can share one state dir, such as nightly and a preview build.
it.effect("keeps a newer app's socket when an older app on the same state dir quits", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
const platform = yield* HostProcess.Platform;
const userId = yield* HostProcess.UserId;
if (platform === "win32") return;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-takeover-test-"));
Expand All @@ -183,8 +183,8 @@ describe("desktop app control server", () => {

it.effect("binds its address again after the socket file is removed", () =>
Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const userId = yield* HostProcessUserId;
const platform = yield* HostProcess.Platform;
const userId = yield* HostProcess.UserId;
if (platform === "win32") return;
yield* Effect.promise(async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-app-reclaim-test-"));
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/app/DesktopAppActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type DesktopAppActivationResponse,
} from "@t3tools/contracts";
import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
import { HostProcessUserId } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -315,7 +315,7 @@ export const make = Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const path = yield* Path.Path;
const userId = yield* HostProcessUserId;
const userId = yield* HostProcess.UserId;
const runPromise = Effect.runPromiseWith(yield* Effect.context<never>());
const address = resolveDesktopAppControlAddress({
stateDir: path.resolve(desktopEnvironment.stateDir),
Expand Down
64 changes: 61 additions & 3 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as NodeHttp from "node:http";
import * as NodePath from "@effect/platform-node/NodePath";
import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff";
import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
import { HostProcessArguments } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { assert, describe, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
Expand Down 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 @@ -375,12 +385,60 @@ it.effect.each(["startup", "open-url"] as const)(
assert.strictEqual(delivery?.returnUrl, request.returnUrl);
}).pipe(
Effect.provide(layerDesktopClerk(true, [], "darwin", undefined, shell)),
Effect.provideService(HostProcessArguments, entry === "startup" ? ["t3", link] : ["t3"]),
Effect.provideService(HostProcess.Arguments, entry === "startup" ? ["t3", link] : ["t3"]),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(
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),
);
27 changes: 23 additions & 4 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 @@ -11,13 +12,14 @@ import { codexAuthDeliveryUrl, readCodexAuthHandoff } from "@t3tools/shared/code
import { receiveCodexAuthCallback, CodexAuthCallbackError } from "./CodexAuthCallback.ts";
import * as ElectronShell from "../electron/ElectronShell.ts";
import { providerAuthReturnUrl } from "@t3tools/shared/providerAuthReturnUrl";
import { HostProcessArguments } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";
import * as ElectronApp from "../electron/ElectronApp.ts";
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 @@ -182,10 +188,23 @@ export const make = Effect.gen(function* () {
);
return true;
};
const args = yield* HostProcessArguments;
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
4 changes: 2 additions & 2 deletions apps/desktop/src/app/DesktopPreReadyFileSystem.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
Expand Down Expand Up @@ -32,7 +32,7 @@ it.layer(NodeServices.layer)("DesktopPreReadyFileSystem", (it) => {
}),
);

it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32" || process.getuid?.() === 0)(
it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32" || process.getuid?.() === 0)(
"fails instead of treating an unreadable profile as missing",
() =>
Effect.gen(function* () {
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, describe, it } from "@effect/vitest";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -75,7 +75,7 @@ describe("DesktopPreReadyPlatform", () => {
}).pipe(
Effect.provide(
DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
Layer.provide(Layer.succeed(HostProcess.Platform, "linux")),
),
),
);
Expand Down Expand Up @@ -111,7 +111,7 @@ describe("DesktopPreReadyPlatform", () => {
}));
yield* Layer.build(
DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
Layer.provide(Layer.succeed(HostProcess.Platform, "linux")),
),
);
const identity = yield* Effect.promise(() => portalIdentity);
Expand All @@ -135,7 +135,7 @@ describe("DesktopPreReadyPlatform", () => {
});

return DesktopPreReadyPlatform.make.pipe(
Effect.provideService(HostProcessPlatform, "linux"),
Effect.provideService(HostProcess.Platform, "linux"),
Effect.asVoid,
);
});
Expand All @@ -151,7 +151,7 @@ describe("DesktopPreReadyPlatform", () => {
assert.include(contents, "MimeType=x-scheme-handler/t3code;");
assert.include(contents, "Icon=");
assert.equal(setDesktopNameMock.mock.calls.length, 1);
}).pipe(Effect.provideService(HostProcessPlatform, "linux"));
}).pipe(Effect.provideService(HostProcess.Platform, "linux"));
});

it.effect(
Expand All @@ -168,7 +168,7 @@ describe("DesktopPreReadyPlatform", () => {
});

const layerPreReady = DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "darwin")),
Layer.provide(Layer.succeed(HostProcess.Platform, "darwin")),
);

const layerClerkShaped = Layer.effect(
Expand Down
Loading
Loading