Skip to content

feat(extensions): implement iframe-based isolation with TinyBase sync#1957

Merged
yujonglee merged 13 commits intomainfrom
devin/1764287392-extension-iframe-isolation
Nov 28, 2025
Merged

feat(extensions): implement iframe-based isolation with TinyBase sync#1957
yujonglee merged 13 commits intomainfrom
devin/1764287392-extension-iframe-isolation

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Nov 27, 2025

feat(extensions): implement iframe-based isolation with TinyBase sync

Summary

This PR implements VS Code-style iframe isolation for extension UIs, replacing the previous direct script injection approach. Extensions now run in sandboxed iframes with TinyBase state synchronized via postMessage.

Key changes:

  • New /app/ext-host route - Hosts extension iframes, initializes in-memory TinyBase store, and loads extension scripts
  • TinyBase postMessage synchronizer (iframe-sync.ts) - Custom synchronizer using createCustomSynchronizer for parent↔iframe communication
  • Updated TabContentExtension - Now renders extensions in iframes instead of directly in the main window
  • Exposed tinybase/ui-react to extensions via the build system and extension globals
  • Updated hello-world extension - Demonstrates both local state and synced TinyBase state with visual indicators

Architecture: The parent (main webview) owns the canonical TinyBase store with persistence. Each extension iframe has an in-memory MergeableStore that syncs with the parent via postMessage. The iframe never calls Tauri directly - the parent handles all Tauri operations including convertFileSrc for script URLs.

Updates since last revision

Input validation, content-type check & unmount guards (latest):

  • Added explicit type checking in validateSearch - verifies extensionId and scriptUrl are non-empty strings
  • Added URL validation using new URL() to reject malformed URLs and javascript: protocol
  • Returns undefined for invalid inputs instead of silent casts
  • Added early return in component for missing/invalid extensionId
  • Added content-type validation before script injection - throws error if response doesn't include javascript
  • Added isMountedRef guards to all setError and setComponent calls in loadExtensionScript to prevent state updates after unmount
  • Added security documentation to apps/web/content/docs/developers/8.extensions.mdx

Security hardening (previous):

  • Changed targetOrigin default from "*" to window.location.origin for postMessage security
  • Added origin validation in message handlers via isAllowedOrigin() check
  • Added explicit contentWindow null check with error logging instead of silent no-op
  • Added .catch() handlers for startSync() rejections with proper error logging
  • Added isMountedRef in ext-host.tsx to prevent state updates after unmount
  • Fixed pathname check in __root.tsx to use exact match (=== "/app/ext-host") or subpath (startsWith("/app/ext-host/")) to avoid matching unintended routes
  • Removed allow-same-origin from iframe sandbox attribute (now just sandbox="allow-scripts")

Previous updates:

  • Fixed TypeScript errors by using untyped MergeableStore from tinybase instead of schema-typed versions
  • Fixed iframe Tauri access issue: Parent now converts file paths via convertFileSrc and passes scriptUrl to iframe
  • Added __TAURI_INTERNALS__ polyfill in index.html: Provides stub implementations for Tauri APIs in iframe context
  • Added iframe context guards in auth.tsx and main.tsx to skip Tauri-dependent code
  • Created main-app-layout.tsx: Loaded via lazy() to prevent auth.tsx from being imported in iframe context

Review & Testing Checklist for Human

  • Test extension loading end-to-end - Run ONBOARDING=0 pnpm -F desktop tauri dev, click "Unknown" profile section, click "Hello World". Verify extension loads with "Store Status: Connected: Yes" in green. The content-type check may reject scripts if the dev server doesn't set proper headers containing "javascript".
  • Verify TinyBase sync works - Click "Synced +1" button and confirm the counter increments. Close and reopen the extension tab - synced counter should persist, local counter should reset to 0.
  • Test rapid open/close of extension tabs - Open and quickly close extension tabs multiple times to verify no React state update warnings appear in console (validates isMountedRef guards).
  • Verify main app routes still work - Navigate to settings, main view, etc. to ensure AuthProvider/BillingProvider still work correctly for non-iframe routes.

Test recording (recorded before latest security changes - recommend re-testing):

Extension test recording

Notes

  • Console shows "Side effect error: Error: Tauri not available in iframe" messages from use-config.ts - these are expected and come from the polyfill rejecting Tauri API calls in iframe context
  • The extension_state table used by hello-world may not exist in the TinyBase schema - verify this doesn't cause errors
  • This is a breaking change for the extension loading mechanism - any code relying on direct window access from extensions will need updating
  • Type safety is reduced at the iframe boundary by using untyped MergeableStore
  • Extensions cannot call Tauri APIs directly (by design) - they must use TinyBase for state and any future RPC layer for privileged operations
  • Security note: Removing allow-same-origin from sandbox is more restrictive - if extensions need same-origin access for any reason, this may need adjustment
  • Content-type check: The new validation requires the server to return a content-type header containing "javascript" - this should work for properly configured servers but may need adjustment if issues arise

Requested by: yujonglee (@yujonglee)
Link to Devin run: https://app.devin.ai/sessions/1f3bfa6345fe4092ab3dcd1a59391e00

- Add TinyBase postMessage synchronizer utilities for iframe communication
- Create /app/ext-host route for extension iframe hosting
- Update TabContentExtension to render extensions in isolated iframes
- Expose TinyBase hooks to extensions via extension-globals
- Update build script to handle tinybase/ui-react imports
- Update hello-world extension to demonstrate TinyBase store access and sync

This implements a VS Code-like extension isolation pattern where extension UIs
run in sandboxed iframes with TinyBase state synchronized via postMessage.

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Nov 27, 2025

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit 0cdf9ba
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/692918584e466b0008baac65
😎 Deploy Preview https://deploy-preview-1957--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

📝 Walkthrough

Walkthrough

Replaces in-process extension UIs with an iframe-hosted ext-host route, adds parent↔iframe TinyBase synchronizers over postMessage, exposes tinybase/ui-react via a window bridge for runtime extensions, and guards storage/auth when running inside iframe contexts.

Changes

