Skip to content

fix(desktop): T3 Connect sign-in works in the fork - #172

Merged
ImZoomBoy merged 4 commits into
mainfrom
feat/t3-connect-build
Sep 28, 2026
Merged

ImZoomBoy merged 4 commits into
mainfrom
feat/t3-connect-build

Conversation

@ImZoomBoy

@ImZoomBoy ImZoomBoy commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

The fork's desktop app could not sign in to T3 Connect, so it could never reach app.t3.codes/settings/connections. Settings > Connections showed the T3 Connect rows, but there was no sign-in control and the switches stayed off.

Cause

The fork served its window from t3code-fork://app. That was part of the fork's own app identity (620cfc1). T3's production Clerk instance only accepts requests from t3code://app. Its allowed origins live on T3's Clerk account, which the fork cannot change.

Evidence:

  • The main build, run unpacked on an empty temp home: GET https://clerk.t3.codes/v1/client returns 400 with {"code":"origin_invalid","long_message":"The Request HTTP Origin header must be equal to or a subdomain of the requesting URL."}. The console logs Clerk: Production Keys are only allowed for domain "t3.codes". window.Clerk.status is error, and the welcome screen shows "T3 Connect - Loading sign-in..." forever.
  • The same request with only the Origin changed, 3 tries each: t3code://app 200, https://app.t3.codes 200, t3code-fork://app 400, an unknown origin 400.
  • The same build with only the scheme set to t3code: /v1/client 200, Clerk.status ready.

It was not the build values. #137/#138 already copy .env.example to .env in the build. The installed 0.0.43-ap.9 carries relay.t3.codes, the pk_live_ key and t3-relay in its web client, and the ap.9 CI log says Copied .env.example to .env. No newer upstream ref is needed either: upstream still serves its window from t3code://app.

What changed

  • ElectronProtocol.ts: getDesktopScheme is upstream's t3code / t3code-dev again, so the window's origin is t3code://app. A new getDesktopLinkScheme keeps t3code-fork / t3code-fork-dev. All four schemes go in the one privileged-scheme call, because Electron allows that call once and the old origin needs web storage for the copy below.
  • DesktopClerk.ts, DesktopLinuxUrlHandler.ts, DesktopPreReadyPlatform.ts: pass the link scheme. The Clerk bridge registers its scheme as the OS link handler on every launch, so the official install keeps t3code://. The installer's protocol list was already t3code-fork.
  • ForkRendererStorageMigration.ts (new, fork-only): browser storage belongs to an origin. On first launch, before the window loads, it copies localStorage from t3code-fork://app to t3code://app. That covers composer drafts, the prompt stash, custom themes, and sidebar and second mate folds. It never overwrites a key the new origin holds, flushes storage to disk, then writes a marker in userData so it runs once. It loads both origins in a WebContentsView, not a window, because closing the last window quits the app on Windows. A failed or timed-out run (10 second limit) records failed <n> in the marker. Startup tries again on the next launch and gives up after three failed launches, so a lasting fault cannot hold every start.
  • DesktopApp.ts: calls it before the renderer scheme gets its real handler.
  • fork-features.json: new fork-connect-renderer-origin entry. The fork-app-identity keep line now says the link scheme is ours and the renderer scheme is upstream's.
  • docs/operations/fork-windows-build.md: explains the origin, the storage copy and the known limit.

Saved environments and client settings are files on desktop, so the origin change never touched them. IndexedDB holds caches and T3 Connect auth keys, which never worked at the old origin, so it is not copied.

Clerk bridge scheme

The bridge gets the link scheme, not the window scheme. That does not affect email and password sign-in. In the installed @clerk/electron 0.0.44 (dist/esm/index.js), renderer.scheme is used in four places only:

  • buildRedirectUrl (line 39) builds the OAuth return link from it.
  • setupOAuthTransportIpcHandlers (line 88): app.setAsDefaultProtocolClient(options.renderer.scheme);
  • createClerkBridge (line 294): scheme: options.renderer.scheme, inside protocol.registerSchemesAsPrivileged.
  • assertValidRendererOriginConfig (lines 240-241) checks its format.

