Skip to content

feat: add Deno-powered extensions runtime (Phase 1)#1938

Merged
yujonglee merged 5 commits intomainfrom
devin/1764213842-deno-extensions-runtime
Nov 27, 2025
Merged

feat: add Deno-powered extensions runtime (Phase 1)#1938
yujonglee merged 5 commits intomainfrom
devin/1764213842-deno-extensions-runtime

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Nov 27, 2025

Summary

This PR adds the foundation for a Deno-powered plugin/extension system for Hyprnote. It introduces:

  1. crates/extensions-runtime - A Rust crate wrapping deno_core (v0.338) that provides JavaScript execution capabilities. Uses a channel-based async architecture with the V8 runtime running in a dedicated thread.

  2. plugins/extensions - A Tauri plugin exposing extension lifecycle management to the frontend via commands: load_extension, call_function, execute_code, list_extensions.

  3. extensions/hello-world - A minimal example extension with extension.json manifest, main.js entry point for Deno runtime, and ui.tsx React component using @hypr/ui.

  4. Frontend schema update - Added extension tab type to the tab schema in apps/desktop/src/store/zustand/tabs/schema.ts.

  5. Extension tab rendering - Added TabItemExtension and TabContentExtension components with a registry that uses import.meta.glob to discover extension UI components at build time.

The runtime wraps extension code in an IIFE to capture exports and uses v8 Global handles for function references across async boundaries. Extensions have access to a hypr.log() API for logging.

Updates since last revision

Linux build fix:

  • Removed cdylib from crate-type in apps/desktop/src-tauri/Cargo.toml to fix V8 TLS linker error on Linux
  • V8 (used by deno_core) is compiled with a TLS model incompatible with shared libraries on Linux
  • Since mobile support is not needed (macOS/Linux/Windows only), removing cdylib fixes the build without losing functionality