Cohort / File(s) Summary
Iframe Host Route
apps/desktop/src/routes/app/ext-host.tsx
New route that initializes extension globals and a TinyBase mergeable store, creates a parent synchronizer, fetches and injects a remote extension script (scriptUrl), mounts the exported React component inside a TinyBaseProvider, and handles loading/error states and cleanup.
Extension Shell → Iframe
apps/desktop/src/components/main/body/extensions/index.tsx
Replaces in-page dynamic extension loading with an <iframe> host: builds iframe src from panelInfo.entry_path and extensionId, manages iframeRef, starts/stops createIframeSynchronizer, handles iframe onLoad, and renders not-found UI when no entry path.
TinyBase Cross-context Sync
apps/desktop/src/store/tinybase/iframe-sync.ts
Adds createIframeSynchronizer (parent → child) and createParentSynchronizer (iframe → parent) that wrap TinyBase synchronizers using a tinybase-sync postMessage envelope, with message filtering, send/registerReceive/destroy, and a 5s timeout.
Global TinyBase UI Bridge
apps/desktop/src/extension-globals.ts
Exposes tinybase/ui-react as window.__hypr_tinybase_ui_react, adds type to Window, and assigns it during initExtensionGlobals.
Build: tinybase/ui-react Resolver
extensions/build.mjs
Adds resolver/loader hooks so extension builds importing tinybase/ui-react resolve at runtime to a module that returns window.__hypr_tinybase_ui_react.
Example Extension: TinyBase Usage
extensions/hello-world/ui.tsx
Reworked example to include a local counter and a synced counter backed by TinyBase (useStore, useRow, useSetRowCallback), adds store connectivity indicators and timestamps.
Root/Layout & Routing Guards
apps/desktop/src/routes/__root.tsx, apps/desktop/src/components/main-app-layout.tsx, apps/desktop/src/main.tsx, apps/desktop/src/routes/app/route.tsx
Lazily loads MainAppLayout, bypasses top-level providers for /app/ext-host routes, moves navigation/event wiring into the lazy layout, initializes extension globals at startup, and conditions feature mounting for iframe contexts.
Iframe Polyfill & Auth Guards
apps/desktop/index.html, apps/desktop/src/auth.tsx
Adds an index.html polyfill stub for Tauri internals inside iframes; makes storage and Supabase client creation conditional when running inside iframe contexts (isIframeContext).
TinyBase Schema & Persister Changes
apps/desktop/src/store/tinybase/schema-internal.ts, apps/desktop/src/store/tinybase/main.ts
Adds extension_state table schema (counter, last_updated) and conditions persister creation on persist flag (dependency arrays adjusted).
Devtools & Typing Cleanup
apps/desktop/src/routes/app/devtool.tsx, apps/desktop/src/types/extensions.ts
Adds ExtensionStateMonitor to Devtools using useRow; removes an unused import.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Parent as Desktop App (parent)
    participant Iframe as Embedded <iframe>
    participant ExtHost as /app/ext-host (iframe page)
    participant Store as TinyBase Mergeable Store

    Parent->>Iframe: create <iframe src="/app/ext-host?extensionId&scriptUrl">
    Parent->>Parent: createIframeSynchronizer(store, iframe) — start
    Iframe->>ExtHost: ext-host route loads with search params
    ExtHost->>Store: create mergeable store
    ExtHost->>ExtHost: createParentSynchronizer(store) — start
    ExtHost->>ExtHost: fetch scriptUrl → validate JS → inject script
    ExtHost->>ExtHost: read __hypr_panel_exports.default → mount inside TinyBaseProvider
    note over ExtHost,Parent: Bidirectional TinyBase sync via postMessage (tinybase-sync envelope)
    ExtHost->>Parent: postMessage(sync)
    Parent->>Parent: iframe synchronizer applies → update parent store
    Parent->>Iframe: postMessage(sync update)
    ExtHost->>ExtHost: parent synchronizer applies incoming updates
    Parent->>Parent: destroy iframe synchronizer on parent unmount
    ExtHost->>ExtHost: destroy parent synchronizer on iframe unload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • apps/desktop/src/store/tinybase/iframe-sync.ts — envelope format, origin/iframe filtering, timeout, and typing.
    • apps/desktop/src/routes/app/ext-host.tsx — secure fetch/injection of remote script, content-type and export validation, async lifecycle and cleanup.
    • apps/desktop/src/components/main/body/extensions/index.tsx — iframe URL composition, onLoad handling, single-synchronizer invariant.
    • extensions/build.mjs — resolver/loader mapping and runtime global bridge for tinybase/ui-react.
    • apps/desktop/src/auth.tsx & apps/desktop/index.html — iframe polyfill semantics and conditional auth/storage initialization.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(extensions): implement iframe-based isolation with TinyBase sync' accurately summarizes the main architectural change: replacing direct script injection with iframe-based isolation and TinyBase synchronization.
Description check ✅ Passed The description comprehensively explains the changeset, detailing the new /app/ext-host route, TinyBase synchronizer, iframe updates, security hardening, and testing procedures, all of which are directly related to the file changes provided.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1764287392-extension-iframe-isolation

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

@netlify
Copy link

netlify bot commented Nov 27, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit 0cdf9ba
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/69291858be13540008b8a3db
😎 Deploy Preview https://deploy-preview-1957--hyprnote-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (7)
apps/desktop/src/components/main/body/extensions/index.tsx (1)

106-119: Consider adding error handling for startSync().

If synchronization fails to start, the user won't see any feedback. Consider handling potential errors.

     const synchronizer = createIframeSynchronizer<Schemas>(
       store,
       iframeRef.current,
     );
     synchronizerRef.current = synchronizer;
-    synchronizer.startSync();
+    synchronizer.startSync().catch((err) => {
+      console.error("Failed to start iframe sync:", err);
+    });
   }, [store]);
extensions/hello-world/ui.tsx (1)

46-52: Consider disabling the synced button when store is not connected.

If the store is not connected, clicking "Synced +1" will update state on an undefined store, which may silently fail or cause unexpected behavior.

             <Button
               onClick={handleIncrementSynced}
               variant="outline"
               size="sm"
               className="bg-green-50 hover:bg-green-100 border-green-200"
+              disabled={!storeConnected}
             >
               Synced +1
             </Button>
apps/desktop/src/routes/app/ext-host.tsx (2)

29-45: Potential stale closure: loadExtensionScript captures entryPath but effect has empty deps.

If entryPath changes without remounting the component, the effect won't re-run with the new value. Consider adding entryPath to the dependency array, or if remounting is guaranteed, add a comment documenting this assumption.

   useEffect(() => {
     initExtensionGlobals();

     const store = createMergeableStore();
     storeRef.current = store;

     const synchronizer = createParentSynchronizer(store);
     synchronizerRef.current = synchronizer;

     synchronizer.startSync().then(() => {
       loadExtensionScript();
     });

     return () => {
       synchronizer.destroy();
     };
-  }, []);
+  }, [entryPath]);

Note: You'll need to move loadExtensionScript inside the effect or wrap it with useCallback with entryPath as a dependency to satisfy the linter.


38-40: Missing error handling for startSync() failure.

If synchronization fails, the extension won't load but no error will be displayed to the user.

-    synchronizer.startSync().then(() => {
-      loadExtensionScript();
-    });
+    synchronizer.startSync()
+      .then(() => {
+        loadExtensionScript();
+      })
+      .catch((err) => {
+        setError(`Failed to start sync: ${err instanceof Error ? err.message : String(err)}`);
+      });
apps/desktop/src/store/tinybase/iframe-sync.ts (3)

17-26: Consider stricter validation in the type guard.

The type guard only validates that payload is an array but doesn't check its length or element types. Malformed messages could pass validation and cause runtime errors during destructuring (lines 61, 112).

Consider adding stricter validation:

 function isTinybaseSyncEnvelope(data: unknown): data is TinybaseSyncEnvelope {
   return (
     typeof data === "object" &&
     data !== null &&
     "kind" in data &&
     data.kind === "tinybase-sync" &&
     "payload" in data &&
-    Array.isArray(data.payload)
+    Array.isArray(data.payload) &&
+    data.payload.length === 4 &&
+    typeof data.payload[0] === "string" &&
+    (data.payload[1] === null || typeof data.payload[1] === "string") &&
+    typeof data.payload[2] === "number"
   );
 }

74-74: Extract magic number to named constant.

The timeout value 5 (seconds) is hardcoded and duplicated in both functions. This makes it harder to maintain and understand the timeout behavior.

+const SYNC_TIMEOUT_SECONDS = 5;
+
 /**
  * Creates a TinyBase synchronizer for the parent (main webview) side
  * that syncs with an iframe via postMessage.
  */
 export function createIframeSynchronizer<Schemas extends [object, object]>(
   store: MergeableStore<Schemas>,
   iframe: HTMLIFrameElement,
   targetOrigin = "*",
 ): Synchronizer<Schemas> {
   // ...
-    5, // request timeout in seconds
+    SYNC_TIMEOUT_SECONDS,
   );

