Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
201 commits
Select commit Hold shift + click to select a range
2cf0ff0
fix(web): Local environment switch stays reachable after turning it o…
ScottN-PV Oct 9, 2026
c5d4d2d
fix(web): keep chat banners inside the lane beside the docked details…
macodev00 Oct 9, 2026
a947f7c
fix(web): settled and snoozed lines line up with the messages above t…
RakshithBhat03 Oct 9, 2026
22ccf8a
fix(web): distinguish project filter from new project (#12113)
voltcrash Oct 9, 2026
b150867
feat(web): assign a thread details panel shortcut (#16694)
maria-rcks Oct 9, 2026
a669827
fix(web): chat content keeps pace with sidebar resizing (#17383)
flamboh Oct 9, 2026
2c1915b
refactor(provider-core): expose model metadata through a ModelCatalog…
juliusmarminge Oct 9, 2026
b66dd40
refactor(provider-core): follow the Effect service conventions throug…
juliusmarminge Oct 9, 2026
68ab16f
refactor(provider-core): latest-version lookups go through a Provider…
juliusmarminge Oct 9, 2026
784b562
refactor(provider-core): MCP provider sessions live in a McpProviderS…
juliusmarminge Oct 9, 2026
31b04e2
refactor(provider): bring opencode, muse, pi, core and testing in lin…
juliusmarminge Oct 9, 2026
f853d51
refactor(provider-acp): ACP, ACP Registry and Grok follow the Effect …
juliusmarminge Oct 9, 2026
33806e7
refactor(provider-cursor): follow the Effect service conventions (#17…
juliusmarminge Oct 9, 2026
6497246
fix(marketing): use app wordmark in header (#13240)
voltcrash Oct 9, 2026
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
fd3549c
refactor(contracts): source control provider kind is an open branded …
juliusmarminge Oct 10, 2026
7bc7328
feat(source-control): each host package ships a client definition (#1…
juliusmarminge Oct 10, 2026
c8a6033
refactor(client-runtime): add project clone sources come from host de…
juliusmarminge Oct 10, 2026
579159d
refactor(web): host presentation and behavior come from client defini…
juliusmarminge Oct 10, 2026
b0249ac
refactor(source-control): reference parsing and project matching are …
juliusmarminge Oct 10, 2026
9b40d37
feat(pull-requests): quick actions follow each host's capabilities, n…
juliusmarminge Oct 10, 2026
a4c8745
feat(projects): new projects can be published to any ready host (#17860)
juliusmarminge Oct 10, 2026
f82796a
fix(server): send Claude MCP servers over the control channel (#17898)
juliusmarminge Oct 10, 2026
ec43786
fix(web): a finished reply replaced by a steer is no longer labeled p…
juliusmarminge Oct 10, 2026
61eb4af
fix(client-runtime): queued runs that start after a steer show up in …
juliusmarminge Oct 10, 2026
2a4ee33
feat(mobile): choose the microphone order for voice input (#17896)
juliusmarminge Oct 10, 2026
4f63275
feat(source-control): host settings live on each host's definition, w…
juliusmarminge Oct 10, 2026
38c187b
fix(pi): preserve tool images and structured results (#17851)
StiensWout Oct 10, 2026
a8bdfdb
fix(server): Claude 5 task lists reach the tasks drawer (#14964)
Mnigos Oct 10, 2026
f92afe7
fix(web): find bar and thread details panel stop covering each other …
MatthewFeroz Oct 10, 2026
0442f5b
fix(web): use server metadata for file chip icons (#17923)
Yash-Singh1 Oct 10, 2026
c8b89a0
fix(desktop): copy images from HTML previews (#17555)
Bil0000 Oct 10, 2026
e408cac
docs(pi): update installation and remote login guidance (#17836)
StiensWout Oct 10, 2026
7e8071f
fix(pi): preserve native abort outcomes (#17853)
StiensWout Oct 10, 2026
160b18c
fix(pi): keep thinking defaults specific to each model (#17835)
StiensWout Oct 10, 2026
62da1cd
fix(pi): preserve shell command exit codes (#17834)
StiensWout Oct 10, 2026
6e9c5a8
fix(pi): expire and cancel extension approvals (#17840)
StiensWout Oct 10, 2026
9afde1c
feat(pi): include native sessions in usage reports (#17848)
StiensWout Oct 10, 2026
818e2be
fix(server): route Copilot ACP subagent output into subagent threads …
maria-rcks Oct 10, 2026
74dfbb6
fix(web): composer banner titles truncate beside their icon instead o…
maria-rcks Oct 10, 2026
953edee
fix(server): Muse turns no longer fail on Windows (#17163)
ntindle Oct 10, 2026
5f7294d
fix(pi): allow known read-only T3 tools without approval (#17852)
StiensWout Oct 10, 2026
4772822
fix: worktree threads keep their worktree when the agent starts, and …
maria-rcks Oct 11, 2026
e4ef8d3
fix(server): keep Claude workflows alive while they report progress (…
maria-rcks Oct 11, 2026
0ecb25a
fix(web): media preview centers its content and pins the close button…
maria-rcks Oct 11, 2026
c73b454
fix(server): threads without a project no longer need Git installed (…
t3dotgg Oct 11, 2026
1cfd1c1
fix(web): toggling tools and thinking at the bottom keeps you at the …
t3dotgg Oct 11, 2026
c727d12
fix(web): Compact chip follows Claude's real prompt cache TTL (#17945)
t3dotgg Oct 11, 2026
75c439f
fix(usage): bound OpenCode history reads to prevent backend OOM (#17961)
Yash-Singh1 Oct 11, 2026
0f36a11
refactor: format diff line counts through one shared helper (#17948)
maria-rcks Oct 11, 2026
72fbad7
fix: new projects start their first thread in the project folder, not…
t3dotgg Oct 11, 2026
cf8479e
perf(server): drive shell last-error lookup from thread bindings (#17…
only21mil Oct 11, 2026
7f0b789
perf(server): skip parsing plain terminal history output (#17179)
StiensWout Oct 11, 2026
1c7b267
perf(server): batch terminal history writes every 250 ms (#17183)
StiensWout Oct 11, 2026
4258c8f
perf(server): storage cleanup no longer runs a full status or size wa…
tris203 Oct 11, 2026
febe1b2
perf(usage): usage scans skip OpenCode legacy messages older than the…
tris203 Oct 11, 2026
9ec472c
perf(usage): OpenCode legacy walk stats entries before checking for s…
tris203 Oct 11, 2026
29e09df
fix(web): MCP app permission prompts no longer overflow the screen (#…
juliusmarminge Oct 11, 2026
716357c
feat(provider-antigravity): support subscription quota and usage limi…
Maseeek Oct 11, 2026
43888b5
fix(desktop): keep generated annotation styles in sync (#17838)
t3-code[bot] Oct 11, 2026
a70ee2a
fix(web): Resume continues on the model picked in the composer (#17867)
akbarakma Oct 11, 2026
3410e43
fix(devices): bump agent-device to 0.21.24 for native Windows hosts (…
TanJeeSchuan Oct 11, 2026
c448e63
fix(web): keybinding group headings no longer touch the group above (…
fixfon Oct 11, 2026
e7db61f
feat(web): configure OpenTelemetry exports in diagnostics (#12538)
MatthewFeroz Oct 11, 2026
08f9b56
docs: update user count to 500k (#17783)
Kamkmgamer Oct 11, 2026
a2f4c7f
fix(mobile): working pill no longer bulges on its right end (#16143)
id0Sch Oct 11, 2026
b467be4
perf(opencode): stop retaining every message of a loaded OpenCode thr…
tris203 Oct 11, 2026
e61eba4
fix(web): switch thumbs stay in their track while scrolling (#15240)
raphaelpra Oct 11, 2026
77c8747
fix(keybindings): thread jumps no longer overlap model picker jumps (…
Fluffy-Bunny-23 Oct 11, 2026
18fb8e2
Merge upstream pingdotgg/t3code main 77c87471 into atli (ATLI-401)
gabynevada Oct 11, 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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You can think of T3 Code as an open source "bring-your-own-subscription" alterna

## What makes T3 Code special?

We have over 400,000 users who love T3 Code. It's important we maintain the things they love as we continue to iterate on the product. Here's a brief list of the things we can never compromise on.
We have over 500,000 users who love T3 Code. It's important we maintain the things they love as we continue to iterate on the product. Here's a brief list of the things we can never compromise on.

### 1. Open at the core

Expand Down
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
Loading
Loading