The only check on who is calling is isMainFrameEvent (lines 5-7): return sender.getType() === "window" && Boolean(senderFrame) && senderFrame === sender.mainFrame;. It never compares an origin or a scheme. The token cache and passkey handlers use that same check. On the page side, react/index.js takes the allowed redirect protocol from window.location.protocol, so it is t3code:. Email and password sign-in goes from the page straight to Clerk with the window's own origin, and the built app shows /v1/client returning 200 from t3code://app.

Known limit

Clerk sends a Google, GitHub, Apple or Microsoft sign-in back to t3code://app/, which belongs to the official T3 Code install. So those sign-ins do not come back to the fork. Sign in with email and password, which stays in the window. This was chosen to keep the official install's links its own.

Screenshots

Empty temp home, made-up machine name.

Before (main): the rows show, but there is no "Sign in to T3 Connect" control, and Clerk is in error.

Before: Connections on main

After: the same page, with "Sign in to T3 Connect" at the bottom of the settings sidebar.

After: Connections

After: clicking it opens Clerk's sign-in form.

After: sign-in form

Tests that fail first

The first two were run with the six changed source files put back to origin/main and the new tests kept:

$ (apps/desktop) vp test run src/app/ForkRendererStorageMigration.test.ts src/electron/ElectronProtocol.test.ts
     × serves the renderer from upstream's origin and keeps the fork's link scheme 6ms
 FAIL  src/app/ForkRendererStorageMigration.test.ts
Error: Cannot find module '/src/app/ForkRendererStorageMigration.ts' imported from .../ForkRendererStorageMigration.test.ts
 FAIL  src/electron/ElectronProtocol.test.ts > ElectronProtocol > serves the renderer from upstream's origin and keeps the fork's link scheme
AssertionError: expected 't3code-fork://app/' to equal 't3code://app/'
 Test Files  2 failed (2)
      Tests  1 failed | 7 passed (8)

ForkRendererStorageMigration.test.ts gives each origin its own store and runs the module's real write script. It checks four things. Drafts, stash, themes and second mate folds reach the new origin. A value the new origin holds wins. The flush runs before the marker. A second launch does nothing. Two more tests cover a failing copy. One checks that three failed launches record failed 1, failed 2 and failed 3, and that the fourth skips the copy. The other checks that a success after a failure still never overwrites.

Against the module as it was before the retry cap (91f4b2c67b), the retry test fails because a failed launch wrote no marker:

$ (apps/desktop) vp test run src/app/ForkRendererStorageMigration.test.ts -t "stops retrying"
Error [PlatformError]: NotFound: FileSystem.readFile (...\fork-renderer-storage-migrated)

The whole file on that module gives Tests 3 failed | 1 passed (4).

End to end

The app was built from this branch with node scripts/build-desktop-artifact.ts --platform win --target nsis --keep-stage. It ran unpacked from the stage, not installed. APPDATA and T3CODE_HOME pointed at an empty folder under C:/Users/Glyn/AppData/Local/Temp/fm-t3-connect, with a free port and client-settings.json marking onboarding done, so the first-run project scan never ran. A DevTools protocol client drove the app.

  1. Fresh home: clerk 200 /v1/environment, clerk 200 /v1/client, {"origin":"t3code://app","clerkLoaded":true,"clerkStatus":"ready"}. Settings > Connections lists ["T3 Connect","Publish agent activity"] and the "Sign in to T3 Connect" control. Clicking it opens "Sign in to T3 Code" with the email address field, Continue and "Use passkey instead".
  2. Storage copy: the main build wrote four made-up values at t3code-fork://app and quit. The branch build then logged result: { _tag: 'Copied', copied: 5, skipped: 0 }, which is the four plus one key the app writes itself. All four read back at t3code://app. A second launch logged result: { _tag: 'AlreadyMigrated' }, and the values were still there.
  3. Link handler: after each branch run, HKCU\Software\Classes\t3code still pointed at the official install. Only t3code-fork had been re-pointed at the unpacked build. Both keys were exported before every run and restored after.

