fix(desktop): show a splash when the local backend never becomes ready#23637

open6
main
fix/issue-23372
opened2d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 2d ago

Problem

When the local backend starts but never becomes ready (startup stuck before command readiness), the desktop app shows no window at all, indefinitely. The readiness probe times out every 60 s and only logs; there is no window or splash, and activate/Dock clicks do nothing because the main window waits for readiness and the connecting splash was only used for WSL-only setups.

Reproduced on main (8d3b061121) with the real Linux desktop app: an outside SQLite reader holds a read transaction on statev2.sqlite while the WAL grows to ~48 MB, which stalls the backend's startup writes. The desktop stayed windowless for the whole 4-minute run (httpReadiness.timedOut every 60 s); releasing the reader brought the main window up within ~4 s. Without the stall, the window appears in 5–19 s.

Change

  • BackendInstanceSpec gets an optional onReadinessFailure hook, called after the existing readiness-timeout handling, alongside onReady.

  • The primary backend wires it to DesktopWindow.showConnectingSplash. That function already does nothing when a splash or main window exists or the app is quitting. The splash is still dismissed when the main window first reveals after readiness, and activate already reveals it while the backend is not ready.

  • The splash text is "Starting T3 Code…" unless the setup is WSL-only, which keeps "Connecting to WSL…".

  • The splash now reveals through bindFirstRevealTrigger with the same triggers as the main window (ready-to-show, plus did-finish-load on Linux), shared via firstRevealSubscribers. Otherwise it would never show on Wayland, where ready-to-show waits for show().

  • If the user closes the splash while the backend is still starting, activate (a Dock click, a second launch, or the update menu) reopens it. It only does so after a splash has been shown during that startup, so a normal fast boot never shows a splash.

  • DesktopClerk.resumeProviderAuth now loads a provider sign-in return URL only into the main window (electronWindow.main) instead of currentMainOrFirst. During a stalled boot, that was the splash.

A normal fast boot is unchanged: the splash only appears after the first 60 s readiness timeout.

Scope and approval

Fixes #23372 GitCafe: https://git.cafe/pingdotgg/t3code/issues/23372 Triaged bug via the automated issue flow. The database stall that triggers it is tracked separately in https://git.cafe/pingdotgg/t3code/issues/23482 and is not changed here.

Verification

Focused tests (vp test run on DesktopBackendPool.test.ts, DesktopBackendManager.test.ts and DesktopWindow.test.ts): 3 files, 30 tests, all passing at the first fix; with DesktopClerk.test.ts added, 4 files, 48 tests, all passing at 0bc4b60815. vpr typecheck in apps/desktop and vp lint on the touched files are clean.

  • DesktopBackendPool > shows the connecting splash when the primary backend never becomes ready runs a real primary backend instance with a process that never exits and a 503 readiness probe, advances the test clock past two timeouts, and asserts the splash was shown. It failed on main: a primary readiness timeout left the desktop with no window: the connecting splash was never shown: expected +0 to be above +0.
  • A DesktopWindow test shows the splash on Linux when only did-finish-load fires.
  • DesktopWindow tests: activate reopens a splash that was closed while the backend is still starting, and opens no window on activate during a normal boot. The second test failed before the activate change was narrowed.
  • DesktopClerk test: a provider-auth return link never navigates the splash. It failed on the previous currentMainOrFirst lookup.

Evidence

Real desktop app built from each SHA. Environment for every capture: Xvfb 1920x1080x24, Electron 44.9.3, --no-sandbox --disable-gpu, DPR 1, no font scaling, GTK_THEME=Adwaita (Adwaita:dark for the dark row), and the same seeded statev2.sqlite copied into a fresh home per run. The stall is an outside reader transaction plus a ~48 MB WAL, released at t=190 s. Diff0 is ImageMagick compare -fuzz 0 (changed pixels in red); DiffT is -fuzz 10%. Capture scripts: i23372-capture-run.sh, i23372-diff-pair.sh, i23372-rest.sh. Log timelines: i23372-timelines.txt.

Recordings (times in seconds from launch, from the desktop log)