32-129: Consider extracting shared synchronizer logic.

The two functions share ~80% of their implementation. While the context-specific differences (parent vs. iframe) may justify some duplication, extracting common logic would improve maintainability.

Consider a helper that accepts send/receive strategy:

function createPostMessageSynchronizer<Schemas extends [object, object]>(
  store: MergeableStore<Schemas>,
  config: {
    send: (envelope: TinybaseSyncEnvelope, origin: string) => void;
    shouldAcceptMessage: (event: MessageEvent) => boolean;
    targetOrigin: string;
  },
): Synchronizer<Schemas> {
  const clientId = getUniqueId();
  let handler: ((event: MessageEvent) => void) | null = null;

  const synchronizer = createCustomSynchronizer(
    store as MergeableStore<[object, object]>,
    (_toClientId, requestId, message, body) => {
      const payload: TinybaseSyncPayload = [clientId, requestId, message, body];
      config.send({ kind: "tinybase-sync", payload }, config.targetOrigin);
    },
    (receive) => {
      handler = (event: MessageEvent) => {
        if (!config.shouldAcceptMessage(event)) return;
        if (!isTinybaseSyncEnvelope(event.data)) return;
        const [fromClientId, requestId, message, body] = event.data.payload;
        receive(fromClientId, requestId, message, body);
      };
      window.addEventListener("message", handler);
    },
    () => {
      if (handler) {
        window.removeEventListener("message", handler);
        handler = null;
      }
    },
    SYNC_TIMEOUT_SECONDS,
  );

  return synchronizer as unknown as Synchronizer<Schemas>;
}

Then implement the public functions as thin wrappers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02d5673 and e23c3f1.

📒 Files selected for processing (6)
  • apps/desktop/src/components/main/body/extensions/index.tsx (3 hunks)
  • apps/desktop/src/extension-globals.ts (3 hunks)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
  • apps/desktop/src/store/tinybase/iframe-sync.ts (1 hunks)
  • extensions/build.mjs (2 hunks)
  • extensions/hello-world/ui.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • apps/desktop/src/extension-globals.ts
  • apps/desktop/src/store/tinybase/iframe-sync.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/extension-globals.ts
  • apps/desktop/src/store/tinybase/iframe-sync.ts
  • extensions/hello-world/ui.tsx
  • apps/desktop/src/components/main/body/extensions/index.tsx
  • apps/desktop/src/routes/app/ext-host.tsx
🪛 GitHub Actions: .github/workflows/desktop_ci.yaml
apps/desktop/src/routes/app/ext-host.tsx

[error] 11-11: TS2345: Argument of type '"/app/ext-host"' is not assignable to parameter of type 'keyof FileRoutesByPath | undefined'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote-storybook
  • GitHub Check: Header rules - hyprnote-storybook
  • GitHub Check: Pages changed - hyprnote-storybook
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
🔇 Additional comments (8)
extensions/build.mjs (1)

132-135: LGTM!

The TinyBase UI React aliasing follows the established pattern for external dependencies. The resolution and loading hooks are consistent with existing handlers for React, ReactDOM, and @hypr/* packages.

Also applies to: 178-184

apps/desktop/src/extension-globals.ts (1)

4-4: LGTM!

The global bridge for TinyBase UI React is properly typed and initialized, aligning with the build-time aliasing in build.mjs.

Also applies to: 17-17, 26-26

apps/desktop/src/components/main/body/extensions/index.tsx (2)

152-159: LGTM!

The iframe implementation with sandbox="allow-scripts allow-same-origin" provides appropriate isolation while enabling the required postMessage communication for TinyBase sync.


98-99: I'll help you verify this review comment about the type casting issue. Let me investigate the type definitions and usage patterns in the codebase.
<function_calls>

#!/bin/bash

First, find where useStores is defined and how it's typed

rg -nP --type=ts "useStores(" -C5 | head -100


</function_calls>

This will help me understand the return type of useStores() and why the double cast might be necessary. Let me also search for the Store type definition.
<function_calls>

#!/bin/bash

Find Store type/interface definition

rg -nP --type=ts "type Store|interface Store" -C3 | head -150


</function_calls>

Let me also look at the specific file in question to see the context around lines 98-99:
<function_calls>

#!/bin/bash

Read the extensions/index.tsx file around lines 95-110

find . -path "*/extensions/index.tsx" -type f | head -5


</function_calls>

And check how useStores hook is defined:
<function_calls>

#!/bin/bash

Find the hook definition

rg -nP --type=ts "export.*useStores|function useStores|const useStores" -C10 | head -150


</function_calls>

extensions/hello-world/ui.tsx (1)

25-36: LGTM!

The TinyBase integration correctly uses useStore, useRow, and useSetRowCallback hooks for managing extension-specific persistent state. The type assertion on useRow is a common pattern for TinyBase.

apps/desktop/src/routes/app/ext-host.tsx (2)

62-86: Script injection pattern is functional but consider using new Function or dynamic import alternatives.

The current approach works but has limitations (e.g., CSP restrictions). The backup/restore of __hypr_panel_exports is a good safeguard for concurrent loads.


11-17: Route is correctly registered in TanStack Router file-based routing.

The file apps/desktop/src/routes/app/ext-host.tsx is properly placed for TanStack Router's file-based routing system. The path mapping is correct:

  • File location: apps/desktop/src/routes/app/ext-host.tsx → Route path: /app/ext-host
  • Parent route /app is defined in apps/desktop/src/routes/app/route.tsx with proper Outlet
  • Root route is configured in apps/desktop/src/routes/__root.tsx

The route definition uses the correct createFileRoute syntax matching all other routes in the codebase. TanStack Router v1.139.3 automatically generates the route tree from the file structure, making this configuration valid. No TS2345 error exists.

apps/desktop/src/store/tinybase/iframe-sync.ts (1)

97-103: Verify window.parent context.

The code assumes window.parent exists and is different from window. In a top-level window, window.parent === window, which would create a messaging loop or fail the source check at line 109.

Please verify that this synchronizer will only be called from within an actual iframe context, and consider adding a guard:

if (window === window.parent) {
  throw new Error("createParentSynchronizer must be called from within an iframe");
}

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/desktop/src/components/main/body/extensions/index.tsx (1)

95-96: Double type casting suggests type mismatch.

The as unknown as Store pattern indicates the types from useStores() don't align with your local Store type. This could hide runtime issues if the actual store shape differs.

Consider either:

  1. Properly typing the stores registry upstream, or
  2. Adding runtime validation before using the store.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e23c3f1 and f3b7db0.

