From fb0588b1ccb5da77e675e784451e149d24e007f1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 05:57:30 -0700 Subject: [PATCH 1/2] Structure native view resolution failures Co-authored-by: codex --- .../features/diffs/nativeReviewDiffSurface.test.ts | 12 +++++++++++- .../src/features/diffs/nativeReviewDiffSurface.ts | 10 +++++++++- .../features/terminal/nativeTerminalModule.test.ts | 12 +++++++++++- .../src/features/terminal/nativeTerminalModule.ts | 10 +++++++++- apps/mobile/src/native/nativeViewResolutionError.ts | 13 +++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/src/native/nativeViewResolutionError.ts diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts index 65e7539340d..83692d1905f 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts @@ -58,10 +58,20 @@ describe("resolveNativeReviewDiffView", () => { it("returns null when the view manager cannot be required", async () => { setExpoViewConfigAvailable(); + const cause = new Error("boom"); expoMocks.requireNativeView.mockImplementation(() => { - throw new Error("boom"); + throw cause; }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface"); + expect(resolveNativeReviewDiffView()).toBeNull(); + expect(consoleError).toHaveBeenCalledWith( + expect.objectContaining({ + _tag: "NativeViewResolutionError", + nativeModuleName: "T3ReviewDiffSurface", + cause, + }), + ); }); }); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 4a681663471..08d9d45b18c 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -2,6 +2,8 @@ import type { ComponentType } from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { requireNativeView } from "expo"; +import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; + const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface"; interface ExpoGlobalWithViewConfig { @@ -148,7 +150,13 @@ export function resolveNativeReviewDiffView(): ComponentType( NATIVE_REVIEW_DIFF_MODULE_NAME, ); - } catch { + } catch (cause) { + console.error( + new NativeViewResolutionError({ + nativeModuleName: NATIVE_REVIEW_DIFF_MODULE_NAME, + cause, + }), + ); return null; } diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts index c7418a52533..a37b40cfa6a 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.test.ts @@ -43,10 +43,20 @@ describe("resolveNativeTerminalSurfaceView", () => { it("returns null when the view manager cannot be required", async () => { setExpoViewConfigAvailable(); + const cause = new Error("boom"); expoMocks.requireNativeView.mockImplementation(() => { - throw new Error("boom"); + throw cause; }); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); + expect(resolveNativeTerminalSurfaceView()).toBeNull(); + expect(consoleError).toHaveBeenCalledWith( + expect.objectContaining({ + _tag: "NativeViewResolutionError", + nativeModuleName: "T3TerminalSurface", + cause, + }), + ); }); }); diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.ts index c4686a38b4e..a4369eac386 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.ts @@ -2,6 +2,8 @@ import type { ComponentType } from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { requireNativeView } from "expo"; +import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; + const NATIVE_TERMINAL_MODULE_NAME = "T3TerminalSurface"; interface ExpoGlobalWithViewConfig { @@ -53,7 +55,13 @@ export function resolveNativeTerminalSurfaceView(): ComponentType( NATIVE_TERMINAL_MODULE_NAME, ); - } catch { + } catch (cause) { + console.error( + new NativeViewResolutionError({ + nativeModuleName: NATIVE_TERMINAL_MODULE_NAME, + cause, + }), + ); return null; } diff --git a/apps/mobile/src/native/nativeViewResolutionError.ts b/apps/mobile/src/native/nativeViewResolutionError.ts new file mode 100644 index 00000000000..bfcf8351a66 --- /dev/null +++ b/apps/mobile/src/native/nativeViewResolutionError.ts @@ -0,0 +1,13 @@ +import * as Schema from "effect/Schema"; + +export class NativeViewResolutionError extends Schema.TaggedErrorClass()( + "NativeViewResolutionError", + { + nativeModuleName: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to resolve native view ${this.nativeModuleName}.`; + } +} From 4b5ceb1cbb7cb0f257c37b8432eb5500ee20b897 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:10:07 -0700 Subject: [PATCH 2/2] Avoid repeated native view resolution failures Co-authored-by: codex --- .../src/features/diffs/nativeReviewDiffSurface.test.ts | 3 +++ apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts | 6 ++++++ .../src/features/terminal/nativeTerminalModule.test.ts | 3 +++ apps/mobile/src/features/terminal/nativeTerminalModule.ts | 6 ++++++ 4 files changed, 18 insertions(+) diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts index 83692d1905f..975bf7be13d 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts @@ -66,6 +66,8 @@ describe("resolveNativeReviewDiffView", () => { const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface"); expect(resolveNativeReviewDiffView()).toBeNull(); + expect(resolveNativeReviewDiffView()).toBeNull(); + expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1); expect(consoleError).toHaveBeenCalledWith( expect.objectContaining({ _tag: "NativeViewResolutionError", @@ -73,5 +75,6 @@ describe("resolveNativeReviewDiffView", () => { cause, }), ); + expect(consoleError).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 08d9d45b18c..7660a047752 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -130,6 +130,7 @@ export interface NativeReviewDiffViewProps extends ViewProps { } let cachedNativeReviewDiffView: ComponentType | undefined; +let nativeReviewDiffViewResolutionFailed = false; function getExpoViewConfig(moduleName: string) { return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( @@ -142,6 +143,10 @@ export function resolveNativeReviewDiffView(): ComponentType { const { resolveNativeTerminalSurfaceView } = await import("./nativeTerminalModule"); expect(resolveNativeTerminalSurfaceView()).toBeNull(); + expect(resolveNativeTerminalSurfaceView()).toBeNull(); + expect(expoMocks.requireNativeView).toHaveBeenCalledTimes(1); expect(consoleError).toHaveBeenCalledWith( expect.objectContaining({ _tag: "NativeViewResolutionError", @@ -58,5 +60,6 @@ describe("resolveNativeTerminalSurfaceView", () => { cause, }), ); + expect(consoleError).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.ts index a4369eac386..e5b1f630073 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.ts @@ -35,6 +35,7 @@ export interface NativeTerminalSurfaceProps extends ViewProps { } let cachedNativeTerminalSurfaceView: ComponentType | undefined; +let nativeTerminalSurfaceViewResolutionFailed = false; function getExpoViewConfig(moduleName: string) { return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( @@ -47,6 +48,10 @@ export function resolveNativeTerminalSurfaceView(): ComponentType