Stalled boot held to 190 s, then released:

  • Before 8d3b061121 (i23372-rec-before-long.mp4): readiness timeouts at 61.5, 121.5 and 181.5 with no window at any point; release at 190.0, backend ready at 192.3, main window at 192.5.
  • After 00eb3b9949 (i23372-rec-after-long.mp4): timeout at 61.8, splash shown at 61.9 and kept through the timeouts at 121.8 and 181.8 (one splash, no flicker); release at 190.0, backend ready at 192.1, main window at 192.3, splash closed.

Second instance launched while the splash is up, after only (before has no splash) (i23372-rec-after-f5.mp4): splash at 61.4, second instance at 70.0 exits through the single-instance lock, splash stays the only window, no main window. Before would look the same as the stalled boot: no window.

SIGTERM while stalled: before (i23372-rec-before-f6.mp4) and after (i23372-rec-after-f6.mp4) both exit cleanly at ~70.8 with the backend child gone and nothing reappearing.

Normal fast boot: before (i23372-rec-before-f7.mp4) and after (i23372-rec-after-f7.mp4) both reach the main window at 5.6–5.7 with no splash.

Stills

RowBefore 8d3b061121After 00eb3b9949Diff0DiffT
Stalled boot, t=65 s, lightBefore t=65: no windowAfter t=65: Starting T3 Code splash, lightDiff0 t=65 AE 79200 (the 360×220 splash)DiffT t=65 AE 79200
Splash crop, lightn/a, no splashLight splash crop: bg #ffffff, label #6b7280n/an/a
Stalled boot, t=70 s, darkBefore t=70 dark: no windowAfter t=70 dark: dark splashDiff0 dark AE 79200DiffT dark AE 906
Splash crop, darkn/a, no splashDark splash crop: bg #0a0a0a, label #9ca3afn/an/a
After the 3rd timeout, t=185 sBefore t=185: no windowAfter t=185: one splashDiff0 t=185 AE 79200DiffT t=185 AE 79200
10 s after release, t=200 sBefore t=200: main windowAfter t=200: main window, no splash leftDiff0 t=200 AE 0DiffT t=200 AE 0
Second instance, t=80 sn/aAfter t=80: splash only, no main windown/an/a
Just before SIGTERM, t=68 sBefore t=68: no windowAfter t=68: splashDiff0 t=68 AE 79200DiffT t=68 AE 79200
10 s after SIGTERM, t=80 sBefore t=80: emptyAfter t=80: emptyDiff0 t=80 quit AE 0DiffT t=80 quit AE 0
Fast boot, t=5 sBefore fast boot t=5After fast boot t=5: no splashDiff0 fast t=5 AE 0DiffT fast t=5 AE 0
Fast boot, t=19 sBefore fast boot t=19: main windowAfter fast boot t=19: main windowDiff0 fast t=19 AE 0DiffT fast t=19 AE 0
WSL-only settings (no WSL on Linux, local fallback), t=2 sBefore WSL-only t=2: Connecting to WSL splashAfter WSL-only t=2: Connecting to WSL splashDiff0 WSL t=2 AE 443 (spinner phase only)DiffT WSL t=2 AE 191
WSL-only settings, t=19 sBefore WSL-only t=19: main windowAfter WSL-only t=19: main windowDiff0 WSL t=19 AE 0DiffT WSL t=19 AE 0

What else changed: nothing beyond the splash during a stalled boot. Release timing, the main window, fast boot, quit and the WSL-only splash are identical; the WSL row differs only by spinner rotation. An independent rebuild of the t=65 after-capture at 00eb3b9949 differed from the delivered one only inside the spinner box (AE 443 at fuzz 0).

Not verified here:

  • macOS: Dock/activate revealing or reopening the splash, and a provider sign-in return while the backend is starting. Both are unit-tested only; neither is captured.
  • Windows: not run.
  • Wayland: the splash reveal path is covered by the unit test only, because Xvfb is X11.
  • A real WSL connection: not available on Linux.
  • Raising the splash for a second instance and closing it from a window manager: Xvfb has no window manager.

Model and harness: Claude Opus 5.5 (implementation, tests, evidence, review) and Claude Sonnet 5.5 (review) via Claude Code in T3 Code.

🤖 Generated with Claude Code


Independent review and proof audit (maintainer takeover, head 0bc4b60815)

Reviews (read-only, full diff against main plus the overlapping PRs):

  • Claude Sonnet 5.5: VERDICT: NEEDS_JULIUS. No code blockers; the call is about competing PR #23392, which fixes the same issue https://git.cafe/pingdotgg/t3code/issues/23372 with a splash shown at boot rather than after the 60 s readiness timeout. Only one should land.
  • Claude Opus 5.5: VERDICT: MERGE. Two should-fix notes: onReadinessFailure fires on every 60 s timeout, so on macOS a splash the user closed comes back every minute; and showConnectingSplash checks and sets splashWindowRef in separate steps, which can't race today because nothing in between yields.
  • Both agree: the splash cannot stay up after the main window reveals or block quit; a fast boot never shows a splash; the DesktopClerk change to electronWindow.main is not a regression (with only the splash, the return URL was already lost); the Wayland/X11 reveal fires exactly once; nothing touches auth scopes, permissions, modes or MCP.
  • Correction to the description: a second launch does not reopen a closed splash. The second-instance handler (apps/desktop/src/app/DesktopClerk.ts:225-238) only reveals an existing window and never calls desktopWindow.activate. On Linux and Windows the question is moot: closing the splash is closing the last window, so window-all-closed quits the app (captured below). Reopening is reachable through the macOS Dock/activate, the application menu and the app-control socket only.
  • Overlapping PRs: #22999 (GitHub https://git.cafe/pingdotgg/t3code/issues/22996) and #23007 (GitHub https://git.cafe/pingdotgg/t3code/issues/23005). The intents are compatible: https://git.cafe/pingdotgg/t3code/pulls/23007 changes onReady right next to the new onReadinessFailure line in DesktopBackendPool.ts (keep both), and all three rewrite the layerPool test harness with different option shapes. The conflicts are textual and in the test harness. The reviewers differ on order (this PR first vs https://git.cafe/pingdotgg/t3code/pulls/22999 first); either way it is test-harness merging only.

Proof audit: VERDICT: SUFFICIENT. The author's recordings predate the last two commits (made at 00eb3b9949), so rows 1–4 were recaptured at the head. Rows 5–6 stand on the author's stills: git diff 00eb3b9949 0bc4b60815 -- apps/desktop doesn't touch the splash text or colours. Real Linux desktop app, Xvfb 1920x1080x24, Electron 44.9.3, --no-sandbox --disable-gpu, synthetic statev2.sqlite created by each SHA's own first boot, stall = ~51 MB WAL plus an outside read transaction. Before 8d3b061121, after 0bc4b60815. Times are seconds from launch, from the desktop log; the window list is read over CDP.

RowBeforeAfter
1. Stalled boot, plain second launch at 70, release at 80no window until main at 81.5 (rec, t74)splash 61.4; second launch exits 71.0, splash still the only window; ready 81.6, main 81.7, splash gone at 90 (rec, t74, t90)
2. Fast boot, second launch at ~2 smain 4.4, no splash (rec)main 5.1, no splash (rec); auditor rerun: main 3.8, no splash
3. SIGTERM while stalled at 68.2main and backend child gone 68.7 (rec)splash 61.3; main gone 68.7, backend child 68.8, nothing left at 78 (rec)
4. Close the splash while stalled (Linux)n/a, no splashsplash 61.5; window.close() at 65.0 → before-quit 65.2, app exits; relaunch at 70 is a new primary that shows its own splash after its first timeout, ~60 s later (rec, t67)
5. WSL-only textauthor's f8 stills, unchanged since 00eb3b9949same
6. Dark themeauthor's f3dark stills, unchanged since 00eb3b9949same

Driver: i23637-driver.sh, sha256 75f32b082eeb9e9da7ed78fb21a7ee638deb546c14e0adb17577972a41c18dbe (row 4 used v1, sha256 a7cc8e29916e99b785bd261e7cdd44c8396ff369012999fc8d08a8b5bb6c1403). Helpers: pin.py, cdp-close.mjs. Timelines: i23637-timelines.txt.