⛔ Files ignored due to path filters (1)
  • apps/desktop/src/routeTree.gen.ts is excluded by !**/*.gen.ts
📒 Files selected for processing (2)
  • apps/desktop/src/components/main/body/extensions/index.tsx (3 hunks)
  • apps/desktop/src/store/tinybase/iframe-sync.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/store/tinybase/iframe-sync.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/components/main/body/extensions/index.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
🔇 Additional comments (4)
apps/desktop/src/components/main/body/extensions/index.tsx (4)

103-116: Synchronizer not recreated if store changes after iframe loads.

The handleIframeLoad callback captures store via dependency, but if store reference changes after the iframe has already loaded, no new synchronizer will be created with the updated store. The iframe's onLoad only fires once.

If store can change during the component's lifetime, consider adding an effect that watches store changes:

+ useEffect(() => {
+   if (iframeRef.current && store) {
+     if (synchronizerRef.current) {
+       synchronizerRef.current.destroy();
+     }
+     const synchronizer = createIframeSynchronizer(
+       store as unknown as MergeableStore,
+       iframeRef.current,
+     );
+     synchronizerRef.current = synchronizer;
+     synchronizer.startSync();
+   }
+ }, [store]);

If the store reference is guaranteed stable, this can be safely ignored.


142-145: Clean URL construction with proper encoding.

Good use of URLSearchParams for building the iframe src - this properly encodes the parameters and prevents injection issues.


118-125: Cleanup effect correctly handles unmount.

The empty dependency array ensures cleanup only runs on unmount, and the null assignment after destroy prevents stale reference issues.


149-156: I'll help you verify the security concern about the iframe sandbox and postMessage targetOrigin. Let me start by locating the relevant files and examining the postMessage implementation.
<function_calls>


#!/bin/bash

Find iframe-sync.ts and related synchronizer files

fd -e ts -e tsx | grep -i 'iframe|sync' | head -20


</function_calls>

Now let me search for postMessage and targetOrigin usage across the codebase:
<function_calls>


#!/bin/bash

Search for postMessage and targetOrigin usage

rg -n "postMessage|targetOrigin" --type=ts --type=tsx -C3


</function_calls>

Let me also check the extensions index file structure to understand the context better:
<function_calls>


#!/bin/bash

Read the extensions index file to see full context

wc -l apps/desktop/src/components/main/body/extensions/index.tsx


</function_calls>

<function_calls>


#!/bin/bash

Read relevant portions of the extensions file around lines 149-156

sed -n '140,170p' apps/desktop/src/components/main/body/extensions/index.tsx


</function_calls>

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/desktop/src/routes/app/ext-host.tsx (2)

10-16: Tighten validateSearch to fail fast on missing/invalid params

validateSearch currently just casts extensionId and scriptUrl to string, so malformed or missing values silently become undefined as string and only surface later (e.g., “No script URL provided”). Consider validating types and throwing early to keep route state consistent and errors easier to reason about.

For example:

validateSearch: (search: Record<string, unknown>) => {
  const { extensionId, scriptUrl } = search;

  if (typeof extensionId !== "string" || typeof scriptUrl !== "string") {
    throw new Error("Missing or invalid extensionId/scriptUrl in search params");
  }

  return { extensionId, scriptUrl };
},

This keeps the Route contract tight and avoids passing bad props into the host.


47-92: Script loading flow looks solid; consider clarifying assumptions around __hypr_panel_exports

The script loading logic (fetch, inject inline <script>, snapshot/restore __hypr_panel_exports, and wiring the default export into React state) is coherent and minimal.

Two small, optional improvements:

  • Document the assumption that extension bundles synchronously set window.__hypr_panel_exports.default during evaluation; otherwise authors might be surprised that async initializers aren’t supported.
  • Optionally log (or surface) a more specific hint when exports is missing but the script evaluated, to help distinguish “no default export set” from runtime errors inside the extension script.

No blockers here from a correctness standpoint.

apps/desktop/src/components/main/body/extensions/index.tsx (2)

96-103: Defensively handle missing TinyBase store for STORE_ID

store is derived as:

const stores = useStores();
const store = stores[STORE_ID] as unknown as Store | undefined;

If, for any reason, stores[STORE_ID] is undefined (e.g., store not registered yet or misconfigured), the iframe will render but handleIframeLoad will no-op, so the extension UI appears but never syncs and no explicit error is shown.

If STORE_ID being present is a hard invariant, consider asserting early:

if (!store) {
  throw new Error(`TinyBase store "${STORE_ID}" not found`);
}

Or, if you want graceful handling:

if (!store) {
  return (
    <StandardTabWrapper>
      {/* simple “Store unavailable” UI */}
    </StandardTabWrapper>
  );
}

Either path makes this failure mode much easier to diagnose.


143-147: Iframe src construction is safe and readable

Using convertFileSrc(panelInfo.entry_path) plus URLSearchParams for extensionId and scriptUrl is a good balance of clarity and safety (proper encoding for arbitrary paths/IDs).

If you ever need to extend this, consider centralizing host URL construction into a helper, but there’s no immediate need.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3b7db0 and 75d3af0.

📒 Files selected for processing (2)
  • apps/desktop/src/components/main/body/extensions/index.tsx (3 hunks)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/routes/app/ext-host.tsx
  • apps/desktop/src/components/main/body/extensions/index.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-24T16:32:19.706Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:19.706Z
