Skip to content

feat(browser): show every page load, link clicks included, as a line along the address bar on desktop and mobile - #31597

Merged
AmoabaKelvin merged 6 commits into
mainfrom
browser-loading-indicator
Oct 21, 2026
Merged

AmoabaKelvin merged 6 commits into
mainfrom
browser-loading-indicator

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​125 $\color{#cf222e}{\Huge{\mathbf{−}}}$​41 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​84
Prod 29 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​342 $\color{#cf222e}{\Huge{\mathbf{−}}}$​100 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​242

ELI5

When a page in Orca's browser is loading, a thin blue line now grows along the bottom of the address bar and fades out when the page is done. It also works for links you click inside a page, which before showed no sign of loading at all. The phone shows the same line when it's looking at a desktop browser tab.

What Changed

Before, the only cue was the reload button turning into a small spinner. Worse, BrowserPage.loading only turned on for loads Orca started itself (typing a URL, reload). A link click, form post, JS redirect or in-page back/forward left it off for the whole server wait. On main, a link to a page that takes 4s shows nothing for 4s.

Now:

  • The loading rule. loading starts on a main-frame, cross-document did-start-navigation. That replaces the gated did-start-loading, so the trackNextLoadingEventRef gate and its plumbing are gone. Iframe loads and SPA pushState/hash changes don't count, which is the same rule Chromium uses for its own loading UI. Client-hosted pages use the same rule through one shared check (isPageDocumentNavigationStart).
  • The line. BrowserLoadProgress sits inside the address bar, on its bottom border, clipped to the rounded corners. It is 2px of --callout-note (GitHub's accent blue, #0969da light / #4493f8 dark). It waits 200ms before showing, eases toward 90% over 30s, then fills and fades when the load ends. A failed load fades where it stopped. It runs on Web Animations over transform and opacity only, and nothing runs once a load ends or the tab closes.
  • Reload/Stop. While loading, the button shows an X when clicking it stops the load. Buttons that can only reload (doc preview, remote-streamed pages) keep the reload icon. The tab-strip spinner stays, since it is the only cue for background tabs.
  • Mobile. When the phone views a desktop browser tab, the same line runs along the bottom of its address field (MobileBrowserLoadProgress), with the same delay, curve, finish and fade-in-place on failure. It reads loading and loadError, which the desktop already sent in the session tab and the phone ignored, so there's no wire change. It uses React Native's Animated with the native driver (transform and opacity), mobile's existing accentBlue token, and the existing reduced-motion hook.
  • Tests. happy-dom rejected a cancelled animation's finished promise as unhandled, unlike the spec and Chromium. That's patched in the DOM test setup next to the other happy-dom patches.

Why

The line matters less than the rule behind it. A bar driven by the old loading would have stayed blank for most real browsing.

Design

Chosen: one boolean that the code already had (loading), fixed at its owner, plus a time-based line. Electron gives no load percentage, so the line eases on its own and the real stop finishes it. That's how Chrome Android, t3code and NProgress do it too.

Ruled out:

  • Bar on the old loading. Link clicks, redirects and history moves would show nothing.
  • Letting every did-start-loading count. Iframe polls would run the bar every few seconds.
  • Real progress from CDP network events. Heavy per guest, and Chrome desktop doesn't show a percentage either.
  • Stages from load events (commit, DOM ready). A jump when the server answers would be nice. It needs a new per-page field from the webview to the toolbar, so I left it for later.
  • Line in the toolbar row instead of the address bar. In narrow toolbars the address bar floats as an overlay and the line would stay behind. It would also show on doc previews.
  • Full-width line on the toolbar's bottom edge. Reads as page chrome, not as the address.
  • blue-500 (the tab spinner's blue). The design-system gate rejects raw palette colours. --callout-note already serves as the app's accent blue (chat unread, Jira) and is tuned per theme.

Timings: 200ms show delay (STYLEGUIDE's deferred-feedback rule; Turbo uses 500ms, NProgress none), then 15% at 0.3s, 45% at 2s, 70% at 6s, 85% at 15s, 90% at 30s, then hold. The finish takes 450ms. With reduced motion it is a still full-width line at half opacity.

Open questions for review:

  • Stop fills the line to 100% before fading, as Chrome Android does. Fading in place would need Stop told apart from success, and ERR_ABORTED is swallowed upstream on purpose. I think filling is fine since you pressed Stop yourself.
  • Should the tab-strip spinner switch to the same blue token? It still uses blue-500, which is close but not the same.

Not covered yet: browser tabs owned by a headless orca serve (the phone gets loading: false for those) and remote-streamed pages, whose loading only covers Orca's own RPCs. Both need the host to report page loads, which is a wire change. That's a separate follow-up.

Linked Issue

None. I found no issue or PR for this.

Visual Proof

The line mid-load at 0.6s, 2.5s and 7s, then reduced motion. Light, then dark.

line-light.png

line-dark.png

Phone viewing a desktop tab (Android emulator, over-the-air page built from each branch). Sequence: typed slow URL, link click inside the page, fast page, then a server that drops the connection. Before on top, after below.

mobile-pair.mp4

The same component in a native (non-page) build, driven by a scripted harness: slow load, fast load, failed load.

native-harness.mp4

Desktop. Each video has before (main) on top and after (this branch) below. Recorded from a hidden dev build against a local server that holds responses.

Link click inside the page, server holds 4s. Before: nothing at all for 4s. After: the line runs for the whole wait. This is the main behaviour fix.

Before on top, after below.

Light

link-click-light.mp4

Dark

link-click-dark.mp4
Typed URL, server holds 4s. Before: small spinner on the reload button. After: the line, plus an X for Stop.

Before on top, after below.

Light

slow-load-light.mp4

Dark

slow-load-dark.mp4
Slow headers, streamed body, slow image. The line keeps going until the image finishes, then fills and fades.

Before on top, after below.

Light

staged-load-light.mp4

Dark

staged-load-dark.mp4
Fast pages back to back. No line at all. Loads under 200ms show nothing.

Before on top, after below.

Light

fast-load-light.mp4

Dark

fast-load-dark.mp4
New navigation while one is in flight. The line carries on. No jump back to zero.

Before on top, after below.

Light

renavigate-light.mp4

Dark

renavigate-dark.mp4
Stop mid-load. The line fills and fades, like Chrome Android does on stop.

Before on top, after below.

Light

stop-light.mp4

Dark

stop-dark.mp4
Unreachable host, then a server that drops the connection. The instant failure shows nothing. The slow one fades out where it stopped instead of filling up.

Before on top, after below.

Light

error-light.mp4

Dark

error-dark.mp4
Reload a 1.5s page. Same line as any other load.

Before on top, after below.

Light

reload-light.mp4

Dark

reload-dark.mp4
SPA route change (fetch, then pushState). No line, same as Chrome and Safari.

Before on top, after below.

Light

spa-light.mp4

Dark

spa-dark.mp4
Iframe that reloads every 3s. No line and no tab spinner. Iframe loads are not page loads.

Before on top, after below.

Light

iframe-light.mp4

Dark

iframe-dark.mp4
Tab A loading, switch to idle tab B and back. B shows no line while A keeps its tab spinner. Back on A the line is further along, not reset.

Before on top, after below.

Light

tab-switch-light.mp4

Dark

tab-switch-dark.mp4

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • New e2e in tests/e2e/browser-reload-feedback.spec.ts: a link click inside the page whose response the server holds. It sets loading and shows the line, and fails on main (loading stays false). The reload case now checks the line instead of animate-spin.

  • Unit: iframe loads and same-document navigations don't report loading (browser-page-favicon-navigation.test.ts). Fixtures updated for the removed gate.

  • Live in a hidden dev build: the store's loading for link clicks, history, 204s, downloads (link and address bar), SPA, hash and iframe polls; reduced motion switched mid-load; no animations left after a load ends or a tab closes mid-load.

  • pnpm lint, pnpm tc, check:code-quality:changed, check:react-doctor:changed, check:zustand-selector-fanout, oxfmt --check, type-aware oxlint, browser-pane and tab-bar unit tests (1765), and the reload-feedback e2e spec all pass on a branch rebased onto main.

  • Mobile: paired an Android emulator to a hidden desktop dev build and drove loads from the desktop (video above), on both the over-the-air page and a native render. Mobile lint, typecheck, test-typecheck ratchet and browser tests pass. No new mobile unit test: the line's only behaviour is its animation, which the mocked test renderer can't show. Five pane tests stub the new component, because their react-native mocks have no Animated.

  • Desktop tested on macOS. The desktop change is renderer-only and uses no platform-specific APIs. iOS not run; the mobile component uses only cross-platform Animated.

AI Disclosure

Review

Codex review, 3 rounds. Round 1 found two things: the doc preview's reload button would have shown an X that reloads, and reduced motion didn't apply mid-load. Both fixed. Rounds 2 and 3 were clean. A fourth round after adding mobile found two real issues in the mobile line, both fixed: a late native stopAnimation answer could fade out a newer load, and a reduced-motion change mid-load was ignored. It also asked for headless-server loading, which is the separate follow-up above. A separate design review measured that a long-running animation keeps ticking the main thread in hidden or closed tabs, so the line now holds with a finished animation and is cancelled on stop and unmount.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: renderer-only Web Animations on desktop, same on macOS, Linux and Windows. Mobile uses Animated with the native driver on iOS and Android, and JS-driven on the web page (only while loading).
  • Mobile and mixed versions: an old phone ignores the fields it always ignored; a new phone with an old desktop sees the old loading rule (Orca-started loads only).
  • SSH and folder workspaces use the same local webview, so they get the same behaviour.
  • Client-hosted pages still send the same loading boolean to the runtime and mobile. It just stops turning on for iframe loads, so old and new versions work together. No IPC, wire or saved-state change.
  • Performance: nothing runs when idle. While loading, one compositor animation per loading page. The old reload-button spinner (an infinite CSS rotation) is gone.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@AmoabaKelvin
AmoabaKelvin marked this pull request as ready for review October 21, 2026 18:16
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ffe4a334-8e11-46ac-b84c-f7092bb9b83c
📥 Commits

Reviewing files that changed from the base of the PR and between 5ce312e and ef7873d.

📒 Files selected for processing: 10
  • mobile/src/browser/MobileBrowserAddressField.test.tsx
  • mobile/src/browser/MobileBrowserAddressField.tsx
  • mobile/src/browser/MobileBrowserLoadProgress.tsx
  • mobile/src/browser/MobileBrowserPane.tsx
  • mobile/src/browser/MobileBrowserPaneView.tsx
  • mobile/src/browser/mobile-browser-app-resume.test.tsx
  • mobile/src/browser/mobile-browser-dialog-through-the-bridge.test.tsx
  • mobile/src/browser/mobile-browser-frameless-stream.test.tsx
  • mobile/src/browser/mobile-browser-pane-frame-layers.test.tsx
  • mobile/src/browser/mobile-browser-pane-in-page-unmount.test.tsx

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

📝 Walkthrough

Browser loading state now starts on qualifying document navigation events instead of loading events. The browser address bar displays delayed progress lines with success and failure states on desktop and mobile. Reload controls use stop-state props instead of loading spinners. The change removes the loading-event tracking ref from navigation and lifecycle hooks, adds a Happy DOM animation-cancellation setup script, and updates unit and end-to-end tests.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to ef787

The loading indicator is wired to mobile tab state, but the E2E test may pass before the delayed line becomes visible. Strengthen that assertion to protect the intended behavior.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Description check Warning The description is detailed and covers the user impact, implementation, rationale, visual proof, testing, compatibility, and performance. However, the required Linked Issue section states “None” inste… Add the actual linked issue using the required “Fixes #” format. If the author is a maintainer and no issue is required, state that explicitly according to the repository policy and obtain reviewer confirmation.
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 30 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: showing page-load progress along the address bar on desktop and mobile, including link clicks.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed and covers the user impact, implementation, rationale, visual proof, testing, compatibility, and performance. However, the required Linked Issue section states “None” instead of providing an actual issue link.

Resolution

Add the actual linked issue using the required “Fixes #<issue-number>” format. If the author is a maintainer and no issue is required, state that explicitly according to the repository policy and obtain reviewer confirmation.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3eb5c6cf-ec10-43fe-bb8a-773c7e29b1a7
📥 Commits

Reviewing files that changed from the base of the PR and between d4a7494 and 5ce312e.

📒 Files selected for processing: 36
  • config/scripts/happy-dom-animation-cancel.ts
  • config/scripts/vitest-dom-setup.ts
  • config/tsconfig.node.json
  • src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.dead-guest.test.tsx
  • src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-load-progress.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-page-chrome-header.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-page-pane.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-page-toolbar.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-reload-control.tsx
  • src/renderer/src/components/browser-pane/browser-client-page-guest-metadata.ts
  • src/renderer/src/components/browser-pane/host-guest/attach-browser-page-webview.ts
  • src/renderer/src/components/browser-pane/host-guest/bind-browser-page-webview-listeners.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-favicon-navigation.test.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.test.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-webview-loading-handlers.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-webview-navigation-handlers.ts
  • src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-lifecycle.ts
  • src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts
  • src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts
  • src/renderer/src/components/browser-pane/navigate/navigate-browser-page-url.ts
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.ts
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.test.tsx
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.ts
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-webview-url-sync.ts
  • src/renderer/src/components/browser-pane/restored-client-hosted-recovery-window.test.tsx
  • src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.tsx
  • src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-toolbar.tsx
  • src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx
  • src/renderer/src/components/browser-pane/workspace-doc/doc-preview-toolbar.tsx
  • tests/e2e/browser-reload-feedback.spec.ts
💤 Files with no reviewable changes: 15
  • src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.test.ts
  • src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx
  • src/renderer/src/components/browser-pane/host-guest/attach-browser-page-webview.ts
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx
  • src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts
  • src/renderer/src/components/browser-pane/workspace-doc/doc-preview-toolbar.tsx
  • src/renderer/src/components/browser-pane/assemble-chrome/browser-page-pane.tsx
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.ts
  • src/renderer/src/components/browser-pane/navigate/use-browser-page-webview-url-sync.ts
  • src/renderer/src/components/browser-pane/host-guest/use-browser-page-webview-lifecycle.ts
  • src/renderer/src/components/browser-pane/navigate/navigate-browser-page-url.ts
  • src/renderer/src/components/browser-pane/host-guest/browser-page-webview-loading-handlers.ts

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

)
await expect.poll(() => releaseHeld !== undefined, { timeout: 10_000 }).toBe(true)
await expect.poll(getLoading, { timeout: 10_000 }).toBe(true)
await expect(pane.locator('[data-browser-load-progress="loading"]')).toHaveCount(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify that the loading line becomes visible.

data-browser-load-progress="loading" is set before the 200 ms display delay. This assertion can pass while the line remains transparent, and Line 158 can release the response before the line appears. Wait for the line’s computed opacity to become nonzero before release. Apply the same check to the held-reload assertion at Line 95. As per coding guidelines, “assert user-visible behavior through the DOM.”

Source: Coding guidelines

@AmoabaKelvin
AmoabaKelvin marked this pull request as draft October 21, 2026 18:44
Link clicks, form posts, redirects and history moves inside a page never
set loading, because only navigations Orca started armed did-start-loading.
Main-frame did-start-navigation now owns the start, so iframe loads and
same-document changes stay out.
…unts

A long-running animation kept ticking the main thread for hidden or closed tabs. happy-dom now marks a cancelled animation's finished promise handled, as the spec and Chromium do.
@AmoabaKelvin
AmoabaKelvin force-pushed the browser-loading-indicator branch from 5ce312e to ef7873d Compare October 21, 2026 19:17
@AmoabaKelvin AmoabaKelvin changed the title feat(browser): show every page load, link clicks included, as a line along the address bar feat(browser): show every page load, link clicks included, as a line along the address bar on desktop and mobile Oct 21, 2026
@AmoabaKelvin
AmoabaKelvin marked this pull request as ready for review October 21, 2026 23:23
@AmoabaKelvin
AmoabaKelvin merged commit 7e795b6 into main Oct 21, 2026
71 checks passed
@AmoabaKelvin
AmoabaKelvin deleted the browser-loading-indicator branch October 21, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant