Skip to content

fix: recover from web content process crashes like Safari - #253

Closed
barto95100 wants to merge 40 commits into
the-ora:mainfrom
barto95100:fix/web-content-crash-recovery
Closed

barto95100 wants to merge 40 commits into
the-ora:mainfrom
barto95100:fix/web-content-crash-recovery

Conversation

@barto95100

Copy link
Copy Markdown

WebKit reports when the process rendering a page ends (crash, or macOS reclaiming memory), but Ora ignored it: the tab stayed blank and unresponsive, and Reload did nothing.

Recovery now follows Safari:

  • the visible page reloads once, with a notice explaining why;
  • a background page stays unloaded until its tab is shown (no wake-ups for pages nobody looks at);
  • a page that ends again within 30 seconds of a reload shows a crash page with "Try Again" instead of looping.

Reloads keep the back/forward history. Media sessions, Picture in Picture, permission prompts and autofill of the dead page are cleared. The navigation error WebKit sends alongside the termination is ignored, so no misleading "Page Not Found" appears.

Debug builds get Navigation › Copy Page Crash Command: the sandbox forbids Ora to kill a page process, so it copies the kill -9 command to run in Terminal.

Tests: PageCrashRecoveryPolicyTests (7 cases) + manual tests (visible tab, background tabs, repeated crash, mini player / PiP, notice dismissal).

barto95100 and others added 30 commits October 25, 2026 10:55
Moving across child elements of the same anchor re-sent the same URL on every mouseover and re-assigned the @published hoveredLinkURL, re-rendering the link preview. Deduplicate in the injected script and skip identical assignments in Swift. Complements the-ora#246.
urlString, used by open-tab search, was only set when the tab was created, so a tab kept matching the URL it was first opened with. Route navigation updates through Tab.updateURL(_:) which updates both.
The first suggestion could be the active tab itself, so Enter switched to the current tab and nothing happened. Never suggest the active tab, and rank the typed URL before open tabs when the input is a URL.
…s through

Clicking the sidebar or toolbar left the URL bar suggestions open, while the dimming overlay over the page swallowed clicks, so links and the context menu did not work while editing. Watch mouse-downs during editing (without consuming them) and dismiss when they land outside the field and its suggestions; keep the overlay purely visual.
…g it blindly

The WKUIDelegate method was missing the type parameter, so WebKit never called it (compiler: 'nearly matches optional requirement'). Fix the signature, forward whether camera, microphone or both are requested, and answer with WebKit's own prompt instead of an unconditional grant.
oraTests had no Info.plist and no signing team, so tests could not run locally. Generate the Info.plist and sign with the development team.
Introduce SiteOrigin (scheme, host and port, with default ports normalized), a tri-state ask/allow/deny per capability, and SitePermissionStore scoped per space like website data. Private browsing is kept in memory only. Permissions are cleared when a space is deleted. Replaces the unused SitePermissionSettings.
Capture requests are answered from the saved per-space permissions; when nothing is saved, an in-page prompt asks the user, optionally remembering the choice. Unanswered prompts are denied when the page navigates away, the tab is closed or another request replaces them. Allow has no keyboard shortcut on purpose.
…ttings

Add a Site Permissions card to each space in Settings > Spaces to change or forget saved camera and microphone choices. Share the site display rule (host for HTTPS, full origin otherwise) between the prompt and settings, with tests.
The workflow only checked lint and formatting. Build the app and run the unit tests (ad-hoc signing on CI), pin SwiftLint and SwiftFormat to the local versions, cache Swift packages and upload the test results on failure. Upstream Sync now dispatches the CI on its branch, since pull requests opened with GITHUB_TOKEN do not trigger workflows.
ci: build the app and run unit tests
Escape not handled by the page travels up the responder chain as cancelOperation(_:) and reached the window, which exits full screen. Stop it at the page host view, after WebKit, so pages still get the key. Like Safari, it now stops a page that is still loading. Fixes the-ora#179.
fix: keep full screen when Escape is pressed in a web page
The Fullscreen shortcut (Control-Command-F by default) was defined and listed in Settings but never handled, and macOS only offers Globe-F in the View menu, which keyboards without a Globe key cannot type. Handle Ora's customizable shortcut with the other global key handlers, without adding a duplicate menu item.
fix: make the Fullscreen keyboard shortcut work
The app delegate received links before any window was ready, picked an arbitrary window or built one by hand without its environment, and posted a notification the window might not be listening to yet, so links were lost. Let SwiftUI route external events: regular windows accept and prefer them and get them through onOpenURL once ready; settings and private windows refuse them. Remove the now unused manual window factory. Fixes the-ora#178.
openTab required a host, which file URLs do not have, so no tab was created; and the sandbox only lets WebKit read a local file loaded with loadFileURL(_:allowingReadAccessTo:). Use the file name as the initial title and load file URLs with that API.
Pages without a <title> report an empty one, which blanked the tab title. Fall back to the host, or the file name for local files, like Safari.
barto95100 and others added 10 commits October 25, 2026 17:50
Muted autoplay videos (page backgrounds, previews, ads) were marked as played and brought up the mini player, and one code path computed its visibility from all sessions instead of played ones. Only audible media count as played, including a muted video the user unmutes. Ported from the upstream branch tembo/fix-mini-player-trigger-no-media, which no longer applies after upstream's file reorganisation. Fixes the-ora#187.
fix(media): don't show the mini player for silent or muted media
…p with media events