Code review fixes:

  • Fixed serde_v8::to_v8 error handling (no longer panics on conversion failure)
  • Removed per-call Box::leak in execute_code_impl by using static literal script name
  • Added comment explaining why Box::leak is necessary in load_extension_impl (deno_core requires 'static lifetime)
  • Implemented Clone for ExtensionsRuntime to fix mutex held across await points
  • Added path validation in Extension::load to prevent directory escape attacks
  • Map JSON decode errors to InvalidManifest for better error distinction
  • Added doc comment noting permissions are not enforced yet
  • Removed redundant TypeScript declare module block

Review & Testing Checklist for Human

  • Critical: Test the extension system end-to-end - The code compiles and type-checks but was not tested with a running app. Load the hello-world extension and verify both the Deno runtime functions and React UI render correctly.
  • Verify crate-type change doesn't break builds - Confirm tauri dev and tauri build work correctly on macOS, Linux, and Windows after removing cdylib.
  • Test path validation - The canonicalize-based path validation will fail if the entry file doesn't exist. Verify this doesn't break valid extension loading.
  • Security review - Extensions can execute arbitrary JavaScript. The permissions system is defined but not enforced yet.

Test Plan

  1. Start the app with ONBOARDING=0 pnpm -F desktop tauri dev
  2. Use the Tauri commands to load the hello-world extension from extensions/hello-world/
  3. Call greet("World") and verify it returns "Hello, World!"
  4. Check console for hypr.log output
  5. Open an extension tab for hello-world and verify the React UI renders

Notes

  • This is Phase 1 of the extension system - focused on embedding the runtime and validating the architecture
  • list_extensions currently returns an empty vec (placeholder)
  • Extension UI discovery uses import.meta.glob which is build-time only (extensions must be present at build time)
  • The Deno runtime (main.js) and React UI (ui.tsx) are currently separate - no bridge between them yet
  • Using deno_core 0.338 for compatibility (not latest)
  • Box::leak is still used once per extension load (bounded leak) because deno_core's execute_script requires a 'static script name

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

- Add extensions-runtime crate wrapping deno_core for JS/TS execution
- Add tauri-plugin-extensions for extension lifecycle management
- Add hello-world example extension to validate architecture
- Add extension tab type to frontend schema

The extensions runtime uses a channel-based async architecture with:
- Custom Deno ops for Hyprnote APIs (hypr.log)
- IIFE wrapping for extension code to capture exports
- v8 Global handles for function references across async boundaries

This is Phase 1 of the Deno-powered plugin system.

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 4106859
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6927f006448cee000827daed
😎 Deploy Preview https://deploy-preview-1938--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.

@netlify
Copy link

netlify bot commented Nov 27, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit 4106859
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6927f006b38227000840f75b
😎 Deploy Preview https://deploy-preview-1938--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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

📝 Walkthrough

Walkthrough

Adds an extensions system: workspace dependencies, a new Rust extensions-runtime crate (deno_core-based), a Tauri plugin exposing extension commands, desktop UI support for extension tabs and a UI registry, and a Hello World JS extension with manifest and UI.

Changes

Cohort / File(s) Summary
Workspace configuration
Cargo.toml
Added workspace dependencies: hypr-extensions-runtime and tauri-plugin-extensions.
Desktop tab schema
apps/desktop/src/store/zustand/tabs/schema.ts
Added type: "extension" tab variant with extensionId and optional state; updated rowIdfromTab and uniqueIdfromTab handling.
Desktop UI: extensions components & registry
apps/desktop/src/components/main/body/extensions/index.tsx, apps/desktop/src/components/main/body/extensions/registry.ts, apps/desktop/src/components/main/body/index.tsx, apps/desktop/src/types/extensions.d.ts, apps/desktop/tsconfig.json, apps/desktop/vite.config.ts
New TabItemExtension/TabContentExtension components; extension UI registry using Vite glob imports; integration into main tab rendering; path aliases and type declarations for extension UI modules.
Extensions workspace (JS)
extensions/hello-world/extension.json, extensions/hello-world/main.js, extensions/hello-world/ui.tsx, extensions/package.json, extensions/tsconfig.json
New example extension manifest and JS entry exposing greet, add, getInfo; UI view component and package/tsconfig for extensions monorepo.
Extensions runtime crate
crates/extensions-runtime/Cargo.toml, crates/extensions-runtime/src/lib.rs, crates/extensions-runtime/src/error.rs, crates/extensions-runtime/src/manifest.rs, crates/extensions-runtime/src/ops.rs, crates/extensions-runtime/src/runtime.rs
New crate providing manifest types and loader, centralized Error + Result alias, a deno_core op (op_hypr_log), and ExtensionsRuntime with async load/call/execute/shutdown via a dedicated thread and channel-based runtime loop.
Tauri plugin (Rust)
plugins/extensions/Cargo.toml, plugins/extensions/build.rs, plugins/extensions/src/lib.rs, plugins/extensions/src/commands.rs, plugins/extensions/src/error.rs, plugins/extensions/src/ext.rs
New Tauri plugin exposing commands (load_extension, call_function, execute_code, list_extensions), plugin State/ManagedState, error mapping from runtime crate, command build registration, and ExtensionsPluginExt trait + impl integrating hypr_extensions_runtime.
Desktop tauri integration
apps/desktop/src-tauri/Cargo.toml, apps/desktop/src-tauri/src/lib.rs
Added tauri-plugin-extensions dependency and registered the plugin in the Tauri plugin chain.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant UI as Desktop UI
    participant Plugin as Tauri Plugin
    participant Host as ExtensionsRuntime (controller)
    participant JS as Deno/JS Runtime

    UI->>Plugin: load_extension(path)
    Plugin->>Host: LoadExtension { extension, responder }
    activate Host
    Host->>Host: read extension.json & entry file
    Host->>JS: execute extension entry (register exports)
    Host->>Host: discover exported functions
    deactivate Host
    Plugin-->>UI: success

    UI->>Plugin: call_function(ext_id, fn, args)
    Plugin->>Host: CallFunction { ext_id, fn, args, responder }
    activate Host
    Host->>JS: invoke JS function with converted args
    Host->>Host: convert result to JSON Value
    deactivate Host
    Plugin-->>UI: JSON result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • crates/extensions-runtime/src/runtime.rs — thread/Tokio setup, deno_core integration, JS↔Rust value conversions, error propagation.
    • Cross-crate error mapping in plugins/extensions/src/error.rs.
    • Plugin state and concurrency (plugins/extensions/src/lib.rs, ManagedState locking).
    • Desktop integration points: tab schema changes and Vite import-based registry (apps/desktop/.../registry.ts).

Possibly related PRs

Suggested reviewers

  • yujonglee

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% 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: add Deno-powered extensions runtime (Phase 1)' accurately and concisely describes the main change: introducing a Deno-powered extension system as a foundation feature.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the new crates, plugins, frontend changes, and recent fixes. It provides context, test plans, and notes on the implementation.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1764213842-deno-extensions-runtime

📜 Recent 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 0f523d0 and 4106859.

📒 Files selected for processing (1)
  • apps/desktop/src-tauri/Cargo.toml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src-tauri/Cargo.toml
⏰ 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)

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

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: 8

🧹 Nitpick comments (4)
crates/extensions-runtime/src/ops.rs (1)

