-
Notifications
You must be signed in to change notification settings - Fork 532
Extensions
Extensions allow you to add custom functionality to Hyprnote through panels that appear in the sidebar. Each extension can define one or more panels with custom React-based UIs that integrate seamlessly with the application.
Extensions consist of two main parts: a runtime script (main.js) that runs in a sandboxed Deno environment, and optional UI panels (ui.tsx) that render React components within the Hyprnote interface.
A typical extension has the following structure:
my-extension/
├── extension.json # Extension manifest
├── main.js # Runtime script (Deno)
├── ui.tsx # Panel UI component (React)
└── dist/
└── ui.js # Built panel UI (generated)
The extension.json manifest defines your extension's metadata and configuration:
{
"id": "my-extension",
"name": "My Extension",
"version": "0.1.0",
"api_version": "0.1",
"description": "A custom extension for Hyprnote",
"entry": "main.js",
"panels": [
{
"id": "my-extension.main",
"title": "My Extension",
"entry": "dist/ui.js"
}
],
"permissions": {}
}-
id: Unique identifier for your extension (lowercase, hyphenated) -
name: Display name shown in the UI -
version: Semantic version of your extension -
api_version: Hyprnote extension API version (currently0.1) -
description: Brief description of what your extension does -
entry: Path to the runtime script -
panels: Array of panel definitions -
permissions: Required permissions (reserved for future use)
Each panel definition includes:
-
id: Unique panel identifier (typicallyextension-id.panel-name) -
title: Display title shown in the panel tab -
entry: Path to the built UI bundle
The runtime script (main.js) runs in a sandboxed Deno environment and handles extension lifecycle events. The runtime provides a hypr.log API for logging:
__hypr_extension.activate = function (context) {
hypr.log.info(`Activating ${context.manifest.name} v${context.manifest.version}`);
hypr.log.info(`Extension path: ${context.extensionPath}`);
};
__hypr_extension.deactivate = function () {
hypr.log.info("Deactivating extension");
};
__hypr_extension.customMethod = function (arg) {
hypr.log.info(`Called with: ${arg}`);
return `Result: ${arg}`;
};The context object passed to activate contains:
-
manifest: The parsed extension manifest -
extensionPath: Absolute path to the extension directory
Panel UIs are React components that render within the Hyprnote interface. Create a ui.tsx file with a default export:
import { useState } from "react";
import { Button } from "@hypr/ui/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@hypr/ui/components/ui/card";
export interface ExtensionViewProps {
extensionId: string;
state?: Record<string, unknown>;
}
export default function MyExtensionView({ extensionId }: ExtensionViewProps) {
const [count, setCount] = useState(0);
return (
<div className="p-4 h-full">
<Card>
<CardHeader>
<CardTitle>My Extension</CardTitle>
</CardHeader>
<CardContent>
<p>Counter: {count}</p>
<Button onClick={() => setCount((c) => c + 1)}>Increment</Button>
</CardContent>
</Card>
</div>
);
}Extension UIs have access to the following globals provided by Hyprnote:
-
react- React library -
react-dom- React DOM library -
@hypr/ui- Hyprnote UI component library (Button, ButtonGroup, Card, Checkbox, Popover) -
@hypr/utils- Hyprnote utility functions (cn, date-fns helpers, etc.)
-
tinybase/ui-react- TinyBase React hooks for synchronized state (useRow, useStore, useSetRowCallback, etc.)
-
@hypr/tabs- Tab management for opening new views
Import these as you would in a normal React application:
import { useState, useEffect } from "react";
import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/utils";
import { useRow, useSetRowCallback, useStore } from "tinybase/ui-react";
import { useTabs } from "@hypr/tabs";Extensions can read state from the main Hyprnote application using TinyBase. The synchronization happens automatically via postMessage between the iframe and parent window.
Use TinyBase hooks to read from the synchronized store:
import { useStore } from "tinybase/ui-react";
function MyExtension({ extensionId }: ExtensionViewProps) {
const store = useStore();
// Read app data (calendars, events, sessions, etc.)
const calendarIds = store?.getRowIds("calendars") ?? [];
return <div>Calendars: {calendarIds.length}</div>;
}Extensions can open new tabs in the main application using the useTabs hook:
import { useTabs } from "@hypr/tabs";
function MyExtension() {
const openNew = useTabs((state) => state.openNew);
const handleOpenSession = (sessionId: string) => {
openNew({ type: "sessions", id: sessionId });
};
const handleOpenCalendar = () => {
openNew({ type: "calendars", month: new Date() });
};
return (
<div>
<Button onClick={() => handleOpenSession("session-123")}>
Open Session
</Button>
<Button onClick={handleOpenCalendar}>Open Calendar</Button>
</div>
);
}Extensions are built using the build script in the extensions/ directory.
# Build all extensions
pnpm -F @hypr/extensions build
# Build a specific extension
pnpm -F @hypr/extensions build:hello-world
# Or using the build script directly
node build.mjs build # Build all
node build.mjs build hello-world # Build specific extension
node build.mjs clean # Remove all dist folders
node build.mjs install # Install to app data directoryThe build process generates:
-
dist/ui.js- Bundled panel UI (IIFE format) -
dist/ui.js.map- Source map for debugging
mkdir extensions/my-extension
cd extensions/my-extensionCreate extension.json with your extension configuration.
Create main.js with lifecycle handlers.
Create ui.tsx with your React component.
pnpm -F @hypr/extensions build my-extensionCopy the extension to the app data directory:
# Using the build script
pnpm -F @hypr/extensions install:dev
# Or manually (macOS)
cp -r extensions/my-extension ~/Library/Application\ Support/com.hyprnote.dev/extensions/
# Linux
cp -r extensions/my-extension ~/.local/share/com.hyprnote.dev/extensions/
# Windows
cp -r extensions/my-extension %APPDATA%/com.hyprnote.dev/extensions/Launch Hyprnote with ONBOARDING=0 pnpm -F desktop tauri dev and click on the profile area (shows "Unknown" by default) to expand the menu, then click your extension name to see the panel.
Extension UIs run in sandboxed iframes with restricted capabilities.
Extension iframes use the sandbox="allow-scripts" attribute, which restricts the iframe from accessing the parent window's DOM, making top-level navigations, or accessing same-origin storage. Extensions communicate with the parent exclusively through the TinyBase postMessage synchronizer.
In iframe contexts, Tauri's __TAURI_INTERNALS__ is polyfilled with a minimal stub that rejects all invoke calls. This prevents extensions from directly calling Tauri commands.
The TinyBase synchronizer uses postMessage with origin validation. Messages are only accepted from the expected origin, and the message format is validated before processing.
The hello-world extension in the repository demonstrates a complete extension with:
- Extension manifest with panel definition
- Runtime script with lifecycle handlers
- React UI with local and synchronized state using TinyBase
- Usage of @hypr/ui components
cd extensions
pnpm install
pnpm build:hello-world
pnpm install:devThe calendar extension demonstrates a more complex extension that:
- Reads calendar and event data from the main app's store
- Uses the
@hypr/tabsAPI to open sessions - Implements a full calendar UI with month navigation
cd extensions
pnpm install
pnpm build:calendar
pnpm install:dev