Learning: Applies to **/*.{ts,tsx} : Use `motion/react` instead of `framer-motion`.

Applied to files:

  • apps/desktop/src/components/main/body/extensions/index.tsx
🧬 Code graph analysis (2)
apps/desktop/src/routes/app/ext-host.tsx (4)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (14-17)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createParentSynchronizer (92-142)
apps/desktop/src/extension-globals.ts (1)
  • initExtensionGlobals (21-32)
plugins/local-llm/src/ext/plugin.rs (1)
  • store (320-320)
apps/desktop/src/components/main/body/extensions/index.tsx (5)
plugins/local-llm/src/ext/plugin.rs (1)
  • store (320-320)
apps/desktop/src/store/tinybase/main.ts (2)
  • STORE_ID (36-36)
  • Store (73-73)
apps/desktop/src/components/main/body/extensions/registry.ts (1)
  • getPanelInfoByExtensionId (71-76)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createIframeSynchronizer (33-84)
apps/desktop/src/components/main/body/index.tsx (1)
  • StandardTabWrapper (349-368)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
🔇 Additional comments (2)
apps/desktop/src/routes/app/ext-host.tsx (1)

94-120: Error and loading UI are straightforward and adequate

The error and loading branches are simple, read well, and correctly gate on both Component and storeRef.current. Wrapping the extension view in TinyBaseProvider with the in-memory store is exactly what the iframe host needs.

No changes needed here.

apps/desktop/src/components/main/body/extensions/index.tsx (1)

119-126: Synchronizer cleanup on unmount is correct and minimal

The useEffect cleanup that destroys any existing synchronizer and nulls the ref is exactly what’s needed to avoid dangling message listeners when the tab unmounts.

No changes needed.

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/desktop/src/routes/__root.tsx (1)

32-40: Prefer TanStack Router hooks over direct window.location access.

Using window.location.pathname directly is non-idiomatic for TanStack Router. Consider using the router's state via hooks for better type safety and consistency:

+import {
+  createRootRouteWithContext,
+  type LinkProps,
+  Outlet,
+  useNavigate,
+  useRouterState,
+} from "@tanstack/react-router";

function Component() {
-  // ext-host route runs in iframe without Tauri access, so skip auth/billing providers
-  // and navigation events (which use Tauri APIs)
-  const isExtHost =
-    typeof window !== "undefined" &&
-    window.location.pathname.startsWith("/app/ext-host");
+  // ext-host route runs in iframe without Tauri access, so skip auth/billing providers
+  // and navigation events (which use Tauri APIs)
+  const pathname = useRouterState({ select: (s) => s.location.pathname });
+  const isExtHost = pathname.startsWith("/app/ext-host");

  if (isExtHost) {
    return <Outlet />;
  }

  return <MainAppLayout />;
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75d3af0 and 10a0d2a.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/__root.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/routes/__root.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: fmt
🔇 Additional comments (1)
apps/desktop/src/routes/__root.tsx (1)

45-55: LGTM!

Clean extraction of the main app layout logic into a separate component. The wrapping of AuthProvider, BillingProvider, and navigation event handling is correct and maintains the existing behavior for non-ext-host routes.

devin-ai-integration bot and others added 3 commits November 28, 2025 00:33
…ing in iframe

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
…s errors

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/desktop/src/main.tsx (1)

89-91: Consider guarding initWindowsPlugin so it never runs in iframe/ext-host contexts

Right now initWindowsPlugin() runs unconditionally, relying on the __TAURI_INTERNALS__ polyfill to absorb calls inside iframes. To avoid any accidental Tauri calls or noisy rejected Promises in the extension-host iframes, it’s safer to align this with the iframe guard you added in auth.tsx and only initialize the windows plugin in the main webview.

For example:

-// Initialize plugins - the polyfill in index.html handles iframe context
-initWindowsPlugin();
-initExtensionGlobals();
+// Initialize plugins - only run the Windows plugin in the main (non-iframe) webview.
+const isIframeContext =
+  typeof window !== "undefined" && window.self !== window.top;
+
+if (!isIframeContext) {
+  initWindowsPlugin();
+}
+
+initExtensionGlobals();

This keeps the main window behavior unchanged while making the iframe story more explicit.

apps/desktop/src/components/main-app-layout.tsx (1)

29-60: Handle failures from plugin .listen calls to avoid unhandled rejections

The event wiring looks correct and you clean up listeners properly, but the .listen(...) calls return Promises that aren’t error‑handled. If the plugins are unavailable or fail to register, this can produce unhandled rejections.

You can add minimal logging while keeping behavior the same when things work:

   useEffect(() => {
@@
-    windowsEvents
-      .navigate(webview)
-      .listen(({ payload }) => {
-        navigate({ to: payload.path, search: payload.search ?? undefined });
-      })
-      .then((fn) => {
-        unlistenNavigate = fn;
-      });
+    windowsEvents
+      .navigate(webview)
+      .listen(({ payload }) => {
+        navigate({ to: payload.path, search: payload.search ?? undefined });
+      })
+      .then((fn) => {
+        unlistenNavigate = fn;
+      })
+      .catch((error) => {
+        console.error("[MainAppLayout] Failed to bind windows navigate events", error);
+      });
@@
-    deeplink2Events.deepLinkEvent
-      .listen(({ payload }) => {
-        navigate({ to: payload.to, search: payload.search });
-      })
-      .then((fn) => {
-        unlistenDeepLink = fn;
-      });
+    deeplink2Events.deepLinkEvent
+      .listen(({ payload }) => {
+        navigate({ to: payload.to, search: payload.search });
+      })
+      .then((fn) => {
+        unlistenDeepLink = fn;
+      })
+      .catch((error) => {
+        console.error("[MainAppLayout] Failed to bind deeplink events", error);
+      });

This keeps the hook’s semantics but makes it more resilient when the desktop plugins aren’t present or misbehave.

apps/desktop/index.html (1)

17-31: Iframe Tauri polyfill looks good; consider documenting the “fail-closed” semantics

The early, iframe‑only __TAURI_INTERNALS__ stub is a solid way to keep @tauri-apps/api and plugins from throwing in extension iframes, while making any actual Tauri calls reliably fail (invoke rejects, transformCallback is a no‑op, convertFileSrc is identity).

To make future usage clearer, I’d suggest a brief inline note that this polyfill is intentionally “fail‑closed” (no real Tauri side effects) and must not be relied on by extensions for real filesystem or window operations. That will help avoid someone later leaning on convertFileSrc here as if it were the real thing.

apps/desktop/src/auth.tsx (1)

195-202: Fix useAuth guard to correctly detect missing provider

AuthContext is initialized with null, but useAuth only throws when context === undefined, so calling useAuth() outside an AuthProvider will quietly return null instead of giving the intended error.

You can tighten this up with:

 export function useAuth() {
   const context = useContext(AuthContext);

-  if (context === undefined) {
+  if (!context) {
     throw new Error("'useAuth' must be used within an 'AuthProvider'");
   }

   return context;
 }

This makes misuse fail fast with a clear message instead of a later null dereference.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10a0d2a and 1bd4397.

📒 Files selected for processing (5)
  • apps/desktop/index.html (1 hunks)
  • apps/desktop/src/auth.tsx (1 hunks)
  • apps/desktop/src/components/main-app-layout.tsx (1 hunks)
  • apps/desktop/src/main.tsx (1 hunks)
  • apps/desktop/src/routes/__root.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/routes/__root.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/components/main-app-layout.tsx
  • apps/desktop/src/main.tsx
  • apps/desktop/src/auth.tsx
🧬 Code graph analysis (1)
apps/desktop/src/components/main-app-layout.tsx (3)
apps/desktop/src/auth.tsx (1)
  • AuthProvider (74-193)
apps/desktop/src/billing.tsx (1)
  • BillingProvider (43-120)
plugins/windows/src/ext.rs (1)
  • navigate (19-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Redirect rules - hyprnote-storybook
  • GitHub Check: Header rules - hyprnote-storybook
  • GitHub Check: Pages changed - hyprnote-storybook
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: fmt
🔇 Additional comments (2)
apps/desktop/src/components/main-app-layout.tsx (1)

11-27: Good separation of main app vs iframe/ext-host concerns

Dynamic MainAppLayout that wraps routes with AuthProvider/BillingProvider and calls useNavigationEvents() cleanly keeps auth/billing and Tauri-dependent bits out of the iframe/ext-host path while preserving behavior for the main window.

apps/desktop/src/auth.tsx (1)

22-45: Iframe-aware auth initialization is consistent and avoids Tauri usage in extension hosts

The isIframeContext guard plus tauriStorage: SupportedStorage | null and the conditional supabase creation cleanly prevent Tauri storage and Supabase from being instantiated in iframe/ext-host contexts. Downstream checks (if (!supabase) return;) in the effects and methods mean iframe imports would safely no-op rather than touching Tauri APIs.

This aligns well with the new layout split and the iframe polyfill strategy.

Also applies to: 47-53

devin-ai-integration bot and others added 3 commits November 28, 2025 01:00
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
- Use window.location.origin as default targetOrigin instead of '*'
- Add origin validation in message handlers for both iframe and parent synchronizers
- Handle missing contentWindow explicitly with error logging
- Add .catch handlers for startSync() rejections in extensions/index.tsx
- Add mounted ref and error handling for startSync() in ext-host.tsx
- Fix pathname check in __root.tsx to use exact match or subpath
- Remove allow-same-origin from iframe sandbox attribute

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/desktop/src/routes/__root.tsx (1)

31-34: Pathname matching is now precise.

The exact match (pathname === "/app/ext-host") combined with the prefix check (startsWith("/app/ext-host/")) properly addresses the previous concern about matching unintended routes like /app/ext-host-debug.

Based on past review comments indicating the need for more precise pathname matching.

apps/desktop/src/routes/app/ext-host.tsx (1)

27-27: Unmount guard and error handling properly implemented.

The isMountedRef pattern correctly prevents state updates after unmount, and the .catch() handler on startSync() ensures sync failures are logged and surfaced to the user. This addresses the previous review concern about unhandled promise rejections and post-unmount updates.

Based on past review comments requesting unmount guards and error handling for startSync().

Also applies to: 29-60

apps/desktop/src/components/main/body/extensions/index.tsx (2)

104-122: Sync error handling properly implemented.

The .catch() handler on startSync() now logs errors with context (extension ID). This addresses the previous concern about silent failures when the iframe sync handshake fails.

Based on past review comments requesting error handling for startSync().


156-163: Sandbox attribute correctly restricts iframe privileges.

The sandbox="allow-scripts" (without allow-same-origin) prevents extension code from accessing window.parent and manipulating the parent DOM. This addresses the security concern raised in previous reviews while still allowing postMessage communication.

Based on past review comments about removing allow-same-origin for better isolation.

🧹 Nitpick comments (3)
apps/desktop/src/routes/__root.tsx (1)

40-44: Consider adding a loading indicator instead of null fallback.

The Suspense fallback={null} will show nothing during MainAppLayout loading. While this may be brief, a minimal loading indicator (spinner or skeleton) would provide better UX feedback during the initial load.

-    <Suspense fallback={null}>
+    <Suspense fallback={<div className="flex items-center justify-center h-screen"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-neutral-900" /></div>}>
       <MainAppLayout />
     </Suspense>
apps/desktop/src/routes/app/ext-host.tsx (1)

11-13: Validate search parameters to prevent runtime errors.

The validateSearch function casts parameters without validation. If extensionId or scriptUrl are missing or malformed, the component will fail at runtime rather than at the route level.

  validateSearch: (search: Record<string, unknown>) => ({
-    extensionId: search.extensionId as string,
-    scriptUrl: search.scriptUrl as string,
+    extensionId:
+      typeof search.extensionId === "string" && search.extensionId
+        ? search.extensionId
+        : (() => {
+            throw new Error("Missing or invalid extensionId");
+          })(),
+    scriptUrl:
+      typeof search.scriptUrl === "string" && search.scriptUrl
+        ? search.scriptUrl
+        : (() => {
+            throw new Error("Missing or invalid scriptUrl");
+          })(),
  }),
apps/desktop/src/components/main/body/extensions/index.tsx (1)

96-97: Type assertion chain is fragile.

The double cast stores[STORE_ID] as unknown as Store | undefined bypasses TypeScript's type checking. If useStores() returns a different structure or if STORE_ID is incorrect, this will silently fail at runtime.

Consider a runtime guard:

  const stores = useStores();
- const store = stores[STORE_ID] as unknown as Store | undefined;
+ const store = stores[STORE_ID];
+ if (!store) {
+   console.error(`[extensions] Store "${STORE_ID}" not found`);
+   return (
+     <StandardTabWrapper>
+       <div className="flex items-center justify-center h-full">
+         <p className="text-red-500">Store initialization error</p>
+       </div>
+     </StandardTabWrapper>
+   );
+ }

Then update the subsequent checks to handle the typed store safely.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fbb5c1 and 2ab69eb.

📒 Files selected for processing (4)
  • apps/desktop/src/components/main/body/extensions/index.tsx (3 hunks)
  • apps/desktop/src/routes/__root.tsx (2 hunks)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
  • apps/desktop/src/store/tinybase/iframe-sync.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/store/tinybase/iframe-sync.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/routes/__root.tsx
  • apps/desktop/src/components/main/body/extensions/index.tsx
  • apps/desktop/src/routes/app/ext-host.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-24T16:32:19.706Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:19.706Z
Learning: Applies to **/*.{ts,tsx} : Use `motion/react` instead of `framer-motion`.