Verification log
  • 2026-10-22, fs-17, head 0bc4b60815705fdbd3771d55f69a2416b0e97d99, before 8d3b061121.
  • Review round: Sonnet 5.5 NEEDS_JULIUS (competing https://git.cafe/pingdotgg/t3code/pulls/23392), Opus 5.5 MERGE.
  • Audit pass 1: rows 1–4 lacked head-SHA proof (author's captures at 00eb3b9949). Captured at the head, then audit pass 2: SUFFICIENT, with an R2 rerun and the driver hash checked.
  • Not exercised live: reopening a closed splash through activate (unit-tested only; on Linux the close quits the app, and the t3 app control-socket activate never reached the app at either SHA, so it proves nothing); a provider sign-in return while stalled.
  • Unverified: macOS (Dock/activate reopen, the repeat every 60 s after closing), Windows, Wayland (unit test only), a real WSL connection.
  • Model and harness: Claude Opus 5.5 (lead, capture, audits, review) and Claude Sonnet 5.5 (review) via Claude Code in T3 Code.

Checks

All checks passed

Added 4 commits

  • fix(desktop): reveal the connecting splash on Wayland too00eb3b9
  • Merge remote-tracking branch 'origin/main' into fix/issue-23372940d828
  • fix(desktop): show the connecting splash when the local backend is slow to startb76378b
  • test(desktop): reproduce #23372, no window after primary readiness timeoutde87828

Notes from my run at #23372 (#23392, now superseded by this PR). They may help with the parts marked "Not verified".

Reproduced on main (3b95e7831b), Linux and macOS. I used packaged builds (AppImage on Xvfb, and the arm64 .app on a Mac), each with its own profile and a seeded T3 home. To stall the backend I froze the bin.mjs child with SIGSTOP right after spawn. On both platforms no window appeared for the whole wait, including after the 60 s readiness timeout. Launching again (Linux) and open (macOS) did nothing. On macOS, list_windows for the app's pid stayed empty, and the trace showed desktop.lifecycle.activate → desktop.window.activate with no window, the same spans as in the issue.

macOS Dock/activate, which this PR lists as not captured. I tested it on my branch, where the splash shows at launch instead of after the timeout. The splash showed, and Cmd+W closed it (list_windows returned []). After that, open brought nothing back until I changed activate. Screenshot of the reopened splash: https://github.com/user-attachments/assets/f6939ea8-83f9-4013-b545-2177104636e7

Two things I think also apply here (I only read this branch's code; I didn't run it):

  1. A closed splash isn't reopened on activate. In DesktopWindow.ts activate, if the user closes the splash while the backend isn't ready, splashWindowRef is None and it falls through to createMainIfBackendReady, which does nothing. Here the next readiness timeout calls onReadinessFailure and brings the splash back, so the window could stay gone for up to 60 s rather than for good. My fix was to call showConnectingSplash from activate when the splash is gone and the backend is still starting: 79185c4b5b.
  2. A provider sign-in return link can load into the splash. DesktopClerk.ts resumeProviderAuth uses electronWindow.currentMainOrFirst. During a stalled boot the only window is the splash, so the return URL goes to the 360×220 splash. On my branch I switched it to electronWindow.main and return when that's None. A test in DesktopClerk.test.ts puts a splash first and checks it's never navigated. Same commit.

Recordings from my branch (Linux, 4x speed, splash from launch rather than after the timeout; main window blurred):

Not tested: Windows, Wayland, a real WSL connection, and the database stall itself. I couldn't reproduce the 658 s stall; #23482 tracks it.

Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This focused desktop bug fix adds a splash after prolonged local-backend readiness failures and adjusts startup-window handling across platforms. It also changes provider-authentication return navigation, so the authentication-sensitive path warrants human review.

You can add or adjust custom eligibility rules. Learn more.

Added 1 commit

  • fix(desktop): reopen the splash on activate and keep sign-in returns out of it07ca044
Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 940d828881b6c74c8ae847b5213b101ec73bbe55 and 0bc4b60815705fdbd3771d55f69a2416b0e97d99.

📒 Files selected for processing: 4
  • apps/desktop/src/app/DesktopClerk.test.ts
  • apps/desktop/src/app/DesktopClerk.ts
  • apps/desktop/src/window/DesktopWindow.test.ts
  • apps/desktop/src/window/DesktopWindow.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

📝 Walkthrough

The desktop shows a connecting splash after primary-backend readiness failures. Splash text reflects WSL settings, Linux can reveal it after did-finish-load, and activation can recreate it while the backend is still awaited. Provider-auth callbacks now target the main window.

Changes: Desktop startup behavior

Layer / File(s)Summary
Splash content and window lifecycle
apps/desktop/src/window/DesktopWindow.ts, apps/desktop/src/window/DesktopWindow.test.ts
The splash label depends on WSL settings. The splash reveals on ready-to-show on all platforms and on did-finish-load on Linux. Activation recreates a startup splash if the backend is still awaited. Tests cover Linux reveal, splash recreation, and normal boot.
Readiness failure displays the splash
apps/desktop/src/backend/DesktopBackendManager.ts, apps/desktop/src/backend/DesktopBackendPool.ts, apps/desktop/src/backend/DesktopBackendPool.test.ts
The backend manager invokes an optional callback after saving a readiness-failure snapshot. The primary backend configures the callback to show the connecting splash. A test simulates readiness failures and checks that the splash is displayed.
Provider-auth callback targets main window
apps/desktop/src/app/DesktopClerk.ts, apps/desktop/src/app/DesktopClerk.test.ts
Provider-auth resume uses electronWindow.main instead of currentMainOrFirst. A test verifies that the connecting splash is not navigated.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

mermaid
1sequenceDiagram
2 participant DesktopBackendManager
3 participant DesktopBackendPool
4 participant DesktopWindow
5 DesktopBackendManager->>DesktopBackendManager: Persist readiness-failure snapshot
6 DesktopBackendManager->>DesktopBackendPool: Invoke readiness-failure callback
7 DesktopBackendPool->>DesktopWindow: Show connecting splash

Possibly related issues

  • pingdotgg/t3code#18613: The change directs provider-auth callbacks to the main window, but does not address early launch-event capture.

Suggested reviewers: maria-rcks

Merge Risk: ⚪ Minimal · up to 0bc4b

The splash and provider-auth changes have no established issue that should delay merging after normal checks.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main change: showing a splash when the local backend does not become ready.
Linked Issues checkPassedIssue #23372 requires visible desktop feedback when the backend does not reach readiness. The PR adds onReadinessFailure, wires the primary backend to DesktopWindow.showConnectingSplash, and keeps…
Out of Scope Changes checkPassedThe changes remain within issue #23372. The readiness hook, splash text and reveal behavior, activation reopening, provider-auth target correction, and focused tests support visible and usable startup…
Description checkPassedThe pull request description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction case, implementation, scope boundaries, focused tests, man…
Full details: Linked Issues check

Explanation

Issue #23372 requires visible desktop feedback when the backend does not reach readiness. The PR adds onReadinessFailure, wires the primary backend to DesktopWindow.showConnectingSplash, and keeps the splash until the main window appears. Tests cover a stalled primary backend, Linux first reveal, splash reopening on activation, and protection against provider-auth navigation to the splash. The reported fast-boot, quit, and WSL-only behavior remains unchanged. The database stall in #23482 is separate and is not required by #23372.

Full details: Out of Scope Changes check

Explanation

The changes remain within issue #23372. The readiness hook, splash text and reveal behavior, activation reopening, provider-auth target correction, and focused tests support visible and usable startup feedback. No unrelated backend stall fix or unrelated product behavior is included.

Full details: Description check

Explanation

The pull request description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the reproduction case, implementation, scope boundaries, focused tests, manual evidence, limitations, and agent details. The scope section links the tracked issue and identifies the automated triage, although it does not quote a specific maintainer approval comment.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Added 1 commit

  • fix(desktop): only reopen the splash on activate after a slow start0bc4b60

Thanks, both points checked out against this branch and are fixed in 07ca044999 and 0bc4b60815:

  1. A closed splash is reopened on activate while the backend is still starting. It only reopens after a splash was already shown during that startup, because activate also fires on a second launch and the update menu, and on macOS at launch, so a normal boot still shows no splash.
  2. resumeProviderAuth uses electronWindow.main, so a sign-in return link never loads into the splash. A DesktopClerk test covers it.

Your macOS repro notes are useful context for the parts listed as not verified here.