- Coalesce DOM scans (at most one every 250 ms) and only report next/previous button changes: busy pages like YouTube sent hundreds of events per minute, each re-rendering the sidebar.
- Don't republish unchanged media state.
- Automatic Picture in Picture uses the video playing with sound, not the first video of the page, and leaving a tab whose video is already in Picture in Picture keeps it there.
- 'Back to tab' from the Picture in Picture window now works: the page of a tab in Picture in Picture stays attached to the window, hidden, so WebKit can bring the video back, and Ora then shows that tab like Safari.
fix(media): reliable Picture in Picture, no more media event flood
WebKit already reports them like Safari (core count capped at 8, no deviceMemory, real language and screen). Overriding them made Ora stand out from Safari users and look inconsistent — language differing from the Accept-Language header, a screen smaller than the window — which YouTube's bot detection flagged, stopping playback after a few minutes. Keep canvas, WebGL, audio and media device protections; tests guard against reintroducing the overrides.
applicationNameForUserAgent is appended by WebKit to its own Mozilla/AppleWebKit prefix but was given a full user agent, so the prefix appeared twice, with a hard-coded, outdated Safari version. Pass only Safari's suffix, using the installed Safari's version (validated, with a fallback).
Canvas and audio noise was a fixed offset on fixed samples, so Ora produced the same unique signature on every site and every session — an identifier in itself. Derive the noise from a secret drawn at each launch, mixed with the space, the browsing mode and, in the page, the top-level site and frame origin. A site reads stable values within a session; sites and sessions cannot be linked.
fix(privacy): make fingerprinting protection blend in like Safari
WebKit reports the end of a page's process (crash or memory reclaim), but Ora ignored it and left the tab blank and unresponsive. The visible page now reloads once with a notice, a background page reloads only when its tab is shown, and a page that ends again within 30 seconds of a reload shows a crash page instead of looping. Media sessions, Picture in Picture, permission prompts and autofill of the dead page are cleared.
Debug builds only. The sandbox forbids Ora to kill a web content process, so the command copies the kill -9 command for the space's pages to the clipboard, to run in Terminal.
@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[High impact] The PR changes browser behavior across page recovery, permissions, privacy, and app operations.

Keep the production app identifier unchanged before merging so release packaging and existing user settings continue to work.

Findings

  1. P1 Release identity no longer matches ▶
  2. P2 Media buttons stay disabled ▶
  3. P2 Reload bypasses the crash clock ▶

Summary

Adds recovery when a page's WebKit process ends:

  • Crashed pages reload or stop at a retry page.
  • Sites can ask for camera and microphone access.
  • Picture in Picture videos can return to their tab.
  • Fingerprinting noise varies by site, space, and session.
  • Ora routes external links and makes common page controls easier to use.
  • Local files load in tabs with a useful starting title.
  • Ora uses the installed Safari version in its web identity.
  • CI runs pinned checks and can test scheduled upstream updates.
  • Ora uses new app identities and an update feed.

Also changes permissions, Picture in Picture, privacy scripts, external links, CI, and app identity. The production identifier change needs fixing before merge. Two smaller issues affect media controls and retries through Reload.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Web content process ends] --> B[Clear media, prompts, and autofill]
  B --> C{Reload recorded within 30 seconds?}
  C -->|Yes| D[Show crash page]
  C -->|No| E{Tab selected?}
  E -->|Yes| F[Record reload and show notice]
  E -->|No| G[Wait until tab is selected]
  G --> F
  F --> H[Reload existing page]
  D --> I[Try Again]
  I --> J[Record reload]
  J --> H
Loading

Reviews (1) · Last reviewed commit: "chore(debug): add a command to simulate ..." · Reviewed by Greptile

Comment thread project.yml
CURRENT_PROJECT_VERSION: 105
PRODUCT_NAME: Ora
PRODUCT_BUNDLE_IDENTIFIER: com.orabrowser.app
PRODUCT_BUNDLE_IDENTIFIER: com.barto95100.ora

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Release identity no longer matches

The release app now uses com.barto95100.ora, but scripts/build.sh still exports with a provisioning profile mapped to com.orabrowser.app. Release packaging no longer has a matching profile entry. The new identifier also moves existing users to a different UserDefaults.standard domain without migrating their settings.

Keep the production identifier unchanged; use a separate identifier only for development builds.

Comment on lines +167 to +169
if (lastCaps && lastCaps.hasNext === hasNext && lastCaps.hasPrevious === hasPrevious) return;
lastCaps = { hasNext, hasPrevious };
post({ type: 'caps', hasNext, hasPrevious });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Media buttons stay disabled

caps() now caches button states even when MediaController has no session to receive them. If next/previous buttons exist before playback starts, the first message is dropped. Playback then creates a session with both controls disabled, while later scans suppress the unchanged message.

Save these values until a session exists, or resend them when playback creates one.

Comment on lines +343 to +346
if navigationError is PageCrashError {
crashRecovery.recordReload()
browserPage?.reload()
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Reload bypasses the crash clock

retryNavigation() records a crash retry, but the normal Reload command calls Tab.reload() without recording it. After the crash page has been open for more than 30 seconds, pressing Command-R and crashing again immediately makes Ora reload automatically instead of showing the crash page.

Route both retry paths through the same code so every user-requested crash reload updates the clock.

@barto95100
barto95100 deleted the fix/web-content-crash-recovery branch October 25, 2026 20:19
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