Applied to files:

  • apps/desktop/src/components/main/body/extensions/index.tsx
🧬 Code graph analysis (3)
apps/desktop/src/routes/__root.tsx (2)
apps/desktop/src/components/main-app-layout.tsx (1)
  • MainAppLayout (17-27)
plugins/windows/src/events.rs (2)
  • window (14-14)
  • window (32-32)
apps/desktop/src/components/main/body/extensions/index.tsx (4)
apps/desktop/src/store/tinybase/main.ts (2)
  • STORE_ID (36-36)
  • Store (73-73)
apps/desktop/src/components/main/body/extensions/registry.ts (1)
  • getPanelInfoByExtensionId (71-76)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createIframeSynchronizer (41-102)
apps/desktop/src/components/main/body/index.tsx (1)
  • StandardTabWrapper (349-368)
apps/desktop/src/routes/app/ext-host.tsx (5)
apps/desktop/src/routes/__root.tsx (1)
  • Route (21-25)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (14-17)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createParentSynchronizer (113-165)
apps/desktop/src/extension-globals.ts (1)
  • initExtensionGlobals (21-32)
plugins/local-llm/src/ext/plugin.rs (1)
  • store (320-320)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
🔇 Additional comments (5)
apps/desktop/src/routes/__root.tsx (1)

6-6: LGTM! Lazy loading prevents Tauri imports in iframe context.

The lazy loading of MainAppLayout is appropriate given that auth.tsx instantiates a Supabase client at module level using Tauri APIs. This prevents those imports from executing in the iframe context where Tauri is unavailable.

Also applies to: 13-15

apps/desktop/src/routes/app/ext-host.tsx (2)

10-16: LGTM! Route configuration is straightforward.

The route definition and search parameter validation are clear and appropriate for passing extension metadata to the iframe host.


77-79: Preserving and restoring __hypr_panel_exports is good practice.

The code correctly saves any previous __hypr_panel_exports value before script injection and restores it afterward. This prevents collisions if multiple extensions are loaded sequentially.

Also applies to: 96-98

apps/desktop/src/components/main/body/extensions/index.tsx (2)

111-114: Based on my thorough verification, I can now provide the rewritten review comment:

Store is correctly typed as MergeableStore—no runtime risk from this cast.

The Store type exported from apps/desktop/src/store/tinybase/main.ts (line 73) is defined as MergeableStore<Schemas>. The cast to unparameterized MergeableStore is necessary due to a TypeScript generic variance limitation: createIframeSynchronizer in iframe-sync.ts (line 42) accepts the unparameterized type from TinyBase's import. While the double cast bypasses type checking, it doesn't indicate an actual type mismatch—the store is a MergeableStore at runtime. The synchronizer will function correctly.

Likely an incorrect or invalid review comment.


148-152: Based on my verification, I've confirmed that convertFileSrc is indeed idempotent according to Tauri's official documentation—it's a pure converter function that returns the same URL given the same file path input. Additionally, the code flow shows that entry_path always originates from the plugin backend's metadata (via @hypr/plugin-extensions PanelInfo) as a file system path, and scriptUrl is generated fresh each time without being persisted back to storage. There is no evidence of pre-converted URLs being stored or reused.

No issue exists. The concern about double-conversion is not applicable to this codebase.

The code is safe because:

  • convertFileSrc is idempotent by design
  • entry_path is always a file system path from plugin metadata
  • The converted URL is only passed as a parameter, never persisted back to storage

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/routes/app/ext-host.tsx (1)

61-106: Add content-type validation before script execution.

The script is fetched and executed without verifying the response content-type. As noted in a prior review, adding a content-type check provides an additional safeguard against executing unexpected payloads.

       const response = await fetch(scriptUrl);
       if (!response.ok) {
         throw new Error(`Failed to fetch extension script: ${response.status}`);
       }

+      const contentType = response.headers.get("content-type");
+      if (!contentType?.includes("javascript") && !contentType?.includes("text/plain")) {
+        throw new Error(`Invalid content-type for extension script: ${contentType}`);
+      }
+
       const scriptContent = await response.text();
🧹 Nitpick comments (2)
apps/desktop/src/store/tinybase/schema-internal.ts (1)

85-88: Consider schema extensibility for future extensions.

The extension_state table schema is currently hardcoded with counter and last_updated fields specific to the hello-world demo. If other extensions need different state shapes, they would require schema modifications.

Consider whether a more flexible approach (e.g., a generic data JSON string field per extension) would better support diverse extension state needs, or if each extension is expected to have its own dedicated table entry in the schema.

apps/desktop/src/routes/app/ext-host.tsx (1)

61-106: Move loadExtensionScript inside the useEffect or wrap with useCallback.

loadExtensionScript is defined as a function inside the component body but is only called from within the useEffect. Since it references scriptUrl and state setters, this creates a stale closure risk if the effect re-runs. Either move the function inside the useEffect callback or wrap it with useCallback and add it to the dependency array.

   useEffect(() => {
     isMountedRef.current = true;
     initExtensionGlobals();

     const store = createMergeableStore();
     storeRef.current = store;

     const synchronizer = createParentSynchronizer(store);
     synchronizerRef.current = synchronizer;

+    const loadExtensionScript = async () => {
+      if (!scriptUrl) {
+        setError("No script URL provided");
+        return;
+      }
+
+      try {
+        const response = await fetch(scriptUrl);
+        if (!response.ok) {
+          throw new Error(`Failed to fetch extension script: ${response.status}`);
+        }
+
+        const scriptContent = await response.text();
+
+        const previousExports = (
+          window as Window & { __hypr_panel_exports?: unknown }
+        ).__hypr_panel_exports;
+
+        const script = document.createElement("script");
+        script.textContent = scriptContent;
+        document.head.appendChild(script);
+        document.head.removeChild(script);
+
+        const exports = (
+          window as Window & {
+            __hypr_panel_exports?: {
+              default?: ComponentType<ExtensionViewProps>;
+            };
+          }
+        ).__hypr_panel_exports;
+
+        if (exports?.default) {
+          setComponent(() => exports.default!);
+          (
+            window as Window & { __hypr_panel_exports?: unknown }
+          ).__hypr_panel_exports = previousExports;
+        } else {
+          setError("Extension did not export a default component");
+        }
+      } catch (err) {
+        setError(
+          `Failed to load extension: ${err instanceof Error ? err.message : String(err)}`,
+        );
+      }
+    };
+
     synchronizer
       .startSync()
       .then(() => {
         if (isMountedRef.current) {
           loadExtensionScript();
         }
       })
       // ... rest of the effect
-  }, []);
+  }, [scriptUrl]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab69eb and 38b07e3.

📒 Files selected for processing (6)
  • apps/desktop/src/main.tsx (1 hunks)
  • apps/desktop/src/routes/app/devtool.tsx (3 hunks)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
  • apps/desktop/src/routes/app/route.tsx (2 hunks)
  • apps/desktop/src/store/tinybase/main.ts (2 hunks)
  • apps/desktop/src/store/tinybase/schema-internal.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • apps/desktop/src/store/tinybase/main.ts
  • apps/desktop/src/store/tinybase/schema-internal.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/store/tinybase/main.ts
  • apps/desktop/src/routes/app/route.tsx
  • apps/desktop/src/store/tinybase/schema-internal.ts
  • apps/desktop/src/routes/app/devtool.tsx
  • apps/desktop/src/routes/app/ext-host.tsx
  • apps/desktop/src/main.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/app/ext-host.tsx (6)
apps/desktop/src/routes/app/devtool.tsx (1)
  • Route (18-20)
apps/desktop/src/routes/app/route.tsx (1)
  • Route (8-13)
apps/desktop/src/routes/__root.tsx (1)
  • Route (21-25)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (14-17)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createParentSynchronizer (113-165)
apps/desktop/src/extension-globals.ts (1)
  • initExtensionGlobals (21-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (macos, depot-macos-14)
  • GitHub Check: fmt
🔇 Additional comments (7)
apps/desktop/src/routes/app/devtool.tsx (1)

126-157: LGTM!

The ExtensionStateMonitor component correctly uses useRow to read extension state from the persisted store. The type assertion is appropriate given TinyBase's dynamic row shape. The fallback handling for counter (defaulting to 0) and conditional rendering of lastUpdated are well implemented.

apps/desktop/src/routes/app/route.tsx (1)

15-26: LGTM!

The conditional rendering correctly bypasses ListenerProvider and SideEffects for the ext-host route, which is appropriate since iframes should not run main-window side effects. Using startsWith for the pathname check is suitable here.

apps/desktop/src/main.tsx (3)

69-71: LGTM!

Standard iframe context detection pattern. The window.self !== window.top check reliably identifies when running inside an iframe.


78-82: LGTM!

The logic correctly ensures iframes don't persist the store (parent handles persistence) and don't claim to be the main window.


97-99: Based on my verification, I can now provide the final assessment:

No issues found - plugin initialization is safe in iframe context, but for different reasons than stated.

The review comment incorrectly assumes initWindowsPlugin() relies on the Tauri polyfill. However, examining the actual implementation in plugins/windows/js/index.ts, the init() function only sets up DOM event listeners for drag-and-drop functionality—it does NOT call any Tauri APIs. Similarly, initExtensionGlobals() only assigns React and utility libraries to window globals.

The polyfill in index.html is actually needed for other parts of the codebase (like the getCurrentWebviewWindowLabel() call on line 82), which is properly guarded to only execute outside iframe context. Both unconditional initialization calls on lines 98-99 are genuinely safe in iframe context regardless of the polyfill.

apps/desktop/src/routes/app/ext-host.tsx (1)

29-59: LGTM on mount guard implementation.

The isMountedRef pattern correctly prevents state updates after unmount, and error handling for startSync() is properly implemented. This addresses the concerns from previous reviews.

apps/desktop/src/store/tinybase/main.ts (1)

159-199: Persist-guarded creation of localPersister2 and [persist] deps look correct

The early if (!persist) return undefined; plus the [persist] dependency array align localPersister2’s lifecycle with the persist flag and avoid unnecessary FS work when persistence is disabled. This is consistent with how the main localPersister is handled and should behave well even if persist changes at runtime, especially given the existing useEffect guard on !persist || !localPersister || !localPersister2.

If you expect persist to toggle dynamically in practice (not just per-context), it’s worth manually testing that transition to confirm persisters are created/destroyed and window listeners registered/unregistered as intended.

- Add explicit type checking for extensionId and scriptUrl in validateSearch
- Validate scriptUrl is a well-formed URL, reject javascript: protocol
- Return undefined for invalid inputs instead of silent casts
- Add content-type validation before script injection
- Add early return for missing extensionId in component
- Add security documentation to extensions.mdx

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@argos-ci
Copy link

argos-ci bot commented Nov 28, 2025

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
web (Inspect) ⚠️ Changes detected (Review) 3 changed Nov 28, 2025, 3:36 AM

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/desktop/src/routes/app/ext-host.tsx (1)

61-91: Sync lifecycle properly guarded—addresses previous review.

The useEffect correctly implements the mount guard and error handling requested in the previous review. The isMountedRef prevents post-unmount state updates, and the .catch() handler surfaces sync failures gracefully.

apps/web/content/docs/developers/8.extensions.mdx (1)

219-236: Security documentation addresses previous review feedback.

The new Security Model section documents the trust boundaries and validation mechanisms requested in the previous review. The documentation accurately describes:

  • How convertFileSrc restricts file access
  • URL and content-type validation steps
  • Export contract validation
  • Iframe sandbox restrictions

Verify that the iframe implementation actually uses sandbox="allow-scripts" as documented:

#!/bin/bash
# Description: Verify iframe sandbox attribute matches documentation
# Expected: Find iframe with sandbox="allow-scripts" attribute

rg -n '<iframe' apps/desktop/src --type tsx --type ts -A 5 | rg -i 'sandbox'
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38b07e3 and ce04697.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
  • apps/web/content/docs/developers/8.extensions.mdx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/routes/app/ext-host.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/app/ext-host.tsx (3)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (14-17)
apps/desktop/src/store/tinybase/iframe-sync.ts (1)
  • createParentSynchronizer (113-165)
apps/desktop/src/extension-globals.ts (1)
  • initExtensionGlobals (21-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: fmt
  • GitHub Check: ci (macos, macos-14)
  • GitHub Check: ci (macos, depot-macos-14)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
🔇 Additional comments (1)
apps/desktop/src/routes/app/ext-host.tsx (1)

27-48: Validation improvements address previous review feedback.

The validateSearch now properly validates types and URL format, rejecting javascript: URLs. While the previous review suggested throwing errors for invalid params, returning partial results is a valid alternative approach—the component safely handles missing fields with appropriate error states (lines 94-97, 147-156).

… unmount

- Wrap setError and setComponent calls in loadExtensionScript with isMountedRef checks
- Only restore previousExports when component is still mounted
- Prevents React state update warnings on unmounted components

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/desktop/src/routes/app/ext-host.tsx (1)

15-45: Clarify script URL trust model and optionally tighten allowed protocols

isValidUrl currently accepts any syntactically valid URL except javascript:, so scriptUrl could point at arbitrary HTTP/data/blob/file URLs if someone manually navigates to this route. Given this host is intended to run extension bundles from trusted locations (e.g., convertFileSrc output), you may want to (a) document that assumption here and (b) optionally narrow which protocols you accept.

One way (leaving the exact allow‑list for you to tune) is:

-function isValidUrl(url: string): boolean {
-  try {
-    const parsed = new URL(url);
-    if (parsed.protocol === "javascript:") {
-      return false;
-    }
-    return true;
-  } catch {
-    return false;
-  }
-}
+// Extension scripts are expected to be served from trusted locations
+// (for example, Tauri convertFileSrc output for installed extensions).
+// This helper intentionally rejects obviously unsafe schemes and can
+// be tightened further if needed.
+function isValidUrl(url: string): boolean {
+  try {
+    const parsed = new URL(url);
+    if (parsed.protocol === "javascript:") {
+      return false;
+    }
+    // Optionally narrow to an explicit allow‑list that matches your setup,
+    // e.g. asset:/http(s) only:
+    // const allowed = new Set(["asset:", "https:", "http:"]);
+    // if (!allowed.has(parsed.protocol)) return false;
+    return true;
+  } catch {
+    return false;
+  }
+}

This keeps current behavior but makes the trust model explicit and leaves a clear place to harden further if desired. This overlaps with the earlier security‑assumptions comment.

🧹 Nitpick comments (2)
apps/desktop/src/routes/app/ext-host.tsx (2)

93-101: Avoid loading scripts when extensionId is missing to keep invalid routes side‑effect‑free

If the route is hit with a valid scriptUrl but missing/invalid extensionId, ExtHostComponent will still create the store, start sync, and call loadExtensionScript; only the render path short‑circuits on !extensionId. For malformed URLs or manual navigation, it’s slightly safer and cheaper to skip the loader entirely when extensionId is falsy.

You can guard this in loadExtensionScript without touching hook dependencies:

-  const loadExtensionScript = async () => {
-    if (!scriptUrl) {
+  const loadExtensionScript = async () => {
+    if (!extensionId) {
+      // Render-level guard will already show a clearer message.
+      return;
+    }
+
+    if (!scriptUrl) {
       if (isMountedRef.current) {
         setError("No script URL provided");
       }
       return;
     }

This keeps behavior for valid routes the same while avoiding unnecessary sync and network work when the extension ID itself is invalid.

Also applies to: 155-164


115-145: Always restore __hypr_panel_exports so globals don’t leak across loads or after unmount

Right now you restore __hypr_panel_exports only in the success path and only when isMountedRef.current is true. If the script sets __hypr_panel_exports but:

  • Fails the “default export” contract, or
  • Resolves after unmount,

then the global may remain overwritten until the next host load.

You can decouple global cleanup from React mount state and run it unconditionally after inspecting exports:

-      const previousExports = (
-        window as Window & { __hypr_panel_exports?: unknown }
-      ).__hypr_panel_exports;
+      const windowWithExports = window as Window & {
+        __hypr_panel_exports?: {
+          default?: ComponentType<ExtensionViewProps>;
+        };
+      };
+      const previousExports = windowWithExports.__hypr_panel_exports;
@@
-      const script = document.createElement("script");
+      const script = document.createElement("script");
       script.textContent = scriptContent;
       document.head.appendChild(script);
       document.head.removeChild(script);
 
-      const exports = (
-        window as Window & {
-          __hypr_panel_exports?: {
-            default?: ComponentType<ExtensionViewProps>;
-          };
-        }
-      ).__hypr_panel_exports;
-
-      if (exports?.default) {
-        if (isMountedRef.current) {
-          setComponent(() => exports.default!);
-          (
-            window as Window & { __hypr_panel_exports?: unknown }
-          ).__hypr_panel_exports = previousExports;
-        }
-      } else {
-        if (isMountedRef.current) {
-          setError("Extension did not export a default component");
-        }
-      }
+      const exports = windowWithExports.__hypr_panel_exports;
+
+      if (exports?.default) {
+        if (isMountedRef.current) {
+          setComponent(() => exports.default);
+        }
+      } else if (isMountedRef.current) {
+        setError("Extension did not export a default component");
+      }
+
+      // Always restore previous exports so global state doesn't leak between loads.
+      windowWithExports.__hypr_panel_exports = previousExports;

This keeps the React behavior identical while ensuring the global helper surface is always returned to its prior value.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce04697 and 0cdf9ba.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/app/ext-host.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/desktop/src/routes/app/ext-host.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: ci (macos, macos-14)
  • GitHub Check: ci (macos, depot-macos-14)
  • GitHub Check: ci (linux, depot-ubuntu-22.04-8)
  • GitHub Check: ci (linux, depot-ubuntu-24.04-8)
  • GitHub Check: fmt
🔇 Additional comments (2)
apps/desktop/src/routes/app/ext-host.tsx (2)

61-91: startSync lifecycle and unmount behavior now look robust

The useEffect block now correctly:

  • Tracks mount state with isMountedRef.
  • Wraps startSync() in a .then/.catch, guarding loadExtensionScript() and setError with isMountedRef.current.
  • Always calls synchronizer.destroy() in the cleanup.

This addresses the earlier unhandled‑rejection and post‑unmount setState concerns for the TinyBase sync bootstrap. No changes needed here.


166-191: Error/loading states and TinyBase provider wiring look correct

The render logic cleanly separates:

  • Missing/invalid extensionId → dedicated error message.
  • Generic loader failures → error branch.
  • Initial “no component or store yet” → loading message.
  • Happy path → TinyBaseProvider with a non‑null store and the extension component receiving extensionId.

This keeps the UX straightforward and ensures the TinyBase store is always present when you render the extension. No changes needed.

@yujonglee yujonglee merged commit 545bd9c into main Nov 28, 2025
16 of 17 checks passed
@yujonglee yujonglee deleted the devin/1764287392-extension-iframe-isolation branch November 28, 2025 03:45
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