5-8: Consider returning () or a Result instead of allocating "ok" on every call.

Allocating a new String on every log call is unnecessary. If the caller doesn't need a return value, use -> (). If error propagation is desired, use Result<(), Error>.

Apply this diff to return () instead:

-pub fn op_hypr_log(#[string] message: String) -> String {
+pub fn op_hypr_log(#[string] message: String) {
     tracing::info!(target: "extension", "{}", message);
-    "ok".to_string()
 }
plugins/extensions/src/commands.rs (1)

36-42: Stub implementation returns empty list.

list_extensions always returns an empty vector. Consider implementing this by querying the runtime state, or add a TODO comment to track this.

 #[tauri::command]
 #[specta::specta]
 pub async fn list_extensions<R: tauri::Runtime>(
-    _app: tauri::AppHandle<R>,
+    app: tauri::AppHandle<R>,
 ) -> Result<Vec<String>, Error> {
-    Ok(vec![])
+    // TODO: Implement by querying loaded extensions from runtime state
+    app.list_extensions().await
 }
crates/extensions-runtime/src/runtime.rs (2)

32-50: Consider implementing Drop for graceful shutdown.

ExtensionsRuntime spawns a background thread but doesn't implement Drop. If the runtime is dropped without calling shutdown(), the background thread continues running orphaned.

+impl Drop for ExtensionsRuntime {
+    fn drop(&mut self) {
+        // Best-effort shutdown - ignore errors since we're dropping
+        let _ = self.sender.try_send(RuntimeRequest::Shutdown);
+    }
+}

40-47: Handle runtime creation failure gracefully.

Line 44 uses .unwrap() which can panic if the Tokio runtime fails to build (e.g., resource exhaustion). Consider propagating this error or using a fallible constructor.

-impl ExtensionsRuntime {
-    pub fn new() -> Self {
+impl ExtensionsRuntime {
+    pub fn new() -> Result<Self> {
         let (tx, rx) = mpsc::channel(100);

         std::thread::spawn(move || {
             let rt = tokio::runtime::Builder::new_current_thread()
                 .enable_all()
                 .build()
-                .unwrap();
+                .expect("Failed to create tokio runtime for extensions");

             rt.block_on(runtime_loop(rx));
         });

-        Self { sender: tx }
+        Ok(Self { sender: tx })
     }

Note: If this change is adopted, update Default impl and callers accordingly.

📜 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 d0be4ff and 14f5636.

⛔ Files ignored due to path filters (7)
  • Cargo.lock is excluded by !**/*.lock
  • plugins/extensions/permissions/autogenerated/commands/call_function.toml is excluded by !plugins/**/permissions/**
  • plugins/extensions/permissions/autogenerated/commands/execute_code.toml is excluded by !plugins/**/permissions/**
  • plugins/extensions/permissions/autogenerated/commands/list_extensions.toml is excluded by !plugins/**/permissions/**
  • plugins/extensions/permissions/autogenerated/commands/load_extension.toml is excluded by !plugins/**/permissions/**
  • plugins/extensions/permissions/autogenerated/reference.md is excluded by !plugins/**/permissions/**
  • plugins/extensions/permissions/schemas/schema.json is excluded by !plugins/**/permissions/**
📒 Files selected for processing (16)
  • Cargo.toml (2 hunks)
  • apps/desktop/src/store/zustand/tabs/schema.ts (4 hunks)
  • crates/extensions-runtime/Cargo.toml (1 hunks)
  • crates/extensions-runtime/src/error.rs (1 hunks)
  • crates/extensions-runtime/src/lib.rs (1 hunks)
  • crates/extensions-runtime/src/manifest.rs (1 hunks)
  • crates/extensions-runtime/src/ops.rs (1 hunks)
  • crates/extensions-runtime/src/runtime.rs (1 hunks)
  • extensions/hello-world/extension.json (1 hunks)
  • extensions/hello-world/main.js (1 hunks)
  • plugins/extensions/Cargo.toml (1 hunks)
  • plugins/extensions/build.rs (1 hunks)
  • plugins/extensions/src/commands.rs (1 hunks)
  • plugins/extensions/src/error.rs (1 hunks)
  • plugins/extensions/src/ext.rs (1 hunks)
  • plugins/extensions/src/lib.rs (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/zustand/tabs/schema.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/zustand/tabs/schema.ts
🧬 Code graph analysis (6)
plugins/extensions/build.rs (1)
crates/extensions-runtime/src/runtime.rs (1)
  • new (37-50)
crates/extensions-runtime/src/error.rs (1)
plugins/extensions/src/error.rs (1)
  • from (16-30)
plugins/extensions/src/commands.rs (3)
crates/extensions-runtime/src/runtime.rs (3)
  • load_extension (52-63)
  • call_function (65-83)
  • execute_code (85-97)
plugins/extensions/src/ext.rs (6)
  • load_extension (8-11)
  • load_extension (28-36)
  • call_function (13-18)
  • call_function (38-56)
  • execute_code (20-24)
  • execute_code (58-69)
plugins/extensions/src/error.rs (1)
  • from (16-30)
plugins/extensions/src/lib.rs (3)
crates/extensions-runtime/src/runtime.rs (5)
  • new (37-50)
  • load_extension (52-63)
  • call_function (65-83)
  • execute_code (85-97)
  • default (109-111)
plugins/extensions/src/commands.rs (4)
  • load_extension (7-12)
  • call_function (16-24)
  • execute_code (28-34)
  • list_extensions (38-42)
plugins/extensions/src/ext.rs (9)
  • load_extension (8-11)
  • load_extension (28-36)
  • call_function (13-18)
  • call_function (38-56)
  • execute_code (20-24)
  • execute_code (58-69)
  • state (29-29)
  • state (44-44)
  • state (63-63)
plugins/extensions/src/ext.rs (3)
crates/extensions-runtime/src/runtime.rs (4)
  • load_extension (52-63)
  • call_function (65-83)
  • execute_code (85-97)
  • args (277-282)
plugins/extensions/src/commands.rs (3)
  • load_extension (7-12)
  • call_function (16-24)
  • execute_code (28-34)
crates/extensions-runtime/src/manifest.rs (1)
  • load (32-38)
crates/extensions-runtime/src/runtime.rs (3)
crates/extensions-runtime/src/ops.rs (1)
  • op_hypr_log (5-8)
plugins/extensions/src/ext.rs (6)
  • load_extension (8-11)
  • load_extension (28-36)
  • call_function (13-18)
  • call_function (38-56)
  • execute_code (20-24)
  • execute_code (58-69)
crates/extensions-runtime/src/manifest.rs (1)
  • entry_path (40-42)
🪛 GitHub Actions: .github/workflows/desktop_ci.yaml
apps/desktop/src/store/zustand/tabs/schema.ts

[error] 78-78: TypeScript error TS2554: Expected 2-3 arguments, but got 1.

⏰ 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). (4)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
🔇 Additional comments (14)
extensions/hello-world/main.js (1)

1-18: LGTM! Simple and effective example extension.

The implementation demonstrates the core extension API patterns clearly. For a hello-world example, the lack of input validation and error handling is acceptable.

crates/extensions-runtime/src/error.rs (1)

1-25: LGTM! Error types cover the essential failure modes.

The error variants appropriately capture extension lifecycle issues. The conversion to plugin-local errors (as shown in plugins/extensions/src/error.rs) demonstrates good error boundary design.

extensions/hello-world/extension.json (1)

1-8: LGTM! Clean and minimal extension manifest.

The manifest correctly defines the extension metadata and entry point. Empty permissions are appropriate for this demonstration extension.

plugins/extensions/build.rs (1)

1-10: LGTM! Standard Tauri plugin build configuration.

The build script correctly registers the four extension commands using the standard Tauri plugin builder pattern.

crates/extensions-runtime/Cargo.toml (2)

1-13: LGTM! Dependencies are appropriate for the extensions runtime.

The dependency set correctly includes Deno core, async runtime (tokio), serialization (serde), and logging (tracing). The tokio features align with the runtime loop pattern shown in the relevant code snippet.


7-7: Update deno_core to a current stable version; 0.338 is significantly outdated and falls within the period of disclosed security advisories.

The pinned version 0.338 is 30 minor versions behind the latest (0.368.0 as of November 2025). More critically, multiple security advisories were published for deno_core in 2024:

  • CVE-2024-27934 (March 2024): Use-after-free vulnerability
  • RUSTSEC-2024-0403 / RUSTSEC-2024-0405 (mid–late 2024): Denial-of-service issues via op_panic sandbox exposure

Given the timing and version lag, 0.338 likely falls within affected ranges. Upgrade to 0.368 or the latest stable version to incorporate security fixes and performance improvements.

Cargo.toml (1)

43-43: LGTM! Workspace dependencies correctly added.

The new workspace members for the extensions runtime and plugin follow the established patterns and correctly reference their paths.

Also applies to: 121-121

crates/extensions-runtime/src/lib.rs (1)

1-8: LGTM!

Clean module structure with appropriate public re-exports. The ops module is correctly kept internal since it contains Deno operation implementations.

plugins/extensions/src/lib.rs (1)

1-45: LGTM!

The plugin initialization is well-structured with proper state management using Arc<Mutex<State>>. The tauri::Wry hardcoding in collect_commands! is a known limitation of the specta/tauri integration for type generation.

plugins/extensions/src/error.rs (1)

1-31: LGTM!

Comprehensive error handling with proper conversion from the runtime error type. All error variants are appropriately mapped with descriptive messages for channel errors.

crates/extensions-runtime/src/runtime.rs (2)

119-135: LGTM for runtime loop initialization.

The runtime loop is properly structured with a dedicated JsRuntime and hypr global initialization. The hypr.log bridge to tracing::info! is a good pattern for debugging extensions.


197-200: I'll verify the concerns raised in this review comment about Box::leak memory leaks and the lifetime requirements of deno_core's execute_script method.
<function_calls>

deno_core JsRuntime execute_script lifetime requirements 'static

</function_calls>

Let me also search the codebase to understand how execute_script is being used and check the deno_core version being used:
<function_calls>


#!/bin/bash

Find and check Cargo.toml for deno_core version

find . -name "Cargo.toml" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && grep -A2 "deno_core" {}'


</function_calls>

<function_calls>


#!/bin/bash

Check the actual execute_script usage in the runtime.rs file

cat -n crates/extensions-runtime/src/runtime.rs | sed -n '190,210p'


</function_calls>

<function_calls>


#!/bin/bash

Search for all uses of execute_script in the codebase to understand patterns

rg -n "execute_script" --type rust -B2 -A2


</function_calls>

plugins/extensions/Cargo.toml (1)

19-19: I'll help you verify whether the test feature on tauri is needed in the runtime dependencies. Let me start by examining the Cargo.toml file context and checking patterns across other plugins.
<function_calls>

#!/bin/bash

First, let's see the context around line 19 in plugins/extensions/Cargo.toml

echo "=== Context around line 19 in plugins/extensions/Cargo.toml ==="
cat -n plugins/extensions/Cargo.toml | head -30

echo ""
echo "=== Checking tauri dependency patterns in other plugins ==="
rg -n 'tauri\s*=.features' plugins//Cargo.toml


</function_calls>

crates/extensions-runtime/src/manifest.rs (1)

15-23: Permissions are defined but not enforced in the runtime.

ExtensionPermissions defines db, network, and filesystem permission vectors, but these are never validated during extension loading or execution. The ExtensionsRuntime accepts extensions without checking these permissions, and there is no enforcement logic in the codebase. Consider implementing permission validation in load_extension() or call_function() methods, or document this as a planned feature.

- Add hello-world extension UI component using @hypr/ui (Card, Button)
- Add extension tab rendering support in Body component (TabItemExtension, TabContentExtension)
- Add extension registry with import.meta.glob for dynamic component loading
- Wire extensions plugin into desktop app (lib.rs)
- Add Vite alias @extensions for extensions directory
- Add TypeScript path alias and type declarations for extensions
- Add extensions package.json and tsconfig.json for proper TypeScript support

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

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

1 similar comment
@devin-ai-integration
Copy link
Contributor

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

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: 2

🧹 Nitpick comments (5)
apps/desktop/src/types/extensions.d.ts (1)

3-6: Consolidate duplicated ExtensionViewProps interface.

This interface is duplicated in extensions/hello-world/ui.tsx (lines 12-15). Consider exporting it from a single shared location (e.g., this declaration file) and importing it in the extension UI files to avoid drift.

apps/desktop/src/components/main/body/extensions/registry.ts (2)

14-18: Add defensive check for missing default export.

If an extension module doesn't have a default export (perhaps due to a developer error), accessing mod.default will be undefined, leading to silent failures when rendering.

 for (const path in extensionModules) {
   const mod = extensionModules[path];
   const parts = path.split("/");
   const extensionId = parts[parts.length - 2];
-  extensionComponents[extensionId] = mod.default;
+  if (mod.default) {
+    extensionComponents[extensionId] = mod.default;
+  } else {
+    console.warn(`Extension "${extensionId}" is missing a default export in ui.tsx`);
+  }
 }

5-7: Consider supporting both .tsx and .ts extensions.

The glob pattern only matches ui.tsx. If an extension author creates ui.ts (no JSX), it won't be discovered.

 const extensionModules = import.meta.glob<{
   default: ComponentType<ExtensionViewProps>;
-}>("@extensions/*/ui.tsx", { eager: true });
+}>("@extensions/*/ui.{ts,tsx}", { eager: true });
extensions/hello-world/ui.tsx (1)

13-16: Consider importing ExtensionViewProps from the shared type declaration.

As noted earlier, this interface is duplicated. Once consolidated, import it here:

-export interface ExtensionViewProps {
-  extensionId: string;
-  state?: Record<string, unknown>;
-}
+import type { ExtensionViewProps } from "@/types/extensions";
+export type { ExtensionViewProps };
apps/desktop/src/components/main/body/extensions/index.tsx (1)

58-62: Use explicit undefined check for tabIndex.

The current truthy check {tabIndex && ...} would hide the index if tabIndex is 0. While the current usage starts indices at 1, an explicit check is more defensive.

-          {tabIndex && (
+          {tabIndex !== undefined && (
             <span className="text-xs text-neutral-400 shrink-0">
               {tabIndex}
             </span>
           )}
📜 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 e1f7e4b and ebe1f07.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • apps/desktop/src-tauri/Cargo.toml (1 hunks)
  • apps/desktop/src-tauri/src/lib.rs (1 hunks)
  • apps/desktop/src/components/main/body/extensions/index.tsx (1 hunks)
  • apps/desktop/src/components/main/body/extensions/registry.ts (1 hunks)
  • apps/desktop/src/components/main/body/index.tsx (3 hunks)
  • apps/desktop/src/types/extensions.d.ts (1 hunks)
  • apps/desktop/tsconfig.json (1 hunks)
  • apps/desktop/vite.config.ts (2 hunks)
  • extensions/hello-world/ui.tsx (1 hunks)
  • extensions/package.json (1 hunks)
  • extensions/tsconfig.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.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/vite.config.ts
  • apps/desktop/src/types/extensions.d.ts
  • apps/desktop/src/components/main/body/extensions/registry.ts
**/*.config.{ts,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Agent configuration should be centralized and externalized from implementation logic

Files:

  • apps/desktop/vite.config.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/vite.config.ts
  • apps/desktop/src/types/extensions.d.ts
  • apps/desktop/src/components/main/body/index.tsx
  • apps/desktop/src/components/main/body/extensions/index.tsx
  • apps/desktop/src/components/main/body/extensions/registry.ts
  • extensions/hello-world/ui.tsx
🧠 Learnings (4)
📚 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: After some amount of TypeScript changes, run `pnpm -r typecheck`.

Applied to files:

  • extensions/tsconfig.json
📚 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:

  • extensions/tsconfig.json
📚 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} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props. Just inline them.

Applied to files:

  • extensions/tsconfig.json
📚 Learning: 2025-11-24T16:32:23.055Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:23.055Z
Learning: Applies to **/*.{ts,tsx} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.

Applied to files:

  • extensions/tsconfig.json
🧬 Code graph analysis (3)
apps/desktop/src/types/extensions.d.ts (1)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (13-16)
apps/desktop/src/components/main/body/extensions/registry.ts (2)
apps/desktop/src/types/extensions.d.ts (2)
  • ExtensionViewProps (3-6)
  • ExtensionViewProps (11-11)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (13-16)
extensions/hello-world/ui.tsx (1)
apps/desktop/src/types/extensions.d.ts (2)
  • ExtensionViewProps (3-6)
  • ExtensionViewProps (11-11)
⏰ 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 (12)
extensions/package.json (2)

1-13: Verify whether React should be a runtime vs. dev-only dependency.

The package configuration looks solid overall. However, clarify the intended usage: if extension authors will write React components that run at runtime, react and @types/react should be in "dependencies" rather than "devDependencies". Currently they're only available to the package's development tooling.

If extensions can use React at runtime, apply this diff:

{
  "name": "@hypr/extensions",
  "version": "0.0.0",
  "private": true,
  "type": "module",
+  "dependencies": {
+    "@hypr/ui": "workspace:^",
+    "react": "^19.2.0"
+  },
  "devDependencies": {
-    "@hypr/ui": "workspace:^",
     "@hypr/utils": "workspace:^",
     "@types/react": "^19.2.7",
-    "react": "^19.2.0",
     "typescript": "~5.6.3"
   }
}

6-12: Workspace dependencies and version pinning look appropriate.

Using "workspace:^" for monorepo packages and pinning TypeScript to ~5.6.3 aligns with managing cross-package consistency. The React 19.2.0 with @types/react ^19.2.7 is compatible.

extensions/tsconfig.json (1)

1-25: LGTM!

The TypeScript configuration is well-structured with appropriate strict mode settings, bundler-friendly module resolution, and correct path aliases for the monorepo's shared packages.

apps/desktop/tsconfig.json (1)

21-27: LGTM!

The path alias configuration correctly maps @extensions/* to the extensions workspace, enabling clean imports of extension modules from the desktop app.

apps/desktop/vite.config.ts (1)

15-23: LGTM!

The alias configuration is cleanly refactored to support both the development-only updater mock and the new @extensions alias. Using path.resolve ensures correct resolution across platforms.

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

27-27: LGTM!

The import follows the established pattern for tab type components.


289-300: LGTM!

The extension tab handling in TabItem correctly follows the established pattern used by other tab types, with consistent prop passing.


327-329: LGTM!

The extension tab handling in ContentWrapper is consistent with other tab type handlers.

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

18-62: LGTM!

The component is a clean, idiomatic React implementation using @hypr/ui components. Good demonstration of the extension UI pattern with local state management.

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

21-88: LGTM!

TabItemExtension is well-implemented with proper drag controls, context menu integration, and consistent styling using cn. The pattern follows the established conventions for tab items.


90-113: LGTM!

TabContentExtension handles both the missing extension case with a helpful fallback UI and the happy path cleanly. Good defensive programming.

apps/desktop/src-tauri/Cargo.toml (1)

34-34: Workspace dependency is properly defined and correctly configured.

All verification checks pass:

  • tauri-plugin-extensions is defined in root Cargo.toml at plugins/extensions/
  • The desktop crate correctly references it with { workspace = true }
  • The extensions plugin depends on hypr-extensions-runtime (workspace reference)
  • hypr-extensions-runtime at crates/extensions-runtime/ specifies deno_core = "0.338" as required
  • Alphabetical ordering is correct (between tauri-plugin-dialog and tauri-plugin-fs)

The dependency chain is complete, versions match the PR objectives, and the configuration follows workspace best practices.

- Remove Box::leak in execute_code_impl by using static literal script name
- Add comment explaining why Box::leak is necessary in load_extension_impl
- Handle serde_v8::to_v8 errors properly instead of unwrap
- Map JSON decode errors to InvalidManifest for better error distinction
- Validate manifest entry path to prevent directory escape attacks
- Implement Clone for ExtensionsRuntime to fix mutex held across await
- Remove redundant TypeScript declare module block (TS doesn't support multi-segment wildcards)
- Add doc comment about permissions not being enforced yet

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: 4

🧹 Nitpick comments (2)
plugins/extensions/src/ext.rs (1)

7-25: Consider using async_trait for cleaner trait definition.

The current approach using impl Future in return position works but is verbose and requires Rust 1.75+. If backwards compatibility is needed or for cleaner syntax, consider async_trait.

+use async_trait::async_trait;
+
+#[async_trait]
 pub trait ExtensionsPluginExt<R: Runtime> {
-    fn load_extension(
+    async fn load_extension(
         &self,
         path: PathBuf,
-    ) -> impl std::future::Future<Output = Result<(), crate::Error>>;
+    ) -> Result<(), crate::Error>;

-    fn call_function(
+    async fn call_function(
         &self,
         extension_id: String,
         function_name: String,
         args_json: String,
-    ) -> impl std::future::Future<Output = Result<String, crate::Error>>;
+    ) -> Result<String, crate::Error>;

-    fn execute_code(
+    async fn execute_code(
         &self,
         extension_id: String,
         code: String,
-    ) -> impl std::future::Future<Output = Result<String, crate::Error>>;
+    ) -> Result<String, crate::Error>;
 }
crates/extensions-runtime/src/runtime.rs (1)

198-205: Box::leak trades memory for lifetime satisfaction.

The comment explains the rationale, but this is a memory leak. For a small number of extensions loaded once, this is acceptable. However, if extensions can be reloaded or this code path is hit repeatedly, memory will grow unbounded.

Consider alternatives:

  1. Store script names in a Vec<Box<str>> on the runtime state (cleaned up on shutdown)
  2. Use a static string interner like string-interner or lasso crate
+// In runtime_loop or a state struct:
+let mut script_names: Vec<Box<str>> = Vec::new();

 // In load_extension_impl:
-let script_name: &'static str = Box::leak(extension.manifest.id.clone().into_boxed_str());
+let boxed_name: Box<str> = extension.manifest.id.clone().into_boxed_str();
+script_names.push(boxed_name);
+let script_name: &str = script_names.last().unwrap();

Note: This requires passing script_names to load_extension_impl or restructuring.

📜 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 ebe1f07 and 0f523d0.

📒 Files selected for processing (4)
  • apps/desktop/src/types/extensions.d.ts (1 hunks)
  • crates/extensions-runtime/src/manifest.rs (1 hunks)
  • crates/extensions-runtime/src/runtime.rs (1 hunks)
  • plugins/extensions/src/ext.rs (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/types/extensions.d.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/types/extensions.d.ts
🧠 Learnings (2)
📚 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} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props. Just inline them.

Applied to files:

  • apps/desktop/src/types/extensions.d.ts
📚 Learning: 2025-11-24T16:32:23.055Z
Learnt from: CR
Repo: fastrepl/hyprnote PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T16:32:23.055Z
Learning: Applies to **/*.{ts,tsx} : Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.

Applied to files:

  • apps/desktop/src/types/extensions.d.ts
🧬 Code graph analysis (3)
apps/desktop/src/types/extensions.d.ts (1)
extensions/hello-world/ui.tsx (1)
  • ExtensionViewProps (13-16)
plugins/extensions/src/ext.rs (3)
crates/extensions-runtime/src/runtime.rs (3)
  • load_extension (53-64)
  • call_function (66-84)
  • execute_code (86-98)
plugins/extensions/src/commands.rs (3)
  • load_extension (7-12)
  • call_function (16-24)
  • execute_code (28-34)
crates/extensions-runtime/src/manifest.rs (1)
  • load (35-51)
crates/extensions-runtime/src/runtime.rs (3)
crates/extensions-runtime/src/ops.rs (1)
  • op_hypr_log (5-8)
plugins/extensions/src/ext.rs (6)
  • load_extension (8-11)
  • load_extension (28-39)
  • call_function (13-18)
  • call_function (41-61)
  • execute_code (20-24)
  • execute_code (63-77)
crates/extensions-runtime/src/manifest.rs (1)
  • entry_path (53-55)
⏰ 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 (9)
apps/desktop/src/types/extensions.d.ts (1)

3-6: Shared ExtensionViewProps contract is clear and consistent

The minimal props shape (extensionId plus optional state) is reasonable for extension UIs and matches the example extension’s definition, giving you a single, predictable contract for the registry and components to share. No issues from a typing or design perspective here.

crates/extensions-runtime/src/manifest.rs (2)

34-51: Path validation correctly prevents directory traversal attacks.

The canonicalization approach properly validates that the entry file stays within the extension directory. However, there's a subtle TOCTOU (time-of-check-to-time-of-use) consideration: the file is validated here but read later in runtime.rs.

For Phase 1 this is acceptable since extensions are trusted, but for production consider:

  1. Reading the file content here and passing it along, or
  2. Re-validating at read time in the runtime

53-55: LGTM!

The entry_path helper is simple and correctly joins the base path with the manifest entry.

plugins/extensions/src/ext.rs (3)

28-39: Lock pattern correctly avoids holding mutex across await.

The pattern of cloning the runtime handle before awaiting properly addresses the concurrency concern from the previous review.


41-61: Consistent lock pattern and proper error handling.

The implementation correctly:

  1. Parses JSON args before acquiring the lock
  2. Clones runtime and releases lock before await
  3. Maps errors appropriately

63-77: LGTM!

Consistent with the other methods. The lock-clone-release pattern is applied correctly.

crates/extensions-runtime/src/runtime.rs (3)

259-297: Function invocation implementation looks correct.

The implementation properly:

  1. Looks up extension state and function by name
  2. Converts args with proper error handling (addressing previous review)
  3. Calls the function and deserializes the result

139-176: Runtime loop handles all request types appropriately.

The message loop correctly dispatches to implementation functions and sends results via oneshot channels. The Shutdown variant cleanly breaks the loop.


246-257: Based on my investigation, I can now provide the rewritten review comment:


execute_code_impl ignores extension context and executes code in global runtime scope.

The function receives extension_id through the public API (line 86), but the runtime handler deliberately discards it with an underscore pattern (line 165: extension_id: _,), then calls execute_code_impl without passing any extension context. In contrast, call_function_impl (line 259) accepts and uses extension_id to isolate calls to a specific extension's state.

This creates asymmetric behavior: all extensions share the same global JavaScript runtime state during code execution, while function calls are properly isolated per extension.

No Phase 1 requirements or design documentation were found in the codebase to clarify if this is intentional. Verify with your team whether global execution scope is the intended design or if extension isolation should be added.

V8 (used by deno_core) is compiled with a TLS model incompatible with
shared libraries on Linux. Since mobile support is not needed (only
macOS/Linux/Windows desktop), removing cdylib from crate-type fixes
the linker error without losing any required functionality.

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@yujonglee yujonglee merged commit 1230a2e into main Nov 27, 2025
13 checks passed
@yujonglee yujonglee deleted the devin/1764213842-deno-extensions-runtime branch November 27, 2025 06:47
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