The run stopped at the email address field. The next step needs the user's T3 account.

The built bundle carries the public values. server.asar holds the web client: relay.t3.codes 4 matches, the pk_live_ key 5, t3-relay 3, the OAuth client id 4. app.asar holds the key once and "t3code-fork" 4 times.

To finish connecting, after this ships

  1. In the fork, open Settings > Connections.
  2. Click "Sign in to T3 Connect" at the bottom of the settings sidebar.
  3. Enter your email address, click Continue, then enter your password. Do not use the Google, GitHub, Apple or Microsoft buttons.
  4. Turn on the "T3 Connect" switch.
  5. Open https://app.t3.codes/settings/connections, signed in to the same account, and look for this machine in the list. That last step was not reached here.

Checks

$ vp fmt --check <10 changed files>
All matched files use the correct format.
Finished in 577ms on 10 files using 24 threads.

$ vp lint --report-unused-disable-directives <10 changed files>
lint exit 0

$ (apps/desktop) vp run typecheck
exit 0, 0 errors

$ (apps/desktop) vp test run <the 8 desktop test files this touches>
 Test Files  8 passed (8)
      Tests  77 passed (77)

$ node scripts/check-fork-features.ts   (after merging origin/main a81e7f894e)
Fork features: 35 entries, every file and test present.
 ❯ scripts/build-desktop-artifact.test.ts (71 tests | 11 failed)
 ❯ apps/server/src/process/sessionProcessJob.test.ts (10 tests | 6 failed)
 Test Files  2 failed | 30 passed (32)
      Tests  17 failed | 563 passed (580)

Those 17 fail the same on origin/main (a81e7f894e) on this Windows machine. With the changed files put back to main:

$ vp test run scripts/build-desktop-artifact.test.ts apps/server/src/process/sessionProcessJob.test.ts
 ❯ scripts/build-desktop-artifact.test.ts (71 tests | 11 failed)
 ❯ apps/server/src/process/sessionProcessJob.test.ts (10 tests | 6 failed)
 Test Files  2 failed (2)
      Tests  17 failed | 64 passed (81)

The full desktop suite ((apps/desktop) vp test run) gave Test Files 4 failed | 105 passed | 4 skipped, Tests 23 failed | 1352 passed | 35 skipped. All 23 are Linux-only tests: Niri, KDE and Hyprland screenshot helpers, and a Unix file-permission launcher test. On main, the same four files fail 22. The 23rd is the KDE test "retains the frozen screenshot for feedback...". It failed once under full-suite load with EPERM: operation not permitted, rmdir on a temp folder, and passed alone 3 of 3 times. It is a Windows temp-folder flake in code this change does not touch.

The fork served its window from t3code-fork://app. T3's production Clerk
instance only accepts t3code://app, so GET /v1/client failed with
origin_invalid, Clerk went to status error, and T3 Connect sat at
"Loading sign-in..." with no way to sign in.

The window is served from upstream's t3code://app again. The Clerk bridge,
the installer and the Linux desktop entry keep t3code-fork:// as the link
scheme the OS routes to the app, so an official T3 Code install keeps its
t3code:// links. Google and GitHub sign-in return to t3code://app/, which
belongs to the official install, so they do not come back to the fork.
Email and password sign-in stays in the window.

Browser storage belongs to an origin. On first launch,
ForkRendererStorageMigration copies localStorage (drafts, prompt stash,
themes, sidebar and second mate folds) from t3code-fork://app to
t3code://app once, never over a key the new origin holds, then flushes it
to disk and writes a marker in userData.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.5 KiB 13.5 KiB −3 B (−0.0%) 15.1 KiB ✅
Codex Thread snapshot wire 7.1 KiB 7.1 KiB +5 B (+0.1%) 7.3 KiB ✅
Codex Live turn WebSocket wire 6.4 KiB 6.4 KiB −8 B (−0.1%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 56.1 KiB 56.1 KiB 0 B (0.0%) 66.4 KiB ✅
Codex Live turn messages 7 7 0 (0.0%) 21 ✅
Claude Total thread wire 13.5 KiB 13.5 KiB −11 B (−0.1%) 15.1 KiB ✅
Claude Thread snapshot wire 7.1 KiB 7.1 KiB +2 B (+0.0%) 7.3 KiB ✅
Claude Live turn WebSocket wire 6.4 KiB 6.4 KiB −13 B (−0.2%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 56.9 KiB 56.9 KiB 0 B (0.0%) 66.4 KiB ✅
Claude Live turn messages 7 7 0 (0.0%) 21 ✅

Baseline: a81e7f8 · PR result: 379d41b · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 114.0 KiB
  • Claude decoded thread snapshot: 114.7 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@ImZoomBoy

Copy link
Copy Markdown
Owner Author

Code review

Two read-only passes: one against the filed task, one against AGENTS.md, CONTRIBUTING.md and a code smell baseline. Nothing is a hard violation. Every finding is listed with its confidence and severity.

Against the task (5 findings)

  1. Email and password sign-in is not proven end to end. The run stopped at the email field, so the password step and any code step never ran. The decision allowed stopping there. Confidence high, severity medium.
  2. The app.t3.codes connection is not shown. The PR names the blocker (the captain's account) and gives the steps, which meets the "or say exactly what blocks it" branch. Confidence high, severity low.
  3. Linux desktop entry and link handler also moved to the fork scheme. The task names Windows only. Without it Linux would take over t3code://, so this looks in scope. Confidence medium, severity low.
  4. The Clerk bridge now gets the link scheme, not the window scheme (DesktopClerk.ts:84). If @clerk/electron also uses that value to check which page is signing in, email sign-in could fail with a wrong-origin error after the email step. The library source was not installed in the review copy, so this is unconfirmed. Confidence low, severity medium.
  5. A failed migration repeats on every launch. If the copy fails or times out, no done-marker is written, so each later start runs it again before the window opens, up to 20 seconds each time, with no retry limit. Confidence medium, severity low to medium.

Checked and fine: the copy covers every key the task names, all in localStorage. Skipping IndexedDB matches the PR's reasoning.

Against the repo standards and smell baseline (14 judgement calls)

  1. ElectronProtocol.test.ts:31-40 only checks the scheme constants against themselves, against the AGENTS.md rule on tests that mirror the implementation. Confidence medium, severity low.
  2. ForkRendererStorageMigration.test.ts asserts call counts on unhandleMock, closeMock and flushMock, the same rule. Confidence low, severity low.
  3. planLegacyStorageCopy spells out an inferable return type, and executeJavaScript(...) as unknown is an extra cast (AGENTS.md "Inferred types over annotations"). Confidence medium, severity low.
  4. The 3 to 4 line "Fork:" comments at each call site in DesktopClerk.ts, DesktopApp.ts and registerDesktopSchemePrivilegesSync go against AGENTS.md comment guidance. Confidence low, severity low.
  5. The migration blocks startup before the window opens, for up to 20 seconds (MIGRATION_TIMEOUT), on first launch or after a failure (AGENTS.md "Performance without compromise"; overlaps finding 5 above). Confidence medium, severity medium.
  6. fork-windows-build.md lists what the migration copies, close to the rule against enumerating details the code records. Confidence low, severity low.
  7. Mysterious Name: attempt builds an error but the name does not say so; kept means "skipped because the new origin already has it". Confidence high and medium, severity low.
  8. Mysterious Name: getDesktopScheme sits next to getDesktopLinkScheme without saying it is the window scheme. getDesktopRendererScheme would be clearer, though keeping upstream's name eases merges. Confidence medium, severity low.
  9. Duplicated Code: getDesktopScheme and getDesktopLinkScheme share the same isDevelopment ? DEV : PROD shape. Confidence medium, severity low.
  10. Primitive Obsession: schemes travel as plain string, and step: Schema.String is free text. Confidence medium, severity low.
  11. Data Clumps: legacyScheme, currentScheme and host travel together, and { scheme, host } recurs in the Clerk renderer object. Confidence low, severity low.
  12. Shotgun Surgery: one scheme split touched 6 source files plus a long keep entry in fork-features.json. Mostly unavoidable in a fork. Confidence low, severity low.
  13. The migration test hardcodes "t3code-fork" and "t3code" instead of reading them from ElectronProtocol. Confidence medium, severity low.
  14. Speculative Generality: the timeout failure passes cause: null, and ForkRendererStorageMigrationResult is a hand-written union used for one log line. Confidence low, severity low.

Checked and fine: one concern per PR holds, since the origin switch and the migration depend on each other. No em dashes added.

Worth resolving before merge

  • Finding 4: confirm how @clerk/electron uses renderer.scheme, since a wrong answer breaks the one sign-in path the fork keeps.
  • Finding 5: write a done-marker or a retry cap after a failed copy, so a lasting fault cannot slow every start.

A failed or timed-out copy wrote no marker, so every later launch ran it
again before the window opened. The marker now records "failed <n>" after
each failed launch, and startup skips the copy once three launches have
failed. A later success still writes "copied". The copy never overwrites a
key the new origin holds, including on a retry.

The timeout drops from 20 to 10 seconds. Loading two blank pages takes far
less, so this only bounds how long a stuck copy can hold the window.

Review follow-ups in the same lines: the result field "kept" is now
"skipped", the error builder is "failedAt", planLegacyStorageCopy infers its
return type, and the test reads the schemes from ElectronProtocol.
@ImZoomBoy

ImZoomBoy commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner Author

Fix round, commits 8b985cf and 422ee55 (the second makes two new helpers private for the unused-exports check).

Against the task:

  1. Email and password not proven end to end. Left. The next step needs the user's account.
  2. app.t3.codes connection not shown. Left, same reason. The body gives the steps.
  3. Linux entry moved to the fork scheme. Left on purpose. Without it Linux would take over t3code://.
  4. Clerk bridge scheme. No change: the claim does not hold. In @clerk/electron 0.0.44, renderer.scheme is used only for the OAuth return link, the OS link registration, the privileged-scheme call and a format check. The only caller check is isMainFrameEvent, which never compares an origin. The body quotes the lines.
  5. Failed copy repeats every launch. Fixed. The marker records failed <n>, and the copy stops after three failed launches. A retry still never overwrites. New test "stops retrying a failing copy after three launches" fails on 91f4b2c with NotFound on the marker file.

Against the repo standards:

  1. Scheme test checks constants. Left. It is the regression test for the origin Clerk accepts.
  2. Call-count asserts in the migration test. Left. They check the blank handlers and the view are released, and that the flush runs.
  3. Spelled-out return type. Taken for planLegacyStorageCopy. Left the as unknown cast, because executeJavaScript returns any.
  4. "Fork:" comments. Left. The fork marks its seams this way for upstream merges.
  5. Startup block. Partly taken: the timeout is 10 seconds, not 20, and finding 5 caps retries.
  6. Doc lists what is copied. Left. It tells a maintainer what survives the origin change.
  7. Names. Taken: attempt is failedAt, kept is skipped.
  8. getDesktopScheme name. Left, to keep upstream's name for merges.
    9-12. Duplicated shape, string schemes, data clumps, shotgun surgery. Left, not trivial to change and outside what this fix needs.
  9. Hardcoded schemes in the test. Taken. The test reads them from ElectronProtocol.
  10. cause: null and the result union. Left. The union now carries a third case, GaveUp.

autoprintworks added 2 commits September 28, 2026 18:48
The unused-exports check flags MAX_MIGRATION_ATTEMPTS and
parseMigrationMarker, since nothing outside the module imports them.
@ImZoomBoy
ImZoomBoy merged commit d482468 into main Sep 28, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant