From b3398a17805a5d1723a98712e1811be4eec6b422 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Tue, 12 Aug 2025 16:38:22 -0400 Subject: [PATCH 01/65] gabi- --- .../ui/src/components/ResetButton.tsx | 13 +- .../app-store/ui/src/pages/StorePage.tsx | 6 +- .../AndroidHomescreen/components/AppIcon.tsx | 24 ++-- .../components/GestureZone.tsx | 21 ++- .../components/HomeScreen.tsx | 133 ++++++++++++++---- .../AndroidHomescreen/components/Modal.tsx | 30 ++++ .../AndroidHomescreen/components/Widget.tsx | 8 +- .../components/AndroidHomescreen/index.tsx | 4 +- .../ui/src/stores/persistenceStore.ts | 4 + .../packages/settings/settings/src/lib.rs | 2 +- hyperdrive/packages/settings/ui/src/App.tsx | 2 +- 11 files changed, 186 insertions(+), 61 deletions(-) create mode 100644 hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx diff --git a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx index 1470ee7fb..0984e256e 100644 --- a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx @@ -3,8 +3,12 @@ import { FaExclamationTriangle } from 'react-icons/fa'; import useAppsStore from '../store'; import { BsArrowClockwise } from 'react-icons/bs'; import { Modal } from './Modal'; +import classNames from 'classnames' -const ResetButton: React.FC = () => { +interface ResetButtonProps { + className?: string +} +const ResetButton: React.FC = ({ className }) => { const resetStore = useAppsStore(state => state.resetStore); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -26,10 +30,11 @@ const ResetButton: React.FC = () => { <> {isOpen && ( diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index addb09043..960e0e090 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -233,9 +233,9 @@ export default function StorePage() { )} -
-

Can't find the app you're looking for?

- +
+

Can't find the app?

+
); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx index 0977996d8..b809e6b0f 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx @@ -6,14 +6,14 @@ import classNames from 'classnames'; interface AppIconProps { app: HomepageApp; isEditMode: boolean; - showLabel?: boolean; + isUndocked?: boolean; isFloating?: boolean; } export const AppIcon: React.FC = ({ app, isEditMode, - showLabel = true, + isUndocked = true, isFloating = false }) => { const { openApp } = useNavigationStore(); @@ -33,7 +33,7 @@ export const AppIcon: React.FC = ({ 'animate-wiggle': isEditMode && isFloating, 'hover:scale-110': !isEditMode && isFloating, 'opacity-50': !app.path && !(app.process && app.publisher) && !app.base64_icon, - 'p-2': showLabel, + 'p-2': isUndocked, })} onMouseDown={() => setIsPressed(true)} onMouseUp={() => setIsPressed(false)} @@ -45,8 +45,8 @@ export const AppIcon: React.FC = ({ data-app-publisher={app.publisher} > -
{app.base64_icon ? ( {app.label} @@ -57,11 +57,13 @@ export const AppIcon: React.FC = ({ )}
- {showLabel && ( - - {app.label} - - )} + + {app.label} + ); -}; \ No newline at end of file +}; diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index 00db68f3b..c14e55a3e 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useNavigationStore } from '../../../stores/navigationStore'; import classNames from 'classnames'; -import { BsChevronLeft, BsClock } from 'react-icons/bs'; +import { BsChevronLeft, BsClock, BsHouse } from 'react-icons/bs'; export const GestureZone: React.FC = () => { - const { toggleRecentApps, runningApps, currentAppId, switchToApp, isRecentAppsOpen } = useNavigationStore(); + const { toggleRecentApps, runningApps, currentAppId, switchToApp, isRecentAppsOpen, closeAllOverlays } = useNavigationStore(); const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null); const [isActive, setIsActive] = useState(false); const [_isHovered, setIsHovered] = useState(false); @@ -66,19 +66,26 @@ export const GestureZone: React.FC = () => {
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {!isActive &&
- - + {!isActive &&
+ +
}
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index ce1b28b51..80e49dedb 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -6,12 +6,30 @@ import { Draggable } from './Draggable'; import { AppIcon } from './AppIcon'; import { Widget } from './Widget'; import type { HomepageApp } from '../../../types/app.types'; -import { BsCheck, BsClock, BsGridFill, BsImage, BsLayers, BsSearch, BsX } from 'react-icons/bs'; +import { BsCheck, BsClock, BsEnvelope, BsGridFill, BsImage, BsLayers, BsPencilSquare, BsSearch, BsX } from 'react-icons/bs'; import classNames from 'classnames'; +import { Modal } from './Modal'; export const HomeScreen: React.FC = () => { const { apps } = useAppStore(); - const { homeScreenApps, dockApps, appPositions, widgetSettings, removeFromHomeScreen, toggleWidget, moveItem, backgroundImage, setBackgroundImage, addToDock, removeFromDock, isInitialized, setIsInitialized, addToHomeScreen } = usePersistenceStore(); + const { + homeScreenApps, + dockApps, + appPositions, + widgetSettings, + removeFromHomeScreen, + toggleWidget, + moveItem, + backgroundImage, + setBackgroundImage, + addToDock, + removeFromDock, + isInitialized, + setIsInitialized, + addToHomeScreen, + doNotShowOnboardingAgain, + setDoNotShowOnboardingAgain, + } = usePersistenceStore(); const { isEditMode, setEditMode } = useAppStore(); const { toggleAppDrawer, toggleRecentApps } = useNavigationStore(); const [draggedAppId, setDraggedAppId] = React.useState(null); @@ -19,6 +37,8 @@ export const HomeScreen: React.FC = () => { const [showBackgroundSettings, setShowBackgroundSettings] = React.useState(false); const [showWidgetSettings, setShowWidgetSettings] = React.useState(false); const [searchQuery, setSearchQuery] = React.useState(''); + const [showOnboarding, setShowOnboarding] = React.useState(!doNotShowOnboardingAgain); + const [showWidgetOnboarding, setShowWidgetOnboarding] = React.useState(!doNotShowOnboardingAgain); console.log({ appPositions }) @@ -312,9 +332,22 @@ export const HomeScreen: React.FC = () => { {widgetApps - .filter(app => !searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) .map((app, index) => ( - + + {showWidgetOnboarding && index === 0 &&
+ This is a widget. Drag it, resize it, or hide it! +
} +
))} @@ -323,7 +356,7 @@ export const HomeScreen: React.FC = () => { onDragOver={handleDockDragOver} onDrop={(e) => handleDockDrop(e, dockAppsList.length)} > -
+
{Array.from({ length: 4 }).map((_, index) => { const app = dockAppsList[index]; @@ -331,7 +364,10 @@ export const HomeScreen: React.FC = () => {
{ e.stopPropagation(); @@ -375,7 +411,7 @@ export const HomeScreen: React.FC = () => {
) : ( @@ -389,15 +425,15 @@ export const HomeScreen: React.FC = () => { onClick={toggleAppDrawer} className="w-16 h-16 !bg-iris !text-neon !rounded-xl text-2xl hover:!bg-neon hover:!text-iris flex-col justify-center !gap-1" > - - Apps + + My apps
@@ -414,7 +450,7 @@ export const HomeScreen: React.FC = () => { a.id === draggedAppId)!} isEditMode={false} - showLabel={false} + isUndocked={false} />
)} @@ -431,24 +467,6 @@ export const HomeScreen: React.FC = () => { alt="Hyperdrive" className="h-8 hidden md:block self-start" /> - {!isEditMode && <> -
- - setSearchQuery(e.target.value)} - value={searchQuery} - /> -
- - } {isEditMode && (
@@ -558,8 +576,63 @@ export const HomeScreen: React.FC = () => {
)} + + {!isEditMode && <> + + + + +
+ + setSearchQuery(e.target.value)} + value={searchQuery} + /> +
+ } + + {showOnboarding && ( + setShowOnboarding(false)} + > +

Welcome to Hyperware

+

Your gateway to the internet, reimagined.

+

Treat your node like your desktop by customizing the interface and pinning your favorite apps.

+

Your node, your data: finally, you have full control over your information.

+
+ + +
+
+ )} + {/*
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx new file mode 100644 index 000000000..02f285e7a --- /dev/null +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx @@ -0,0 +1,30 @@ +import classNames from "classnames"; +import React, { ReactNode } from "react"; +import { BsX } from "react-icons/bs"; + +interface ModalProps { + children: ReactNode; + onClose: () => void; + backdropClassName?: string; + modalClassName?: string; +} + +export const Modal: React.FC = ({ + children, + backdropClassName, + modalClassName, + onClose +}) => { + return ( +
+
+ + {children} +
+
+ ); +}; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx index 99efb5255..239c6292f 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx @@ -9,9 +9,11 @@ interface WidgetProps { app: HomepageApp; index: number; totalWidgets: number; + children?: React.ReactNode; + className?: string; } -export const Widget: React.FC = ({ app, index, totalWidgets }) => { +export const Widget: React.FC = ({ app, index, totalWidgets, children, className }) => { const { toggleWidget, widgetSettings, setWidgetPosition, setWidgetSize } = usePersistenceStore(); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); @@ -129,7 +131,7 @@ export const Widget: React.FC = ({ app, index, totalWidgets }) => { position={position} onMove={(pos) => setWidgetPosition(app.id, pos)} enableHtmlDrag={false} - className="z-20" + className={classNames("z-20", className)} >
-
-
+
+
Loading Hyperware...
diff --git a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts index 358e12292..6f9a4ac6e 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts @@ -20,6 +20,8 @@ interface PersistenceStore { setWidgetSize: (appId: string, size: Size) => void; setBackgroundImage: (imageUrl: string | null) => void; setIsInitialized: (isInitialized: boolean) => void; + doNotShowOnboardingAgain: boolean; + setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain: boolean) => void; } export const usePersistenceStore = create()( @@ -31,6 +33,8 @@ export const usePersistenceStore = create()( appPositions: {}, widgetSettings: {}, backgroundImage: null, + doNotShowOnboardingAgain: false, + setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain) => set({ doNotShowOnboardingAgain }), setIsInitialized: (isInitialized) => set({ isInitialized }), diff --git a/hyperdrive/packages/settings/settings/src/lib.rs b/hyperdrive/packages/settings/settings/src/lib.rs index 00451d69b..1e4328380 100644 --- a/hyperdrive/packages/settings/settings/src/lib.rs +++ b/hyperdrive/packages/settings/settings/src/lib.rs @@ -243,7 +243,7 @@ fn initialize(our: Address) { while let Err(e) = state.fetch() { println!("failed to fetch settings: {e}, trying again in 5s..."); homepage::add_to_homepage( - "Settings", + "Node settings", Some(ICON), Some("/"), Some(&make_widget(&state)), diff --git a/hyperdrive/packages/settings/ui/src/App.tsx b/hyperdrive/packages/settings/ui/src/App.tsx index 5df023fde..3408cc46f 100644 --- a/hyperdrive/packages/settings/ui/src/App.tsx +++ b/hyperdrive/packages/settings/ui/src/App.tsx @@ -209,7 +209,7 @@ function App() {
-

System diagnostics and settings

+

Node settings and system diagnostics

Date: Tue, 12 Aug 2025 16:54:25 -0400 Subject: [PATCH 02/65] gabi-2 --- .../components/GestureZone.tsx | 7 ++-- .../components/HomeScreen.tsx | 39 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index c14e55a3e..cb6362885 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -75,15 +75,16 @@ export const GestureZone: React.FC = () => { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {!isActive &&
+ {!isActive &&
+
} diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index 80e49dedb..bbd24881d 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -421,20 +421,28 @@ export const HomeScreen: React.FC = () => { ); })}
- - + My apps +
+
- - Recent - + + Recent +
@@ -580,7 +588,7 @@ export const HomeScreen: React.FC = () => { {!isEditMode && <> { -
+
{

Your node, your data: finally, you have full control over your information.

-
+
{ onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery} /> + +
+ No installed apps found. + { + openApp({ + id: 'app-store', + label: 'App Store', + process: 'main', + package_name: 'app-store', + publisher: 'sys', + order: 0, + favorite: false, + }, `?search=${searchQuery}`) + }} + > + Search the app store + +
} diff --git a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts index 03ea8d3f0..b7dfae466 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts @@ -7,7 +7,7 @@ interface NavigationStore { isAppDrawerOpen: boolean; isRecentAppsOpen: boolean; - openApp: (app: HomepageApp) => void; + openApp: (app: HomepageApp, query?: string) => void; closeApp: (appId: string) => void; switchToApp: (appId: string) => void; toggleAppDrawer: () => void; @@ -65,7 +65,7 @@ export const useNavigationStore = create((set, get) => ({ // If already on homepage, let default browser behavior handle it }, - openApp: async (app) => { + openApp: async (app: HomepageApp, query?: string) => { console.log('openApp called with:', app); // Don't open apps without a valid path @@ -128,7 +128,7 @@ export const useNavigationStore = create((set, get) => ({ const hostname = currentHost.split(':')[0]; // 'localhost' from 'localhost:3000' const baseDomain = hostname; // For localhost, we just use 'localhost' - const subdomainUrl = `${protocol}//${expectedSubdomain}.${baseDomain}${port}${appUrl}`; + const subdomainUrl = `${protocol}//${expectedSubdomain}.${baseDomain}${port}${appUrl}${query || ''}`; // Debug logging console.log('Opening secure subdomain app in new tab:', { @@ -139,7 +139,8 @@ export const useNavigationStore = create((set, get) => ({ expectedSubdomain, subdomainUrl, protocol, - port + port, + query, }); const newWindow = window.open(subdomainUrl, '_blank'); From f909c330cade37c4fd181e5cbd62b778305ec761 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 13 Aug 2025 14:25:01 -0400 Subject: [PATCH 04/65] searching an uninstalled app in the homepage links you to the appstore --- Cargo.lock | 224 +++++++++++++++++- .../app-store/ui/src/pages/StorePage.tsx | 4 +- hyperdrive/packages/file-explorer/Cargo.toml | 1 + .../file-explorer/explorer/Cargo.toml | 3 + .../components/HomeScreen.tsx | 19 +- .../homepage/ui/src/stores/navigationStore.ts | 18 +- hyperdrive/packages/terminal/Cargo.lock | 80 +++++-- 7 files changed, 308 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd4429540..9f2966494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,28 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "add-node-provider" +version = "0.1.0" +dependencies = [ + "hyperware_process_lib 2.0.0", + "rmp-serde", + "serde", + "serde_json", + "wit-bindgen 0.42.1", +] + +[[package]] +name = "add-rpcurl-provider" +version = "0.1.0" +dependencies = [ + "hyperware_process_lib 2.0.0", + "rmp-serde", + "serde", + "serde_json", + "wit-bindgen 0.42.1", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -1025,6 +1047,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver 1.0.26", + "serde", + "serde_json", + "topological-sort", +] + [[package]] name = "auto_impl" version = "1.3.0" @@ -1289,6 +1323,22 @@ dependencies = [ "serde", ] +[[package]] +name = "caller-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-util", + "hyperware_app_common", + "once_cell", + "process_macros", + "serde", + "serde_json", + "uuid 1.17.0", + "wit-bindgen 0.41.0", +] + [[package]] name = "camino" version = "1.1.10" @@ -2450,6 +2500,7 @@ name = "explorer" version = "0.1.0" dependencies = [ "anyhow", + "caller-utils", "hyperprocess_macro", "hyperware_app_common", "md5", @@ -2777,6 +2828,17 @@ dependencies = [ "wit-bindgen 0.42.1", ] +[[package]] +name = "get-providers" +version = "0.1.0" +dependencies = [ + "hyperware_process_lib 2.0.0", + "rmp-serde", + "serde", + "serde_json", + "wit-bindgen 0.42.1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -3883,8 +3945,8 @@ dependencies = [ [[package]] name = "kit" -version = "2.1.0" -source = "git+https://github.com/hyperware-ai/kit?rev=531f660#531f660352376393a32952793a17c4605fb721ad" +version = "3.0.1" +source = "git+https://github.com/hyperware-ai/kit?rev=79fe678#79fe678bd287bbd949e9469f4a9a5a28339ab10e" dependencies = [ "alloy", "alloy-sol-macro", @@ -5212,6 +5274,17 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "remove-provider" +version = "0.1.0" +dependencies = [ + "hyperware_process_lib 2.0.0", + "rmp-serde", + "serde", + "serde_json", + "wit-bindgen 0.42.1", +] + [[package]] name = "reqwest" version = "0.12.20" @@ -6514,6 +6587,12 @@ dependencies = [ "wit-bindgen 0.42.1", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower" version = "0.5.2" @@ -7126,6 +7205,16 @@ dependencies = [ "wasmparser 0.220.1", ] +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser 0.227.1", +] + [[package]] name = "wasm-encoder" version = "0.229.0" @@ -7172,6 +7261,25 @@ dependencies = [ "wasmparser 0.220.1", ] +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder 0.227.1", + "wasmparser 0.227.1", +] + [[package]] name = "wasm-metadata" version = "0.230.0" @@ -7197,6 +7305,18 @@ dependencies = [ "semver 1.0.26", ] +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags 2.9.1", + "hashbrown 0.15.4", + "indexmap", + "semver 1.0.26", +] + [[package]] name = "wasmparser" version = "0.229.0" @@ -8026,6 +8146,16 @@ dependencies = [ "wit-bindgen-rust-macro 0.36.0", ] +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt 0.41.0", + "wit-bindgen-rust-macro 0.41.0", +] + [[package]] name = "wit-bindgen" version = "0.42.1" @@ -8047,6 +8177,17 @@ dependencies = [ "wit-parser 0.220.1", ] +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser 0.227.1", +] + [[package]] name = "wit-bindgen-core" version = "0.42.1" @@ -8076,6 +8217,17 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags 2.9.1", + "futures", + "once_cell", +] + [[package]] name = "wit-bindgen-rt" version = "0.42.1" @@ -8103,6 +8255,22 @@ dependencies = [ "wit-component 0.220.1", ] +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.104", + "wasm-metadata 0.227.1", + "wit-bindgen-core 0.41.0", + "wit-component 0.227.1", +] + [[package]] name = "wit-bindgen-rust" version = "0.42.1" @@ -8134,6 +8302,21 @@ dependencies = [ "wit-bindgen-rust 0.36.0", ] +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.104", + "wit-bindgen-core 0.41.0", + "wit-bindgen-rust 0.41.0", +] + [[package]] name = "wit-bindgen-rust-macro" version = "0.42.1" @@ -8168,6 +8351,25 @@ dependencies = [ "wit-parser 0.220.1", ] +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.227.1", + "wasm-metadata 0.227.1", + "wasmparser 0.227.1", + "wit-parser 0.227.1", +] + [[package]] name = "wit-component" version = "0.230.0" @@ -8205,6 +8407,24 @@ dependencies = [ "wasmparser 0.220.1", ] +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.227.1", +] + [[package]] name = "wit-parser" version = "0.229.0" diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index 0179ae547..aa873d547 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -106,12 +106,13 @@ export default function StorePage() { // if we have ?search=something, set the search query to that const location = useLocation(); useEffect(() => { + console.log({ location }) const search = new URLSearchParams(location.search).get("search"); if (search) { setSearchQuery(search); setCurrentPage(1); } - }, [location.search]); + }, [location]); const onInputChange = (e: React.ChangeEvent) => { console.log(e.target.value, searchQuery); @@ -172,6 +173,7 @@ export default function StorePage() { value={searchQuery} onChange={onInputChange} className="grow text-sm !bg-transparent" + autoFocus />
diff --git a/hyperdrive/packages/file-explorer/Cargo.toml b/hyperdrive/packages/file-explorer/Cargo.toml index 299a6ba1d..4aa010ccf 100644 --- a/hyperdrive/packages/file-explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/Cargo.toml @@ -6,5 +6,6 @@ panic = "abort" [workspace] members = [ "explorer", + "target/caller-utils", ] resolver = "2" diff --git a/hyperdrive/packages/file-explorer/explorer/Cargo.toml b/hyperdrive/packages/file-explorer/explorer/Cargo.toml index 3b36f1446..0218351a2 100644 --- a/hyperdrive/packages/file-explorer/explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/explorer/Cargo.toml @@ -7,6 +7,9 @@ serde_urlencoded = "0.7" tracing = "0.1.37" wit-bindgen = "0.42.1" +[dependencies.caller-utils] +path = "../target/caller-utils" + [dependencies.hyperprocess_macro] git = "https://github.com/hyperware-ai/hyperprocess-macro" rev = "9836e2a" diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index a5f33125b..a06f1f0c0 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -613,25 +613,18 @@ export const HomeScreen: React.FC = () => { />
No installed apps found. { - openApp({ - id: 'app-store', - label: 'App Store', - process: 'main', - package_name: 'app-store', - publisher: 'sys', - order: 0, - favorite: false, - }, `?search=${searchQuery}`) + setSearchQuery('') + openApp(apps.find(a => a.id === 'main:app-store:sys')!, `?search=${searchQuery}`) }} > Search the app store diff --git a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts index b7dfae466..ac12dc79d 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts @@ -167,13 +167,11 @@ export const useNavigationStore = create((set, get) => ({ const existingApp = runningApps.find(a => a.id === app.id); // Add to browser history for back button support - if (typeof window !== 'undefined') { - window.history.pushState( - { type: 'app', appId: app.id, previousAppId: currentAppId }, - '', - `#app-${app.id}` - ); - } + window?.history?.pushState( + { type: 'app', appId: app.id, previousAppId: currentAppId }, + '', + `#app-${app.id}${query || ''}` + ); if (existingApp) { set({ @@ -183,7 +181,11 @@ export const useNavigationStore = create((set, get) => ({ }); } else { set({ - runningApps: [...runningApps, { ...app, openedAt: Date.now() }], + runningApps: [...runningApps, { + ...app, + path: `${app.path}${query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''}`, + openedAt: Date.now() + }], currentAppId: app.id, isAppDrawerOpen: false, isRecentAppsOpen: false, diff --git a/hyperdrive/packages/terminal/Cargo.lock b/hyperdrive/packages/terminal/Cargo.lock index 538883c0c..359e56683 100644 --- a/hyperdrive/packages/terminal/Cargo.lock +++ b/hyperdrive/packages/terminal/Cargo.lock @@ -6,7 +6,7 @@ version = 4 name = "add-node-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.1.0", "rmp-serde", "serde", "serde_json", @@ -17,7 +17,7 @@ dependencies = [ name = "add-rpcurl-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.1.0", "rmp-serde", "serde", "serde_json", @@ -66,7 +66,7 @@ name = "alias" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "serde", "serde_json", "wit-bindgen", @@ -89,10 +89,13 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-json-rpc", + "alloy-network", "alloy-provider", "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", + "alloy-signer", + "alloy-signer-local", "alloy-transport", "alloy-transport-http", ] @@ -464,6 +467,22 @@ dependencies = [ "thiserror 2.0.9", ] +[[package]] +name = "alloy-signer-local" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fababf5a745133490cde927d48e50267f97d3d1209b9fc9f1d1d666964d172" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand", + "thiserror 2.0.9", +] + [[package]] name = "alloy-sol-macro" version = "0.8.15" @@ -966,7 +985,7 @@ name = "cat" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "serde", "serde_json", "wit-bindgen", @@ -1210,7 +1229,7 @@ dependencies = [ name = "echo" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "wit-bindgen", ] @@ -1457,7 +1476,7 @@ dependencies = [ name = "get-providers" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.1.0", "rmp-serde", "serde", "serde_json", @@ -1532,7 +1551,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "help" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "wit-bindgen", ] @@ -1562,7 +1581,7 @@ name = "hfetch" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "rmp-serde", "serde", "serde_json", @@ -1573,7 +1592,7 @@ dependencies = [ name = "hi" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "serde", "serde_json", "wit-bindgen", @@ -1706,6 +1725,33 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "hyperware_process_lib" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3abd008d22c3b96ee43300c4c8dffbf1d072a680a13635b5f9da11a0ce9395" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-macro", + "alloy-sol-types", + "anyhow", + "base64", + "bincode", + "hex", + "http", + "mime_guess", + "rand", + "regex", + "rmp-serde", + "serde", + "serde_json", + "sha3", + "thiserror 1.0.69", + "url", + "wit-bindgen", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1965,7 +2011,7 @@ name = "kill" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "serde", "serde_json", "wit-bindgen", @@ -2038,7 +2084,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "regex", "serde", "serde_json", @@ -2108,7 +2154,7 @@ dependencies = [ name = "net-diagnostics" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "rmp-serde", "serde", "wit-bindgen", @@ -2304,7 +2350,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" name = "peer" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "rmp-serde", "serde", "wit-bindgen", @@ -2314,7 +2360,7 @@ dependencies = [ name = "peers" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "rmp-serde", "serde", "wit-bindgen", @@ -2578,7 +2624,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "remove-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib", + "hyperware_process_lib 2.1.0", "rmp-serde", "serde", "serde_json", @@ -3127,7 +3173,7 @@ version = "0.1.1" dependencies = [ "anyhow", "bincode", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "rand", "regex", "serde", @@ -3288,7 +3334,7 @@ version = "0.2.0" dependencies = [ "anyhow", "clap", - "hyperware_process_lib", + "hyperware_process_lib 2.0.1", "serde", "serde_json", "wit-bindgen", From ab2bf5687fea2d928b88f28996aed590e5ff9cfb Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 13 Aug 2025 14:28:26 -0400 Subject: [PATCH 05/65] undo automatic cargo changes --- hyperdrive/packages/file-explorer/Cargo.toml | 1 - .../file-explorer/explorer/Cargo.toml | 3 - hyperdrive/packages/terminal/Cargo.lock | 80 ++++--------------- 3 files changed, 17 insertions(+), 67 deletions(-) diff --git a/hyperdrive/packages/file-explorer/Cargo.toml b/hyperdrive/packages/file-explorer/Cargo.toml index 4aa010ccf..299a6ba1d 100644 --- a/hyperdrive/packages/file-explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/Cargo.toml @@ -6,6 +6,5 @@ panic = "abort" [workspace] members = [ "explorer", - "target/caller-utils", ] resolver = "2" diff --git a/hyperdrive/packages/file-explorer/explorer/Cargo.toml b/hyperdrive/packages/file-explorer/explorer/Cargo.toml index 0218351a2..3b36f1446 100644 --- a/hyperdrive/packages/file-explorer/explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/explorer/Cargo.toml @@ -7,9 +7,6 @@ serde_urlencoded = "0.7" tracing = "0.1.37" wit-bindgen = "0.42.1" -[dependencies.caller-utils] -path = "../target/caller-utils" - [dependencies.hyperprocess_macro] git = "https://github.com/hyperware-ai/hyperprocess-macro" rev = "9836e2a" diff --git a/hyperdrive/packages/terminal/Cargo.lock b/hyperdrive/packages/terminal/Cargo.lock index 359e56683..538883c0c 100644 --- a/hyperdrive/packages/terminal/Cargo.lock +++ b/hyperdrive/packages/terminal/Cargo.lock @@ -6,7 +6,7 @@ version = 4 name = "add-node-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.1.0", + "hyperware_process_lib", "rmp-serde", "serde", "serde_json", @@ -17,7 +17,7 @@ dependencies = [ name = "add-rpcurl-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.1.0", + "hyperware_process_lib", "rmp-serde", "serde", "serde_json", @@ -66,7 +66,7 @@ name = "alias" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "serde", "serde_json", "wit-bindgen", @@ -89,13 +89,10 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-json-rpc", - "alloy-network", "alloy-provider", "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", - "alloy-signer", - "alloy-signer-local", "alloy-transport", "alloy-transport-http", ] @@ -467,22 +464,6 @@ dependencies = [ "thiserror 2.0.9", ] -[[package]] -name = "alloy-signer-local" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47fababf5a745133490cde927d48e50267f97d3d1209b9fc9f1d1d666964d172" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-signer", - "async-trait", - "k256", - "rand", - "thiserror 2.0.9", -] - [[package]] name = "alloy-sol-macro" version = "0.8.15" @@ -985,7 +966,7 @@ name = "cat" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "serde", "serde_json", "wit-bindgen", @@ -1229,7 +1210,7 @@ dependencies = [ name = "echo" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "wit-bindgen", ] @@ -1476,7 +1457,7 @@ dependencies = [ name = "get-providers" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.1.0", + "hyperware_process_lib", "rmp-serde", "serde", "serde_json", @@ -1551,7 +1532,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "help" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "wit-bindgen", ] @@ -1581,7 +1562,7 @@ name = "hfetch" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "rmp-serde", "serde", "serde_json", @@ -1592,7 +1573,7 @@ dependencies = [ name = "hi" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "serde", "serde_json", "wit-bindgen", @@ -1725,33 +1706,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "hyperware_process_lib" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3abd008d22c3b96ee43300c4c8dffbf1d072a680a13635b5f9da11a0ce9395" -dependencies = [ - "alloy", - "alloy-primitives", - "alloy-sol-macro", - "alloy-sol-types", - "anyhow", - "base64", - "bincode", - "hex", - "http", - "mime_guess", - "rand", - "regex", - "rmp-serde", - "serde", - "serde_json", - "sha3", - "thiserror 1.0.69", - "url", - "wit-bindgen", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -2011,7 +1965,7 @@ name = "kill" version = "0.1.0" dependencies = [ "anyhow", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "serde", "serde_json", "wit-bindgen", @@ -2084,7 +2038,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "regex", "serde", "serde_json", @@ -2154,7 +2108,7 @@ dependencies = [ name = "net-diagnostics" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "rmp-serde", "serde", "wit-bindgen", @@ -2350,7 +2304,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" name = "peer" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "rmp-serde", "serde", "wit-bindgen", @@ -2360,7 +2314,7 @@ dependencies = [ name = "peers" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "rmp-serde", "serde", "wit-bindgen", @@ -2624,7 +2578,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" name = "remove-provider" version = "0.1.0" dependencies = [ - "hyperware_process_lib 2.1.0", + "hyperware_process_lib", "rmp-serde", "serde", "serde_json", @@ -3173,7 +3127,7 @@ version = "0.1.1" dependencies = [ "anyhow", "bincode", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "rand", "regex", "serde", @@ -3334,7 +3288,7 @@ version = "0.2.0" dependencies = [ "anyhow", "clap", - "hyperware_process_lib 2.0.1", + "hyperware_process_lib", "serde", "serde_json", "wit-bindgen", From 28c0b2cdd3736f3a12a9b03f0dc10b56d32673d6 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 13 Aug 2025 14:29:37 -0400 Subject: [PATCH 06/65] cargo-again --- Cargo.lock | 224 +---------------------------------------------------- 1 file changed, 2 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f2966494..cd4429540 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,28 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "add-node-provider" -version = "0.1.0" -dependencies = [ - "hyperware_process_lib 2.0.0", - "rmp-serde", - "serde", - "serde_json", - "wit-bindgen 0.42.1", -] - -[[package]] -name = "add-rpcurl-provider" -version = "0.1.0" -dependencies = [ - "hyperware_process_lib 2.0.0", - "rmp-serde", - "serde", - "serde_json", - "wit-bindgen 0.42.1", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -1047,18 +1025,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "auditable-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" -dependencies = [ - "semver 1.0.26", - "serde", - "serde_json", - "topological-sort", -] - [[package]] name = "auto_impl" version = "1.3.0" @@ -1323,22 +1289,6 @@ dependencies = [ "serde", ] -[[package]] -name = "caller-utils" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures", - "futures-util", - "hyperware_app_common", - "once_cell", - "process_macros", - "serde", - "serde_json", - "uuid 1.17.0", - "wit-bindgen 0.41.0", -] - [[package]] name = "camino" version = "1.1.10" @@ -2500,7 +2450,6 @@ name = "explorer" version = "0.1.0" dependencies = [ "anyhow", - "caller-utils", "hyperprocess_macro", "hyperware_app_common", "md5", @@ -2828,17 +2777,6 @@ dependencies = [ "wit-bindgen 0.42.1", ] -[[package]] -name = "get-providers" -version = "0.1.0" -dependencies = [ - "hyperware_process_lib 2.0.0", - "rmp-serde", - "serde", - "serde_json", - "wit-bindgen 0.42.1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -3945,8 +3883,8 @@ dependencies = [ [[package]] name = "kit" -version = "3.0.1" -source = "git+https://github.com/hyperware-ai/kit?rev=79fe678#79fe678bd287bbd949e9469f4a9a5a28339ab10e" +version = "2.1.0" +source = "git+https://github.com/hyperware-ai/kit?rev=531f660#531f660352376393a32952793a17c4605fb721ad" dependencies = [ "alloy", "alloy-sol-macro", @@ -5274,17 +5212,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "remove-provider" -version = "0.1.0" -dependencies = [ - "hyperware_process_lib 2.0.0", - "rmp-serde", - "serde", - "serde_json", - "wit-bindgen 0.42.1", -] - [[package]] name = "reqwest" version = "0.12.20" @@ -6587,12 +6514,6 @@ dependencies = [ "wit-bindgen 0.42.1", ] -[[package]] -name = "topological-sort" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" - [[package]] name = "tower" version = "0.5.2" @@ -7205,16 +7126,6 @@ dependencies = [ "wasmparser 0.220.1", ] -[[package]] -name = "wasm-encoder" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" -dependencies = [ - "leb128fmt", - "wasmparser 0.227.1", -] - [[package]] name = "wasm-encoder" version = "0.229.0" @@ -7261,25 +7172,6 @@ dependencies = [ "wasmparser 0.220.1", ] -[[package]] -name = "wasm-metadata" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" -dependencies = [ - "anyhow", - "auditable-serde", - "flate2", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "url", - "wasm-encoder 0.227.1", - "wasmparser 0.227.1", -] - [[package]] name = "wasm-metadata" version = "0.230.0" @@ -7305,18 +7197,6 @@ dependencies = [ "semver 1.0.26", ] -[[package]] -name = "wasmparser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" -dependencies = [ - "bitflags 2.9.1", - "hashbrown 0.15.4", - "indexmap", - "semver 1.0.26", -] - [[package]] name = "wasmparser" version = "0.229.0" @@ -8146,16 +8026,6 @@ dependencies = [ "wit-bindgen-rust-macro 0.36.0", ] -[[package]] -name = "wit-bindgen" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" -dependencies = [ - "wit-bindgen-rt 0.41.0", - "wit-bindgen-rust-macro 0.41.0", -] - [[package]] name = "wit-bindgen" version = "0.42.1" @@ -8177,17 +8047,6 @@ dependencies = [ "wit-parser 0.220.1", ] -[[package]] -name = "wit-bindgen-core" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser 0.227.1", -] - [[package]] name = "wit-bindgen-core" version = "0.42.1" @@ -8217,17 +8076,6 @@ dependencies = [ "bitflags 2.9.1", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" -dependencies = [ - "bitflags 2.9.1", - "futures", - "once_cell", -] - [[package]] name = "wit-bindgen-rt" version = "0.42.1" @@ -8255,22 +8103,6 @@ dependencies = [ "wit-component 0.220.1", ] -[[package]] -name = "wit-bindgen-rust" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.104", - "wasm-metadata 0.227.1", - "wit-bindgen-core 0.41.0", - "wit-component 0.227.1", -] - [[package]] name = "wit-bindgen-rust" version = "0.42.1" @@ -8302,21 +8134,6 @@ dependencies = [ "wit-bindgen-rust 0.36.0", ] -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.104", - "wit-bindgen-core 0.41.0", - "wit-bindgen-rust 0.41.0", -] - [[package]] name = "wit-bindgen-rust-macro" version = "0.42.1" @@ -8351,25 +8168,6 @@ dependencies = [ "wit-parser 0.220.1", ] -[[package]] -name = "wit-component" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" -dependencies = [ - "anyhow", - "bitflags 2.9.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.227.1", - "wasm-metadata 0.227.1", - "wasmparser 0.227.1", - "wit-parser 0.227.1", -] - [[package]] name = "wit-component" version = "0.230.0" @@ -8407,24 +8205,6 @@ dependencies = [ "wasmparser 0.220.1", ] -[[package]] -name = "wit-parser" -version = "0.227.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver 1.0.26", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.227.1", -] - [[package]] name = "wit-parser" version = "0.229.0" From 2e6013f575d9a59278035ab9514a505a3a588335 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 11:03:23 -0400 Subject: [PATCH 07/65] modalize-message --- hyperdrive/src/http/login.html | 47 ++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/hyperdrive/src/http/login.html b/hyperdrive/src/http/login.html index 0a3ede650..cec8fd3fc 100644 --- a/hyperdrive/src/http/login.html +++ b/hyperdrive/src/http/login.html @@ -143,13 +143,43 @@
-
+
+
+
+ Need help? +
+
@@ -163,7 +193,7 @@

Logging in...

if ('${fake}' === 'true') { document.getElementById("fake-or-not").innerHTML = "Fake node -- any password will work!"; } else { - document.getElementById("fake-or-not").innerHTML = "Restart your node to change networking settings."; + document.getElementById("fake-or-not").remove(); } const firstPathItem = window.location.pathname.split('/')[1]; @@ -242,6 +272,14 @@

Logging in...

return subdomain; } + function showHelpModal() { + document.getElementById("help-modal-backdrop").classList.remove("hidden"); + } + + function hideHelpModal() { + document.getElementById("help-modal-backdrop").classList.add("hidden"); + } + document.addEventListener("DOMContentLoaded", () => { const [isSecureSubdomain, firstPathItem] = initializeLoginForm(); const form = document.getElementById("login-form"); @@ -253,6 +291,11 @@

Logging in...

login(password, isSecureSubdomain, firstPathItem); } }); + + document.getElementById("help-modal-x").addEventListener("click", hideHelpModal); + document.getElementById("help-modal-backdrop").addEventListener("click", hideHelpModal); + document.getElementById("help-message").addEventListener("click", showHelpModal); + }); From db94e8ddaf0877b1dcb232dcba278922f2abb2b2 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 16:22:30 -0400 Subject: [PATCH 08/65] login-help --- css/hyperware.css | 89 ++++++++++++++++++++++++++++++++++ hyperdrive/src/http/login.html | 45 +++++++---------- 2 files changed, 107 insertions(+), 27 deletions(-) diff --git a/css/hyperware.css b/css/hyperware.css index 37855305e..6548890f1 100644 --- a/css/hyperware.css +++ b/css/hyperware.css @@ -2654,6 +2654,10 @@ left: calc(var(--spacing)*2) } + .right-2 { + right: calc(var(--spacing)*2) + } + .z-0 { z-index: 0 } @@ -2666,6 +2670,86 @@ z-index: 20 } + .bg-white { + background-color: var(--color-white); + } + + @media (prefers-color-scheme: dark) { + .dark\:bg-black { + background-color: var(--color-black); + } + + .dark\:shadow-white\/10 { + box-shadow: 0 10px 15px -3px rgba(255, 255, 255, 0.1), 0 4px 6px -2px rgba(255, 255, 255, 0.05); + } + } + + .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + } + + .p-4 { + padding: calc(var(--spacing)*4); + } + + .rounded-lg { + border-radius: var(--radius-lg); + } + + .relative { + position: relative; + } + + .w-full { + width: 100%; + } + + .max-w-md { + max-width: var(--container-md); + } + + .min-h-0 { + min-height: 0; + } + + .max-h-screen { + max-height: 100vh; + } + + .overflow-y-auto { + overflow-y: auto; + } + + .backdrop-blur-sm { + backdrop-filter: blur(4px); + } + + .items-center { + align-items: center; + } + + .justify-center { + justify-content: center; + } + + .z-50 { + z-index: 50; + } + + .hidden { + display: none; + } + + .fixed { + position: fixed; + } + + .inset-0 { + inset: 0; + } + + + .container { width: 100% } @@ -2946,6 +3030,11 @@ line-height: var(--tw-leading, var(--text-5xl--line-height)) } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)) + } + .text-sm { font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)) diff --git a/hyperdrive/src/http/login.html b/hyperdrive/src/http/login.html index cec8fd3fc..d05d29aff 100644 --- a/hyperdrive/src/http/login.html +++ b/hyperdrive/src/http/login.html @@ -143,15 +143,9 @@
-
+
-
+
Need help?
@@ -160,26 +154,21 @@

Logging in...

- +
@@ -274,9 +263,11 @@

Trying to change network settings?

function showHelpModal() { document.getElementById("help-modal-backdrop").classList.remove("hidden"); + document.getElementById("help-modal-backdrop").classList.add("flex"); } function hideHelpModal() { + document.getElementById("help-modal-backdrop").classList.remove("flex"); document.getElementById("help-modal-backdrop").classList.add("hidden"); } From 331bbb3f6d43f735455c61db7cebbbd7db84605d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Thu, 14 Aug 2025 15:30:30 -0700 Subject: [PATCH 09/65] bump version to 1.6.1 --- Cargo.toml | 2 +- hyperdrive/Cargo.toml | 2 +- lib/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6b9c46f48..798d6772a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive_lib" authors = ["Sybil Technologies AG"] -version = "1.6.0" +version = "1.6.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/hyperdrive/Cargo.toml b/hyperdrive/Cargo.toml index d99c31601..f20151454 100644 --- a/hyperdrive/Cargo.toml +++ b/hyperdrive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hyperdrive" authors = ["Sybil Technologies AG"] -version = "1.6.0" +version = "1.6.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f0a8d10e1..b9c53141d 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lib" authors = ["Sybil Technologies AG"] -version = "1.6.0" +version = "1.6.1" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" From 36463d38a0e0c277d62ef83108cf4c28439d26ac Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 21:27:59 -0400 Subject: [PATCH 10/65] review-1 --- .../components/GestureZone.tsx | 203 +++++++++++------- .../components/HomeScreen.tsx | 10 +- .../AndroidHomescreen/components/Modal.tsx | 11 +- .../components/RecentApps.tsx | 28 ++- 4 files changed, 157 insertions(+), 95 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index cb6362885..584b9c324 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -1,113 +1,156 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { useNavigationStore } from '../../../stores/navigationStore'; import classNames from 'classnames'; -import { BsChevronLeft, BsClock, BsHouse } from 'react-icons/bs'; +import { BsClock } from 'react-icons/bs'; export const GestureZone: React.FC = () => { - const { toggleRecentApps, runningApps, currentAppId, switchToApp, isRecentAppsOpen, closeAllOverlays } = useNavigationStore(); - const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null); - const [isActive, setIsActive] = useState(false); - const [_isHovered, setIsHovered] = useState(false); + const { toggleRecentApps, isRecentAppsOpen } = useNavigationStore(); + const [position, setPosition] = useState({ x: window.innerWidth - 80, y: window.innerHeight / 2 }); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); + const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps + const buttonRef = useRef(null); - // Touch handlers + // Touch handlers for drag and tap const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); const touch = e.touches[0]; - setTouchStart({ x: touch.clientX, y: touch.clientY }); - setIsActive(true); + setDragStart({ + x: touch.clientX, + y: touch.clientY, + buttonX: position.x, + buttonY: position.y + }); }; const handleTouchMove = (e: React.TouchEvent) => { - if (!touchStart) return; + if (!dragStart) return; + e.preventDefault(); const touch = e.touches[0]; - const deltaX = touchStart.x - touch.clientX; - const deltaY = touch.clientY - touchStart.y; + const deltaX = touch.clientX - dragStart.x; + const deltaY = touch.clientY - dragStart.y; - // Swipe left (show recent apps) - if (deltaX > 50 && Math.abs(deltaY) < 30) { - toggleRecentApps(); - setTouchStart(null); + // Check if movement exceeds threshold to start dragging + if (!isDragging && (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { + setIsDragging(true); } - // Swipe up/down (switch apps) - if (Math.abs(deltaY) > 50 && Math.abs(deltaX) < 30) { - const currentIndex = runningApps.findIndex(app => app.id === currentAppId); - if (currentIndex !== -1) { - const newIndex = deltaY > 0 - ? Math.min(currentIndex + 1, runningApps.length - 1) - : Math.max(currentIndex - 1, 0); - if (newIndex !== currentIndex) { - switchToApp(runningApps[newIndex].id); - } - } - setTouchStart(null); + // Update position if dragging + if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { + const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); + const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); + setPosition({ x: newX, y: newY }); } }; const handleTouchEnd = () => { - setTouchStart(null); - setIsActive(false); + if (!isDragging && dragStart) { + // Tap - open recent apps + toggleRecentApps(); + } + setDragStart(null); + setIsDragging(false); + }; + + // Mouse handlers for desktop + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setDragStart({ + x: e.clientX, + y: e.clientY, + buttonX: position.x, + buttonY: position.y + }); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!dragStart) return; + + const deltaX = e.clientX - dragStart.x; + const deltaY = e.clientY - dragStart.y; + + if (!isDragging && (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { + setIsDragging(true); + } + + if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { + const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); + const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); + setPosition({ x: newX, y: newY }); + } }; - // Desktop click handler - const handleClick = () => { - toggleRecentApps(); + const handleMouseUp = () => { + if (!isDragging && dragStart) { + toggleRecentApps(); + } + setDragStart(null); + setIsDragging(false); }; + // Mouse event listeners + useEffect(() => { + if (dragStart) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [dragStart, isDragging]); + + // Handle window resize to keep button in bounds + useEffect(() => { + const handleResize = () => { + setPosition(prev => ({ + x: Math.max(30, Math.min(window.innerWidth - 30, prev.x)), + y: Math.max(30, Math.min(window.innerHeight - 30, prev.y)) + })); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + useEffect(() => { if (!isRecentAppsOpen) { - setTouchStart(null); - setIsActive(false); + setDragStart(null); + setIsDragging(false); } }, [isRecentAppsOpen]); return ( - <> -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {!isActive &&
- -
- -
} -
+
+ {/* Black rounded square background */} +
- {/* {isHovered && !isActive && ( -
-
- Click - or - S - Recent apps -
-
- A - All apps -
-
- H - Home -
+ {/* White circle with icon */} +
+
+
- )} */} - +
+
); }; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index a06f1f0c0..d61069a5e 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -586,7 +586,7 @@ export const HomeScreen: React.FC = () => { )} {!isEditMode && <> - { title="Get help & support" > - + */} +
{children}
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx index 08eb213a1..c3c18be4a 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx @@ -9,9 +9,15 @@ export const RecentApps: React.FC = () => { if (!isRecentAppsOpen) return null; return ( -
+
{runningApps.length === 0 ? ( -
+
📱

No running apps

Open an app to see it here

@@ -29,16 +35,24 @@ export const RecentApps: React.FC = () => { {runningApps.map(app => (
switchToApp(app.id)} + className={` + relative flex-shrink-0 w-72 h-96 + bg-gradient-to-b from-black/10 to-black/20 dark:from-white/10 dark:to-white/20 + rounded-3xl overflow-hidden cursor-pointer + group transform transition-all hover:scale-105 hover:shadow-2xl + `} + onClick={(e) => { + try { e.stopPropagation(); } catch { } + try { e.preventDefault(); } catch { } + switchToApp(app.id); + }} >
{app.label} @@ -481,22 +481,26 @@ export const HomeScreen: React.FC = () => {
-
+
Date: Thu, 14 Aug 2025 22:16:23 -0400 Subject: [PATCH 12/65] review-3 --- .../AndroidHomescreen/components/GestureZone.tsx | 7 +++++-- .../AndroidHomescreen/components/HomeScreen.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index c2a6bf9d1..335d12140 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -10,6 +10,7 @@ export const GestureZone: React.FC = () => { const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps const buttonRef = useRef(null); + const isMobile = window.innerWidth < 768; // Touch handlers for drag and tap const handleTouchStart = (e: React.TouchEvent) => { @@ -57,6 +58,7 @@ export const GestureZone: React.FC = () => { // Mouse handlers for desktop const handleMouseDown = (e: React.MouseEvent) => { + if (isMobile) return; try { e.preventDefault(); } catch { } try { e.stopPropagation(); } catch { } setDragStart({ @@ -68,6 +70,7 @@ export const GestureZone: React.FC = () => { }; const handleMouseMove = (e: MouseEvent) => { + if (isMobile) return; try { e.preventDefault(); } catch { } try { e.stopPropagation(); } catch { } if (!dragStart) return; @@ -87,6 +90,7 @@ export const GestureZone: React.FC = () => { }; const handleMouseUp = () => { + if (isMobile) return; if (!isDragging && dragStart) { toggleRecentApps(); } @@ -152,8 +156,7 @@ export const GestureZone: React.FC = () => { {/* White circle with icon */}
-
-
+
); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index dfad26260..85bfea715 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -420,13 +420,13 @@ export const HomeScreen: React.FC = () => {
); })} -
+
@@ -437,7 +437,7 @@ export const HomeScreen: React.FC = () => { onClick={toggleRecentApps} > @@ -520,7 +520,7 @@ export const HomeScreen: React.FC = () => { })}> {showBackgroundSettings && ( -
+
Background
@@ -562,7 +562,7 @@ export const HomeScreen: React.FC = () => {
)} {showWidgetSettings && ( -
+
Widget Manager
{homeApps.filter(app => app.widget).map(app => ( From b8ec746bf10b209ce46430e9fc1e0f6909db846d Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 22:22:36 -0400 Subject: [PATCH 13/65] touch-none --- .../components/GestureZone.tsx | 2 +- .../components/HomeScreen.tsx | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index 335d12140..8d1fde70f 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -134,7 +134,7 @@ export const GestureZone: React.FC = () => {
{ }; const handleDockDrop = (e: React.DragEvent, index: number) => { - e.preventDefault(); - e.stopPropagation(); + try {e.preventDefault();} catch {} + try {e.stopPropagation();} catch {} const appId = e.dataTransfer.getData('appId'); if (appId) { // Add to dock at the specified index @@ -84,7 +84,8 @@ export const HomeScreen: React.FC = () => { }; const handleDockDragOver = (e: React.DragEvent) => { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } e.dataTransfer.dropEffect = 'move'; }; @@ -99,7 +100,8 @@ export const HomeScreen: React.FC = () => { const handleTouchMove = (e: React.TouchEvent) => { if (!draggedAppId || !touchDragPosition) return; - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } const touch = e.touches[0]; setTouchDragPosition({ x: touch.clientX, y: touch.clientY }); }; @@ -262,11 +264,13 @@ export const HomeScreen: React.FC = () => {
{ - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } e.dataTransfer.dropEffect = 'move'; }} onDrop={(e) => { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } const appId = e.dataTransfer.getData('appId'); // Only handle drops from dock apps or if dropping outside dock area const isDroppingOnDock = (e.target as HTMLElement).closest('.dock-area'); @@ -287,7 +291,8 @@ export const HomeScreen: React.FC = () => { const touch = e.touches[0]; const element = document.elementFromPoint(touch.clientX, touch.clientY); if (element?.closest('.dock-area')) { - e.preventDefault(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } } }} > @@ -370,7 +375,8 @@ export const HomeScreen: React.FC = () => { })} onDragOver={handleDockDragOver} onDrop={(e) => { - e.stopPropagation(); + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } handleDockDrop(e, index); }} > From 281d6ca5c3cdb718e23ca3bf7bbd83919ba80610 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 22:29:48 -0400 Subject: [PATCH 14/65] touch-none-2 --- .../AndroidHomescreen/components/GestureZone.tsx | 6 +++--- .../AndroidHomescreen/components/HomeScreen.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index 8d1fde70f..49ecc0e9b 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -152,11 +152,11 @@ export const GestureZone: React.FC = () => { onMouseDown={handleMouseDown} > {/* Black rounded square background */} -
+
{/* White circle with icon */} -
-
+
+
); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index f80d68780..fc4eaf1bb 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -256,9 +256,9 @@ export const HomeScreen: React.FC = () => { data-is-dark-mode={isDarkMode} > - {backgroundImage && ( -
- )} + {/* {backgroundImage && ( +
+ )} */}
{ )} {showWidgetSettings && (
- Widget Manager + Widgets
{homeApps.filter(app => app.widget).map(app => (
From ac56bb15a65a078953edb409909a20a04ce47886 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 22:48:58 -0400 Subject: [PATCH 15/65] passive-false --- .../components/GestureZone.tsx | 24 +++++++++---------- .../components/HomeScreen.tsx | 8 +++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index 49ecc0e9b..19d65f3f6 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -14,8 +14,8 @@ export const GestureZone: React.FC = () => { // Touch handlers for drag and tap const handleTouchStart = (e: React.TouchEvent) => { - try { e.preventDefault(); } catch { } - try { e.stopPropagation(); } catch { } + e.cancelable && e.preventDefault(); + e.stopPropagation(); const touch = e.touches[0]; setDragStart({ x: touch.clientX, @@ -27,8 +27,8 @@ export const GestureZone: React.FC = () => { const handleTouchMove = (e: React.TouchEvent) => { if (!dragStart) return; - try { e.preventDefault(); } catch { } - try { e.stopPropagation(); } catch { } + e.cancelable && e.preventDefault(); + e.stopPropagation(); const touch = e.touches[0]; const deltaX = touch.clientX - dragStart.x; @@ -59,8 +59,8 @@ export const GestureZone: React.FC = () => { // Mouse handlers for desktop const handleMouseDown = (e: React.MouseEvent) => { if (isMobile) return; - try { e.preventDefault(); } catch { } - try { e.stopPropagation(); } catch { } + e.cancelable && e.preventDefault(); + e.stopPropagation(); setDragStart({ x: e.clientX, y: e.clientY, @@ -71,8 +71,8 @@ export const GestureZone: React.FC = () => { const handleMouseMove = (e: MouseEvent) => { if (isMobile) return; - try { e.preventDefault(); } catch { } - try { e.stopPropagation(); } catch { } + e.cancelable && e.preventDefault(); + e.stopPropagation(); if (!dragStart) return; const deltaX = e.clientX - dragStart.x; @@ -101,11 +101,11 @@ export const GestureZone: React.FC = () => { // Mouse event listeners useEffect(() => { if (dragStart) { - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('mousemove', handleMouseMove), { passive: false }; + document.addEventListener('mouseup', handleMouseUp), { passive: false }; return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleMouseMove), { passive: false }; + document.removeEventListener('mouseup', handleMouseUp), { passive: false }; }; } }, [dragStart, isDragging]); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index fc4eaf1bb..304b14b4b 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -526,11 +526,11 @@ export const HomeScreen: React.FC = () => { })}> {showBackgroundSettings && ( -
+
Background
- + { onClick={() => setBackgroundImage(null)} className="w-full px-3 py-1.5 bg-red-500/30 hover:bg-red-500/50 rounded-lg text-white text-sm font-medium transition-all" > - Remove Background + Remove )}
)} {showWidgetSettings && ( -
+
Widgets
{homeApps.filter(app => app.widget).map(app => ( From cd64f273fcfbe281074dee9331cf70ce5fa53482 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 23:00:49 -0400 Subject: [PATCH 16/65] no-prevent-default --- .../components/AndroidHomescreen/components/GestureZone.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index 19d65f3f6..a2a1ac025 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -14,7 +14,6 @@ export const GestureZone: React.FC = () => { // Touch handlers for drag and tap const handleTouchStart = (e: React.TouchEvent) => { - e.cancelable && e.preventDefault(); e.stopPropagation(); const touch = e.touches[0]; setDragStart({ @@ -27,7 +26,6 @@ export const GestureZone: React.FC = () => { const handleTouchMove = (e: React.TouchEvent) => { if (!dragStart) return; - e.cancelable && e.preventDefault(); e.stopPropagation(); const touch = e.touches[0]; @@ -59,7 +57,6 @@ export const GestureZone: React.FC = () => { // Mouse handlers for desktop const handleMouseDown = (e: React.MouseEvent) => { if (isMobile) return; - e.cancelable && e.preventDefault(); e.stopPropagation(); setDragStart({ x: e.clientX, @@ -71,7 +68,6 @@ export const GestureZone: React.FC = () => { const handleMouseMove = (e: MouseEvent) => { if (isMobile) return; - e.cancelable && e.preventDefault(); e.stopPropagation(); if (!dragStart) return; From 380f736522611429c099faf0a9ddb4c2c0cd3315 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 23:13:09 -0400 Subject: [PATCH 17/65] buttons-behave --- .../AndroidHomescreen/components/GestureZone.tsx | 2 +- .../AndroidHomescreen/components/HomeScreen.tsx | 16 +++++++++++----- .../AndroidHomescreen/components/Widget.tsx | 2 -- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index a2a1ac025..dd297ce2b 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -5,7 +5,7 @@ import { BsClock } from 'react-icons/bs'; export const GestureZone: React.FC = () => { const { toggleRecentApps, isRecentAppsOpen } = useNavigationStore(); - const [position, setPosition] = useState({ x: window.innerWidth - 80, y: window.innerHeight / 2 }); + const [position, setPosition] = useState({ x: window.innerWidth - 80, y: 80 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index 304b14b4b..d9e2e4e7c 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -40,7 +40,7 @@ export const HomeScreen: React.FC = () => { const [showOnboarding, setShowOnboarding] = React.useState(!doNotShowOnboardingAgain); const [showWidgetOnboarding, setShowWidgetOnboarding] = React.useState(!doNotShowOnboardingAgain); - console.log({ appPositions }) + // console.log({ appPositions }) useEffect(() => { console.log('isInitialized', isInitialized); @@ -470,7 +470,7 @@ export const HomeScreen: React.FC = () => { )} -
+
Hyperdrive {
-
- - setSearchQuery(e.target.value)} - value={searchQuery} +
+ }
From 64e563a02c8cf22ca3163db3bc16f72861234738 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 14 Aug 2025 23:47:43 -0400 Subject: [PATCH 21/65] search --- .../src/components/AndroidHomescreen/components/HomeScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index 75e206977..dd60eed6a 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -620,7 +620,7 @@ export const HomeScreen: React.FC = () => {
-
+
0, + 'grid-cols-2': filteredApps.length === 0, + })}> {filteredApps.map(app => (
openApp(app)}> @@ -65,7 +71,7 @@ export const AppDrawer: React.FC = () => { ))} {filteredApps.length === 0 && (
No installed apps found. { -
+ Search apps... -
+
} From e18cb1f287324ce0f0229b67f9043e7179731eb8 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Fri, 15 Aug 2025 14:51:18 -0400 Subject: [PATCH 23/65] remember-omnibutton --- .../components/GestureZone.tsx | 31 +++++++++---------- .../ui/src/stores/persistenceStore.ts | 5 ++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index dd297ce2b..ba64cc8b5 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useState, useRef } from 'react'; import { useNavigationStore } from '../../../stores/navigationStore'; import classNames from 'classnames'; -import { BsClock } from 'react-icons/bs'; - +import { usePersistenceStore } from '../../../stores/persistenceStore'; export const GestureZone: React.FC = () => { const { toggleRecentApps, isRecentAppsOpen } = useNavigationStore(); - const [position, setPosition] = useState({ x: window.innerWidth - 80, y: 80 }); + const { omnibuttonPosition, setOmnibuttonPosition } = usePersistenceStore(); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps @@ -19,8 +18,8 @@ export const GestureZone: React.FC = () => { setDragStart({ x: touch.clientX, y: touch.clientY, - buttonX: position.x, - buttonY: position.y + buttonX: omnibuttonPosition.x, + buttonY: omnibuttonPosition.y }); }; @@ -41,7 +40,7 @@ export const GestureZone: React.FC = () => { if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); - setPosition({ x: newX, y: newY }); + setOmnibuttonPosition({ x: newX, y: newY }); } }; @@ -61,8 +60,8 @@ export const GestureZone: React.FC = () => { setDragStart({ x: e.clientX, y: e.clientY, - buttonX: position.x, - buttonY: position.y + buttonX: omnibuttonPosition.x, + buttonY: omnibuttonPosition.y }); }; @@ -81,7 +80,7 @@ export const GestureZone: React.FC = () => { if (isDragging || Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) { const newX = Math.max(30, Math.min(window.innerWidth - 30, dragStart.buttonX + deltaX)); const newY = Math.max(30, Math.min(window.innerHeight - 30, dragStart.buttonY + deltaY)); - setPosition({ x: newX, y: newY }); + setOmnibuttonPosition({ x: newX, y: newY }); } }; @@ -109,15 +108,15 @@ export const GestureZone: React.FC = () => { // Handle window resize to keep button in bounds useEffect(() => { const handleResize = () => { - setPosition(prev => ({ - x: Math.max(30, Math.min(window.innerWidth - 30, prev.x)), - y: Math.max(30, Math.min(window.innerHeight - 30, prev.y)) - })); + setOmnibuttonPosition({ + x: Math.max(30, Math.min(window.innerWidth - 30, omnibuttonPosition.x)), + y: Math.max(30, Math.min(window.innerHeight - 30, omnibuttonPosition.y)) + }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); - }, []); + }, [omnibuttonPosition]); useEffect(() => { if (!isRecentAppsOpen) { @@ -138,8 +137,8 @@ export const GestureZone: React.FC = () => { } )} style={{ - left: position.x - 30, - top: position.y - 30, + left: omnibuttonPosition.x - 30, + top: omnibuttonPosition.y - 30, transform: 'translate(0, 0)' // Prevent transform conflicts }} onTouchStart={handleTouchStart} diff --git a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts index 6f9a4ac6e..9fd208558 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/persistenceStore.ts @@ -22,6 +22,8 @@ interface PersistenceStore { setIsInitialized: (isInitialized: boolean) => void; doNotShowOnboardingAgain: boolean; setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain: boolean) => void; + omnibuttonPosition: Position; + setOmnibuttonPosition: (position: Position) => void; } export const usePersistenceStore = create()( @@ -35,7 +37,8 @@ export const usePersistenceStore = create()( backgroundImage: null, doNotShowOnboardingAgain: false, setDoNotShowOnboardingAgain: (doNotShowOnboardingAgain) => set({ doNotShowOnboardingAgain }), - + omnibuttonPosition: { x: window.innerWidth - 80, y: 80 }, + setOmnibuttonPosition: (position) => set({ omnibuttonPosition: position }), setIsInitialized: (isInitialized) => set({ isInitialized }), addToHomeScreen: (appId) => { From 1dc2b30cc720ac6e5543acf2385590d05b60fefa Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Fri, 15 Aug 2025 15:22:14 -0400 Subject: [PATCH 24/65] recent-apps-behavior --- .../components/AppDrawer.tsx | 35 +++-- .../components/HomeScreen.tsx | 2 +- .../components/RecentApps.tsx | 139 ++++++++++++++---- 3 files changed, 130 insertions(+), 46 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx index 894db42c5..50c7321a6 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx @@ -29,9 +29,14 @@ export const AppDrawer: React.FC = () => { if (!isAppDrawerOpen) return null; + const isMobile = window.innerWidth < 768; + return ( -
-
+
+

My Apps

@@ -40,8 +45,8 @@ export const AppDrawer: React.FC = () => { placeholder="Search apps..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - className="grow self-stretch !bg-transparent p-0" - autoFocus + className="grow self-stretch !bg-transparent !p-0" + autoFocus={!isMobile} />
@@ -55,8 +60,15 @@ export const AppDrawer: React.FC = () => { 'grid-cols-2': filteredApps.length === 0, })}> {filteredApps.map(app => ( -
-
openApp(app)}> +
+
{ + e.stopPropagation(); + openApp(app); + }}>
{!homeScreenApps.includes(app.id) && ( @@ -77,7 +89,8 @@ export const AppDrawer: React.FC = () => { { + onClick={(e) => { + e.stopPropagation(); setSearchQuery('') openApp(apps.find(a => a.id === 'main:app-store:sys')!, `?search=${searchQuery}`) }} @@ -88,14 +101,6 @@ export const AppDrawer: React.FC = () => { )}
- -
); }; \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx index b5d79293c..52eb7b138 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx @@ -614,7 +614,7 @@ export const HomeScreen: React.FC = () => { */}
) : ( <>
- {runningApps.map(app => ( + {runningApps.map(app => { + const swipeState = swipeStates[app.id] || { translateX: 0, opacity: 1, isDragging: false }; + + return (
{ + if (swipeState.isDragging) return; try { e.stopPropagation(); } catch { } try { e.preventDefault(); } catch { } switchToApp(app.id); }} + onTouchStart={(e) => handleTouchStart(e, app.id)} + onTouchMove={(e) => handleTouchMove(e, app.id)} + onTouchEnd={(e) => handleTouchEnd(e, app.id)} >
{app.label} @@ -65,9 +161,6 @@ export const RecentApps: React.FC = () => {
- {/*
-

App Preview

*/} - {app.base64_icon ? ( {app.label} ) : ( @@ -78,24 +171,10 @@ export const RecentApps: React.FC = () => {

opened {dayjs(runningApps.find(a => a.id === app.id)?.openedAt || 0).fromNow()}

- ))} + ); + })}
- -
- - -
)}
From 3bd27117fc292e81570f2cf93e2f0844e615dec3 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Fri, 15 Aug 2025 15:39:47 -0400 Subject: [PATCH 25/65] enable-homing-from-button --- .../AndroidHomescreen/components/GestureZone.tsx | 7 ++++--- .../components/AndroidHomescreen/components/RecentApps.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx index ba64cc8b5..5592a3020 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx @@ -3,7 +3,7 @@ import { useNavigationStore } from '../../../stores/navigationStore'; import classNames from 'classnames'; import { usePersistenceStore } from '../../../stores/persistenceStore'; export const GestureZone: React.FC = () => { - const { toggleRecentApps, isRecentAppsOpen } = useNavigationStore(); + const { toggleRecentApps, isRecentAppsOpen, closeAllOverlays } = useNavigationStore(); const { omnibuttonPosition, setOmnibuttonPosition } = usePersistenceStore(); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); @@ -87,7 +87,8 @@ export const GestureZone: React.FC = () => { const handleMouseUp = () => { if (isMobile) return; if (!isDragging && dragStart) { - toggleRecentApps(); + if (!isRecentAppsOpen) toggleRecentApps(); + else closeAllOverlays(); } setDragStart(null); setIsDragging(false); @@ -147,7 +148,7 @@ export const GestureZone: React.FC = () => { onMouseDown={handleMouseDown} > {/* Black rounded square background */} -
+
{/* White circle with icon */}
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx index 81336335b..255b158a3 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx @@ -99,7 +99,7 @@ export const RecentApps: React.FC = () => { return (
{runningApps.length === 0 ? ( From d471cbda6c83746bc2a85ce04d7f4b8695333829 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Fri, 15 Aug 2025 15:44:32 -0400 Subject: [PATCH 26/65] omnibutton-rename --- .../components/{GestureZone.tsx => OmniButton.tsx} | 5 +++-- .../homepage/ui/src/components/AndroidHomescreen/index.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/{GestureZone.tsx => OmniButton.tsx} (97%) diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx similarity index 97% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx rename to hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx index 5592a3020..e1a03d3c6 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/GestureZone.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { useNavigationStore } from '../../../stores/navigationStore'; import classNames from 'classnames'; import { usePersistenceStore } from '../../../stores/persistenceStore'; -export const GestureZone: React.FC = () => { +export const OmniButton: React.FC = () => { const { toggleRecentApps, isRecentAppsOpen, closeAllOverlays } = useNavigationStore(); const { omnibuttonPosition, setOmnibuttonPosition } = usePersistenceStore(); const [isDragging, setIsDragging] = useState(false); @@ -47,7 +47,8 @@ export const GestureZone: React.FC = () => { const handleTouchEnd = () => { if (!isDragging && dragStart) { // Tap - open recent apps - toggleRecentApps(); + if (!isRecentAppsOpen) toggleRecentApps(); + else closeAllOverlays(); } setDragStart(null); setIsDragging(false); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx index c6cea9643..b04c91234 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx @@ -5,7 +5,7 @@ import { HomeScreen } from './components/HomeScreen'; import { AppContainer } from './components/AppContainer'; import { AppDrawer } from './components/AppDrawer'; import { RecentApps } from './components/RecentApps'; -import { GestureZone } from './components/GestureZone'; +import { OmniButton } from './components/OmniButton'; import PWAUpdateNotification from '../PWAUpdateNotification'; import PWAInstallPrompt from '../PWAInstallPrompt'; import './styles/animations.css'; @@ -123,7 +123,7 @@ export default function AndroidHomescreen() { - + From be0cc53b7d83b40f97b6e53e8c4b78f2bf60d387 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Mon, 18 Aug 2025 14:11:31 -0400 Subject: [PATCH 27/65] cross-app links --- .../packages/app-store/ui/package-lock.json | 23 +++ hyperdrive/packages/app-store/ui/package.json | 1 + hyperdrive/packages/app-store/ui/src/App.tsx | 4 + .../app-store/ui/src/components/AppCard.tsx | 22 ++- .../app-store/ui/src/pages/AppPage.tsx | 99 +++++++++---- .../app-store/ui/src/pages/StorePage.tsx | 131 +++--------------- .../packages/app-store/ui/src/store/index.ts | 28 +++- .../components/AppContainer.tsx | 0 .../components/AppDrawer.tsx | 51 +++---- .../components/AppIcon.tsx | 2 +- .../components/Draggable.tsx | 0 .../components/HomeScreen.tsx | 0 .../components/Modal.tsx | 0 .../components/OmniButton.tsx | 16 ++- .../components/RecentApps.tsx | 0 .../components/Widget.tsx | 0 .../{AndroidHomescreen => Home}/index.tsx | 63 ++++++++- .../styles/animations.css | 0 ...PWAInstallPrompt.tsx => InstallPrompt.tsx} | 4 +- ...otification.tsx => UpdateNotification.tsx} | 4 +- hyperdrive/packages/homepage/ui/src/main.tsx | 4 +- .../ui/src/pages/AndroidHomescreen.tsx | 1 - 22 files changed, 259 insertions(+), 194 deletions(-) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/AppContainer.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/AppDrawer.tsx (68%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/AppIcon.tsx (97%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/Draggable.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/HomeScreen.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/Modal.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/OmniButton.tsx (91%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/RecentApps.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/components/Widget.tsx (100%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/index.tsx (70%) rename hyperdrive/packages/homepage/ui/src/components/{AndroidHomescreen => Home}/styles/animations.css (100%) rename hyperdrive/packages/homepage/ui/src/components/{PWAInstallPrompt.tsx => InstallPrompt.tsx} (97%) rename hyperdrive/packages/homepage/ui/src/components/{PWAUpdateNotification.tsx => UpdateNotification.tsx} (95%) delete mode 100644 hyperdrive/packages/homepage/ui/src/pages/AndroidHomescreen.tsx diff --git a/hyperdrive/packages/app-store/ui/package-lock.json b/hyperdrive/packages/app-store/ui/package-lock.json index 5a15f72f3..18e3cc7c9 100644 --- a/hyperdrive/packages/app-store/ui/package-lock.json +++ b/hyperdrive/packages/app-store/ui/package-lock.json @@ -21,6 +21,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^6.21.3", + "react-toastify": "^11.0.5", "tailwindcss": "^4.1.11", "viem": "^2.15.1", "wagmi": "^2.10.3", @@ -7662,6 +7663,28 @@ } } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react-transition-state": { "version": "2.1.2", "license": "MIT", diff --git a/hyperdrive/packages/app-store/ui/package.json b/hyperdrive/packages/app-store/ui/package.json index 68f85a5a0..9426af84d 100644 --- a/hyperdrive/packages/app-store/ui/package.json +++ b/hyperdrive/packages/app-store/ui/package.json @@ -26,6 +26,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^6.21.3", + "react-toastify": "^11.0.5", "tailwindcss": "^4.1.11", "viem": "^2.15.1", "wagmi": "^2.10.3", diff --git a/hyperdrive/packages/app-store/ui/src/App.tsx b/hyperdrive/packages/app-store/ui/src/App.tsx index 553b80c36..735b3d621 100644 --- a/hyperdrive/packages/app-store/ui/src/App.tsx +++ b/hyperdrive/packages/app-store/ui/src/App.tsx @@ -9,6 +9,7 @@ import AppPage from "./pages/AppPage"; import DownloadPage from "./pages/DownloadPage"; import PublishPage from "./pages/PublishPage"; import MyAppsPage from "./pages/MyAppsPage"; +import { ToastContainer } from "react-toastify"; //@ts-ignore @@ -30,6 +31,9 @@ function App() { } /> +
); } diff --git a/hyperdrive/packages/app-store/ui/src/components/AppCard.tsx b/hyperdrive/packages/app-store/ui/src/components/AppCard.tsx index 74155fd3e..9c90e2756 100644 --- a/hyperdrive/packages/app-store/ui/src/components/AppCard.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/AppCard.tsx @@ -15,6 +15,16 @@ export const AppCard: React.FC<{ if (!app || !app.package_id) return null; const navigate = useNavigate(); + const handleCardClick = (e: React.MouseEvent) => { + // Check if the click was on a button (ActionChip with onClick) + const target = e.target as HTMLElement; + const clickedButton = target.closest('[data-action-button="true"]'); + + if (!clickedButton) { + navigate(`/app/${app.package_id.package_name}:${app.package_id.publisher_node}`); + } + }; + return (
{ - navigate(`/app/${app.package_id.package_name}:${app.package_id.publisher_node}`); - }} + onClick={handleCardClick} >
{app.metadata?.image && } {!app.metadata?.image &&
} + className="w-1/5 min-w-1/5 md:w-1/4 md:min-w-1/4 object-cover rounded-xl aspect-square bg-iris dark:bg-neon flex items-center justify-center" + > + + {app.package_id.package_name.charAt(0).toUpperCase() + (app.package_id.package_name.charAt(1) || '').toLowerCase()} + +
}

{app.metadata?.name || app.package_id.package_name} diff --git a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx index 0ff853ebc..9bc753592 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useCallback, useMemo } from "react"; -import { useParams } from "react-router-dom"; +import { useParams, useLocation } from "react-router-dom"; import useAppsStore from "../store"; import { AppListing, PackageState, ManifestResponse } from "../types/Apps"; import { compareVersions } from "../utils/compareVersions"; @@ -36,10 +36,9 @@ const MOCK_APP: AppListing = { auto_update: false }; -const isMobile = window.innerWidth < 768; - export default function AppPage() { const { id } = useParams(); + const location = useLocation(); const { fetchListing, fetchInstalledApp, @@ -53,7 +52,9 @@ export default function AppPage() { activeDownloads, installApp, clearAllActiveDownloads, - checkMirrors + checkMirrors, + navigateToApp, + addNotification, } = useAppsStore(); const [app, setApp] = useState(null); @@ -243,7 +244,13 @@ export default function AppPage() { } } catch (error) { console.error('Installation flow failed:', error); - setError('Installation failed. Please try again.'); + const errorString = Object.keys(error).length > 0 ? `: ${JSON.stringify(error).slice(0, 100)}...` : ''; + addNotification({ + id: `installation-flow-failed-${id}-${selectedVersion}`, + timestamp: Date.now(), + type: 'error', + message: `Installation flow failed${errorString ? ': ' + errorString : ''}`, + }); } }, [id, selectedMirror, app, selectedVersion, sortedVersions, downloadApp, fetchDownloadsForApp]); @@ -268,19 +275,16 @@ export default function AppPage() { }, 3000); } catch (error) { console.error('Installation failed:', error); - setError('Installation failed. Please try again.'); + const errorString = Object.keys(error).length > 0 ? `: ${JSON.stringify(error).slice(0, 100)}...` : ''; + addNotification({ + id: `installation-failed-${id}-${selectedVersion}`, + timestamp: Date.now(), + type: 'error', + message: `Installation failed${errorString ? ': ' + errorString : ''}`, + }); } }, [id, selectedVersion, app, sortedVersions, installApp, fetchHomepageApps, loadData]); - const handleLaunch = useCallback(() => { - if (!app) return; - const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`); - if (launchUrl) { - window.location.href = window.location.origin.replace('//app-store-sys.', '//') + launchUrl; - } - }, [app, getLaunchUrl]); - - const handleUninstall = async () => { if (!app) return; setIsUninstalling(true); @@ -289,7 +293,13 @@ export default function AppPage() { await loadData(); } catch (error) { console.error('Uninstallation failed:', error); - setError(`Uninstallation failed: ${error instanceof Error ? error.message : String(error)}`); + const errorString = Object.keys(error).length > 0 ? `: ${JSON.stringify(error).slice(0, 100)}...` : ''; + addNotification({ + id: `uninstallation-failed-${id}`, + timestamp: Date.now(), + type: 'error', + message: `Uninstallation failed${errorString ? ': ' + errorString : ''}`, + }); } finally { setIsUninstalling(false); window.location.reload(); @@ -308,7 +318,13 @@ export default function AppPage() { await loadData(); } catch (error) { console.error('Failed to toggle auto-update:', error); - setError(`Failed to toggle auto-update: ${error instanceof Error ? error.message : String(error)}`); + const errorString = Object.keys(error).length > 0 ? `: ${JSON.stringify(error).slice(0, 100)}...` : ''; + addNotification({ + id: `auto-update-failed-${id}-${latestVersion}`, + timestamp: Date.now(), + type: 'error', + message: `Failed to toggle auto-update${errorString ? ': ' + errorString : ''}`, + }); } finally { setIsTogglingAutoUpdate(false); } @@ -339,6 +355,37 @@ export default function AppPage() { window.scrollTo(0, 0); }, [loadData, clearAllActiveDownloads]); + // Handle intent parameter from URL + useEffect(() => { + const searchParams = new URLSearchParams(location.search); + const intent = searchParams.get('intent'); + + if (!intent || !app || !id) return; + + // Auto-trigger actions based on intent + if (intent === 'launch' && canLaunch) { + // Automatically launch the app + setTimeout(() => { + navigateToApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`); + }, 500); // Small delay to ensure UI is ready + } else if (intent === 'install' && !installedApp) { + // Automatically trigger install modal + setTimeout(() => { + if (!isDownloaded) { + // Need to download first + if (!selectedMirror || isMirrorOnline === null) { + setAttemptedDownload(true); + } else { + handleInstallFlow(true); + } + } else { + // Already downloaded, just install + handleInstallFlow(false); + } + }, 500); // Small delay to ensure UI is ready + } + }, [location.search, app, id, canLaunch, installedApp, isDownloaded, selectedMirror, isMirrorOnline, handleInstallFlow]); + if (isLoading) { return (

@@ -348,17 +395,6 @@ export default function AppPage() {
); } - - if (error) { - return ( -
-
-

{error}

-
-
- ); - } - if (!app) { return (
@@ -394,7 +430,7 @@ export default function AppPage() { {(canLaunch || isDevMode) && (
)} + {error &&
+

{error.slice(0, 100)}...

+
}
{app.metadata?.image && } {!app.metadata?.image &&
- + {app.package_id.package_name.charAt(0).toUpperCase() + (app.package_id.package_name.charAt(1) || '').toLowerCase()}
} diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index aa873d547..81bbb0633 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -6,105 +6,19 @@ import { ResetButton } from "../components"; import { AppCard } from "../components/AppCard"; import { BsSearch } from "react-icons/bs"; import classNames from "classnames"; -import { useLocation } from "react-router-dom"; -const mockApps: AppListing[] = [ - { - package_id: { - package_name: "test-app", - publisher_node: "test-node", - }, - tba: "0x0000000000000000000000000000000000000000", - metadata_uri: "https://example.com/metadata", - metadata_hash: "1234567890", - auto_update: false, - metadata: { - name: "Test App", - description: "This is a test app", - properties: { - package_name: "test-app", - publisher: "test-node", - current_version: "1.0.0", - mirrors: [], - code_hashes: [], - }, - }, - }, - { - package_id: { - package_name: "test-app", - publisher_node: "test-node", - }, - tba: "0x0000000000000000000000000000000000000000", - metadata_uri: "https://example.com/metadata", - metadata_hash: "1234567890", - auto_update: false, - metadata: { - name: "Test App", - description: "This is a test app", - properties: { - package_name: "test-app", - publisher: "test-node", - current_version: "1.0.0", - mirrors: [], - code_hashes: [], - }, - }, - }, - { - package_id: { - package_name: "test-app", - publisher_node: "test-node", - }, - tba: "0x0000000000000000000000000000000000000000", - metadata_uri: "https://example.com/metadata", - metadata_hash: "1234567890", - auto_update: false, - metadata: { - name: "Test App TestappTestappTestappTestappTestapp", - description: "adsf adf adsf asdf asdf adgfagafege aadsf adf adsf asdf asdf adgfagafege aadsf adf adsf asdf asdf adgfagafege aadsf adf adsf asdf asdf adgfagafege aadsf adf adsf asdf asdf adgfagafege aadsf adf adsf asdf asdf adgfagafege a", - properties: { - package_name: "test-app", - publisher: "test-node", - current_version: "1.0.0", - mirrors: [], - code_hashes: [], - }, - }, - }, - { - package_id: { - package_name: "test-app", - publisher_node: "test-node", - }, - tba: "0x0000000000000000000000000000000000000000", - metadata_uri: "https://example.com/metadata", - metadata_hash: "1234567890", - auto_update: false, - metadata: { - name: "Test App", - description: "This is a test app", - properties: { - package_name: "test-app", - publisher: "test-nodetest-nodetest-nodetest-nodetest-node", - current_version: "1.0.0", - mirrors: [], - code_hashes: [], - }, - }, - }, -]; +import { useLocation, useNavigate } from "react-router-dom"; export default function StorePage() { - const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, fetchHomepageApps, getLaunchUrl } = useAppsStore(); + const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, fetchHomepageApps, getLaunchUrl, navigateToApp } = useAppsStore(); const [searchQuery, setSearchQuery] = useState(""); const [launchableApps, setLaunchableApps] = useState([]); const [appsNotInstalled, setAppsNotInstalled] = useState([]); - const [isDevMode, setIsDevMode] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); // if we have ?search=something, set the search query to that const location = useLocation(); + const navigate = useNavigate(); useEffect(() => { console.log({ location }) const search = new URLSearchParams(location.search).get("search"); @@ -125,12 +39,6 @@ export default function StorePage() { setCurrentPage(Math.ceil(filteredApps.length / newPageSize)); } - useEffect(() => { - if (searchQuery.match(/``````/)) { - setIsDevMode(!isDevMode); - } - }, [searchQuery]); - useEffect(() => { fetchListings(); fetchInstalled(); @@ -177,20 +85,6 @@ export default function StorePage() { />
- - {isDevMode &&
- {mockApps.map((app) => ( - - - - - ))} -
} {!listings ? (

Loading...

) : filteredApps.length === 0 ? ( @@ -207,9 +101,15 @@ export default function StorePage() { app={app} > {appsNotInstalled.includes(app) - ? + ? navigate(`/app/${app.package_id.package_name}:${app.package_id.publisher_node}?intent=install`)} + /> : launchableApps.includes(app) - ? + ? navigateToApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`)} + /> : } ))} @@ -257,8 +157,13 @@ export default function StorePage() { const ActionChip: React.FC<{ label: string; className?: string; -}> = ({ label, className }) => { + onClick?: () => void; +}> = ({ label, className, onClick }) => { return
{label} + onClick={onClick} + data-action-button={!!onClick} + className={classNames("bg-iris/10 text-iris dark:bg-black dark:text-neon font-bold px-3 py-1 rounded-full flex items-center gap-2", { + 'cursor-pointer hover:opacity-80': onClick, + }, className)}>{label}
} diff --git a/hyperdrive/packages/app-store/ui/src/store/index.ts b/hyperdrive/packages/app-store/ui/src/store/index.ts index 0d8a10c0f..c3a68291c 100644 --- a/hyperdrive/packages/app-store/ui/src/store/index.ts +++ b/hyperdrive/packages/app-store/ui/src/store/index.ts @@ -4,6 +4,7 @@ import { PackageState, AppListing, MirrorCheckFile, DownloadItem, HomepageApp, M import { HTTP_STATUS } from '../constants/http' import HyperwareClientApi from "@hyperware-ai/client-api" import { WEBSOCKET_URL } from '../utils/ws' +import { toast } from 'react-toastify'; const BASE_URL = '/main:app-store:sys' @@ -56,6 +57,10 @@ interface AppsStore { checkMirrors: (packageId: string, onMirrorSelect: (mirror: string, status: boolean | null | 'http') => void) => Promise<{ mirror: string, status: boolean | null | 'http', mirrors: string[] } | { error: string, mirrors: string[] }> setShowPublicAppStore: (show: boolean) => Promise fetchPublicAppStoreStatus: () => Promise + + navigateToApp: (id: string) => void + + addToast: (message: string, status: 'success' | 'error' | 'info' | 'warning') => void; } const useAppsStore = create()((set, get) => ({ @@ -369,10 +374,20 @@ const useAppsStore = create()((set, get) => ({ })); }, + addToast: (message: string, status: 'success' | 'error' | 'info' | 'warning') => { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + toast[status](message, { + toastId: message, + theme: isDark ? 'dark' : 'light', + }); + }, - addNotification: (notification) => set(state => ({ - notifications: [...state.notifications, notification] - })), + addNotification: (notification) => { + set(state => ({ + notifications: [...state.notifications, notification] + })); + get().addToast(notification.message, notification.type === 'error' ? 'error' : 'info'); + }, removeNotification: (id) => set(state => ({ notifications: state.notifications.filter(n => n.id !== id) @@ -457,6 +472,13 @@ const useAppsStore = create()((set, get) => ({ } }, + navigateToApp: (id: string) => { + window.parent.postMessage({ + type: 'OPEN_APP', + id + }, '*'); + }, + resetStore: async () => { try { const response = await fetch(`${BASE_URL}/reset`, { diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppContainer.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/AppContainer.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppContainer.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/AppContainer.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/AppDrawer.tsx similarity index 68% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/AppDrawer.tsx index 50c7321a6..6099ae1af 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppDrawer.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/AppDrawer.tsx @@ -33,8 +33,8 @@ export const AppDrawer: React.FC = () => { return (

My Apps

@@ -56,17 +56,20 @@ export const AppDrawer: React.FC = () => { grid gap-4 md:gap-6 lg:gap-8 `, { - 'grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6': filteredApps.length > 0, - 'grid-cols-2': filteredApps.length === 0, - })}> + 'grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6': filteredApps.length > 0, + 'grid-cols-2': filteredApps.length === 0, + })}> {filteredApps.map(app => (
{ e.stopPropagation(); + if (app.path === null) { + return; + } openApp(app); }}> @@ -82,23 +85,23 @@ export const AppDrawer: React.FC = () => {
))} {filteredApps.length === 0 && ( -
+ No installed apps found. + { + e.stopPropagation(); + setSearchQuery('') + openApp(apps.find(a => a.id === 'main:app-store:sys')!, `?search=${searchQuery}`) + }} > - No installed apps found. - { - e.stopPropagation(); - setSearchQuery('') - openApp(apps.find(a => a.id === 'main:app-store:sys')!, `?search=${searchQuery}`) - }} - > - Search the app store - -
- )} + Search the app store + +
+ )}
diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/AppIcon.tsx similarity index 97% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/AppIcon.tsx index 5b4d71d59..40263e495 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/AppIcon.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/AppIcon.tsx @@ -20,7 +20,7 @@ export const AppIcon: React.FC = ({ const [isPressed, setIsPressed] = useState(false); const handlePress = () => { - if (!isEditMode && app.path) { + if (!isEditMode && app.path && app.path !== null) { openApp(app); } }; diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Draggable.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/Draggable.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Draggable.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/Draggable.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/HomeScreen.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/Modal.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Modal.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/Modal.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/OmniButton.tsx similarity index 91% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/OmniButton.tsx index e1a03d3c6..59b12badd 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/OmniButton.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/OmniButton.tsx @@ -9,10 +9,12 @@ export const OmniButton: React.FC = () => { const [dragStart, setDragStart] = useState<{ x: number; y: number; buttonX: number; buttonY: number } | null>(null); const dragThreshold = 5; // pixels - swipes smaller than this will be treated as taps const buttonRef = useRef(null); - const isMobile = window.innerWidth < 768; + const isMobile = () => window.innerWidth < 768; // Touch handlers for drag and tap const handleTouchStart = (e: React.TouchEvent) => { + if (!isMobile()) return; + console.log('omnibutton handleTouchStart', e); e.stopPropagation(); const touch = e.touches[0]; setDragStart({ @@ -24,6 +26,7 @@ export const OmniButton: React.FC = () => { }; const handleTouchMove = (e: React.TouchEvent) => { + console.log('omnibutton handleTouchMove', e); if (!dragStart) return; e.stopPropagation(); @@ -45,6 +48,8 @@ export const OmniButton: React.FC = () => { }; const handleTouchEnd = () => { + if (!isMobile()) return; + console.log('omnibutton handleTouchEnd'); if (!isDragging && dragStart) { // Tap - open recent apps if (!isRecentAppsOpen) toggleRecentApps(); @@ -56,7 +61,8 @@ export const OmniButton: React.FC = () => { // Mouse handlers for desktop const handleMouseDown = (e: React.MouseEvent) => { - if (isMobile) return; + if (isMobile()) return; + console.log('omnibutton handleMouseDown', e); e.stopPropagation(); setDragStart({ x: e.clientX, @@ -67,7 +73,8 @@ export const OmniButton: React.FC = () => { }; const handleMouseMove = (e: MouseEvent) => { - if (isMobile) return; + if (isMobile()) return; + console.log('omnibutton handleMouseMove', e); e.stopPropagation(); if (!dragStart) return; @@ -86,7 +93,8 @@ export const OmniButton: React.FC = () => { }; const handleMouseUp = () => { - if (isMobile) return; + if (isMobile()) return; + console.log('omnibutton handleMouseUp'); if (!isDragging && dragStart) { if (!isRecentAppsOpen) toggleRecentApps(); else closeAllOverlays(); diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/RecentApps.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/RecentApps.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/RecentApps.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/Widget.tsx similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/components/Widget.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/components/Widget.tsx diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx similarity index 70% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx rename to hyperdrive/packages/homepage/ui/src/components/Home/index.tsx index b04c91234..83a9a8133 100644 --- a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx @@ -6,15 +6,15 @@ import { AppContainer } from './components/AppContainer'; import { AppDrawer } from './components/AppDrawer'; import { RecentApps } from './components/RecentApps'; import { OmniButton } from './components/OmniButton'; -import PWAUpdateNotification from '../PWAUpdateNotification'; -import PWAInstallPrompt from '../PWAInstallPrompt'; +import UpdateNotification from '../UpdateNotification'; +import InstallPrompt from '../InstallPrompt'; import './styles/animations.css'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; dayjs.extend(relativeTime); -export default function AndroidHomescreen() { - const { setApps } = useAppStore(); +export default function Home() { + const { apps, setApps } = useAppStore(); const { runningApps, currentAppId, @@ -24,10 +24,59 @@ export default function AndroidHomescreen() { switchToApp, toggleAppDrawer, closeAllOverlays, - initBrowserBackHandling + initBrowserBackHandling, + openApp, } = useNavigationStore(); const [loading, setLoading] = useState(true); + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (!event?.data?.type) { + // ignore other iframe messages e.g. metamask + return; + } + let allGood = true; + // App Store is a good boy, he can send messages to us + const replaced = window.location.origin.replace(/^(https?:\/\/)/, '$1app-store-sys.') + if ( + replaced !== event.origin + ) { + console.log('expected same origin or app-store-sys, got:', event.origin, window.location.origin, replaced); + allGood = false; + } + if (event.data.type !== 'OPEN_APP') { + console.log('expected OPEN_APP, got:', event.data.type); + allGood = false; + } + if (!allGood) { + console.log('not all good', { event }); + return; + } + + if (event.data.type === 'OPEN_APP') { + const { id } = event.data; + const appMatches = apps.filter(app => app.id.endsWith(':' + id)); + if (appMatches.length > 1) { + console.error('Multiple apps found with the same id:', { id, apps }); + } else if (appMatches.length === 0) { + console.error('App not found:', { id, apps }); + } + const app = appMatches[0]; + if (app) { + openApp(app); + } else { + console.error('App not found:', { id, apps }); + } + } + }; + window.addEventListener('message', handleMessage); + + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [apps, openApp]); + + // Keyboard shortcuts for desktop useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { @@ -126,10 +175,10 @@ export default function AndroidHomescreen() { - + - +
); } diff --git a/hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/styles/animations.css b/hyperdrive/packages/homepage/ui/src/components/Home/styles/animations.css similarity index 100% rename from hyperdrive/packages/homepage/ui/src/components/AndroidHomescreen/styles/animations.css rename to hyperdrive/packages/homepage/ui/src/components/Home/styles/animations.css diff --git a/hyperdrive/packages/homepage/ui/src/components/PWAInstallPrompt.tsx b/hyperdrive/packages/homepage/ui/src/components/InstallPrompt.tsx similarity index 97% rename from hyperdrive/packages/homepage/ui/src/components/PWAInstallPrompt.tsx rename to hyperdrive/packages/homepage/ui/src/components/InstallPrompt.tsx index ab37f9c2d..f84213fbb 100644 --- a/hyperdrive/packages/homepage/ui/src/components/PWAInstallPrompt.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/InstallPrompt.tsx @@ -5,7 +5,7 @@ interface BeforeInstallPromptEvent extends Event { userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; } -const PWAInstallPrompt: React.FC = () => { +const InstallPrompt: React.FC = () => { const [installPrompt, setInstallPrompt] = useState(null); const [showPrompt, setShowPrompt] = useState(false); @@ -94,4 +94,4 @@ const PWAInstallPrompt: React.FC = () => { ); }; -export default PWAInstallPrompt; +export default InstallPrompt; diff --git a/hyperdrive/packages/homepage/ui/src/components/PWAUpdateNotification.tsx b/hyperdrive/packages/homepage/ui/src/components/UpdateNotification.tsx similarity index 95% rename from hyperdrive/packages/homepage/ui/src/components/PWAUpdateNotification.tsx rename to hyperdrive/packages/homepage/ui/src/components/UpdateNotification.tsx index 487309fa7..349f8c794 100644 --- a/hyperdrive/packages/homepage/ui/src/components/PWAUpdateNotification.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/UpdateNotification.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -const PWAUpdateNotification: React.FC = () => { +const UpdateNotification: React.FC = () => { const [showUpdate, setShowUpdate] = useState(false); const [registration, setRegistration] = useState(null); @@ -66,4 +66,4 @@ const PWAUpdateNotification: React.FC = () => { ); }; -export default PWAUpdateNotification; +export default UpdateNotification; diff --git a/hyperdrive/packages/homepage/ui/src/main.tsx b/hyperdrive/packages/homepage/ui/src/main.tsx index 30a1d8a7e..2fb308b8a 100644 --- a/hyperdrive/packages/homepage/ui/src/main.tsx +++ b/hyperdrive/packages/homepage/ui/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import AndroidHomescreen from './pages/AndroidHomescreen.tsx' +import Home from './components/Home' import './index.css' // Register service worker for PWA @@ -23,6 +23,6 @@ if ('serviceWorker' in navigator) { ReactDOM.createRoot(document.getElementById('root')!).render( - + , ) diff --git a/hyperdrive/packages/homepage/ui/src/pages/AndroidHomescreen.tsx b/hyperdrive/packages/homepage/ui/src/pages/AndroidHomescreen.tsx deleted file mode 100644 index 7f092af05..000000000 --- a/hyperdrive/packages/homepage/ui/src/pages/AndroidHomescreen.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../components/AndroidHomescreen'; \ No newline at end of file From 89eed98c8f28b30910bb0448341bae6d48246161 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Mon, 18 Aug 2025 15:24:58 -0400 Subject: [PATCH 28/65] leniency --- .../homepage/ui/src/components/Home/index.tsx | 61 ++++++++++++++++--- .../homepage/ui/src/stores/navigationStore.ts | 5 ++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx index 83a9a8133..90103f7d1 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx @@ -14,6 +14,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; dayjs.extend(relativeTime); export default function Home() { + const { apps, setApps } = useAppStore(); const { runningApps, @@ -35,21 +36,67 @@ export default function Home() { // ignore other iframe messages e.g. metamask return; } + let allGood = true; - // App Store is a good boy, he can send messages to us - const replaced = window.location.origin.replace(/^(https?:\/\/)/, '$1app-store-sys.') - if ( - replaced !== event.origin - ) { - console.log('expected same origin or app-store-sys, got:', event.origin, window.location.origin, replaced); + + const isValidOrigin = (() => { + const currentOrigin = window.location.origin; + const eventOrigin = event.origin; + + // Allow same origin (homepage calling itself) + if (eventOrigin === currentOrigin) { + return true; + } + + // App Store is a good boy, he can send messages to us + const appStoreOrigin = currentOrigin.replace(/^(https?:\/\/)/, '$1app-store-sys.'); + if (eventOrigin === appStoreOrigin) { + return true; + } + + // Allow other apps from same domain/host + // Pattern: https://[app-name]-[publisher].domain.com or http://[app-name]-[publisher].localhost:port + const currentUrl = new URL(currentOrigin); + const eventUrl = new URL(eventOrigin); + + // Must be same protocol and port + if (currentUrl.protocol !== eventUrl.protocol || currentUrl.port !== eventUrl.port) { + return false; + } + + // For localhost: allow any subdomain pattern + if (currentUrl.hostname.includes('localhost')) { + return eventUrl.hostname.endsWith('.localhost') || eventUrl.hostname === 'localhost'; + } + + // For production: allow subdomains of same base domain + const getCurrentBaseDomain = (hostname: string) => { + const parts = hostname.split('.'); + return parts.length >= 2 ? parts.slice(-2).join('.') : hostname; + }; + + const currentBaseDomain = getCurrentBaseDomain(currentUrl.hostname); + const eventBaseDomain = getCurrentBaseDomain(eventUrl.hostname); + + return currentBaseDomain === eventBaseDomain; + })(); + + if (!isValidOrigin) { + console.log('Invalid origin for OPEN_APP:', { + expected: window.location.origin, + got: event.origin, + type: event.data.type + }); allGood = false; } + if (event.data.type !== 'OPEN_APP') { console.log('expected OPEN_APP, got:', event.data.type); allGood = false; } + if (!allGood) { - console.log('not all good', { event }); + console.log('Message rejected:', { event }); return; } diff --git a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts index ac12dc79d..506299dd5 100644 --- a/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts +++ b/hyperdrive/packages/homepage/ui/src/stores/navigationStore.ts @@ -166,6 +166,11 @@ export const useNavigationStore = create((set, get) => ({ const { runningApps, currentAppId } = get(); const existingApp = runningApps.find(a => a.id === app.id); + if (existingApp && currentAppId === app.id) { + console.log('App already open:', { app }); + return; + } + // Add to browser history for back button support window?.history?.pushState( { type: 'app', appId: app.id, previousAppId: currentAppId }, From 40400b745e9ef3db62bccb4f81849b4b8946804f Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 20 Aug 2025 14:27:05 -0400 Subject: [PATCH 29/65] intent processing --- .../app-store/ui/src/pages/AppPage.tsx | 21 +++++++++++++++++-- .../homepage/ui/src/components/Home/index.tsx | 5 +++-- .../homepage/ui/src/types/messages.ts | 12 +++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 hyperdrive/packages/homepage/ui/src/types/messages.ts diff --git a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx index 9bc753592..0294375c5 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx @@ -80,6 +80,8 @@ export default function AppPage() { const [isDevMode, setIsDevMode] = useState(false); const [backtickPressCount, setBacktickPressCount] = useState(0); const [detailExpanded, setDetailExpanded] = useState(false); + const [hasProcessedIntent, setHasProcessedIntent] = useState(false); + useEffect(() => { const backTickCounter = (e: KeyboardEvent) => { if (e.key === '`') { @@ -353,6 +355,8 @@ export default function AppPage() { loadData(); clearAllActiveDownloads(); window.scrollTo(0, 0); + // Reset intent processing flag when navigating to a different app + setHasProcessedIntent(false); }, [loadData, clearAllActiveDownloads]); // Handle intent parameter from URL @@ -360,7 +364,20 @@ export default function AppPage() { const searchParams = new URLSearchParams(location.search); const intent = searchParams.get('intent'); - if (!intent || !app || !id) return; + if (!intent || !app || !id) { + setHasProcessedIntent(true); + return; + } + + if (hasProcessedIntent) return; + + // For install intent, ensure all required data is loaded before proceeding + if (intent === 'install' && !installedApp) { + // Wait for selectedVersion to be set (indicates app data is fully loaded) + if (!selectedVersion || isLoading) return; + } + + setHasProcessedIntent(true); // Auto-trigger actions based on intent if (intent === 'launch' && canLaunch) { @@ -384,7 +401,7 @@ export default function AppPage() { } }, 500); // Small delay to ensure UI is ready } - }, [location.search, app, id, canLaunch, installedApp, isDownloaded, selectedMirror, isMirrorOnline, handleInstallFlow]); + }, [location.search, app, id, canLaunch, installedApp, isDownloaded, selectedMirror, isMirrorOnline, handleInstallFlow, hasProcessedIntent, selectedVersion, isLoading]); if (isLoading) { return ( diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx index 90103f7d1..77cd48ba9 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx @@ -11,6 +11,7 @@ import InstallPrompt from '../InstallPrompt'; import './styles/animations.css'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; +import { isIframeMessage } from '../../types/messages'; dayjs.extend(relativeTime); export default function Home() { @@ -32,8 +33,8 @@ export default function Home() { useEffect(() => { const handleMessage = (event: MessageEvent) => { - if (!event?.data?.type) { - // ignore other iframe messages e.g. metamask + if (!isIframeMessage(event.data)) { + // ignore other iframe messages e.g. metamask return; } diff --git a/hyperdrive/packages/homepage/ui/src/types/messages.ts b/hyperdrive/packages/homepage/ui/src/types/messages.ts new file mode 100644 index 000000000..a9519e59f --- /dev/null +++ b/hyperdrive/packages/homepage/ui/src/types/messages.ts @@ -0,0 +1,12 @@ +export enum IframeMessageType { + OPEN_APP = 'OPEN_APP', + APP_LINK_CLICKED = 'APP_LINK_CLICKED' +} + +export type IframeMessage = + | { type: IframeMessageType.OPEN_APP, id: string } + | { type: IframeMessageType.APP_LINK_CLICKED, url: string } + +export function isIframeMessage(message: any): message is IframeMessage { + return message.type in IframeMessageType; +} \ No newline at end of file From 24f8d046473260c0e87c34ac46ba026abab31f0d Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 20 Aug 2025 22:37:29 -0400 Subject: [PATCH 30/65] iframe messaging; install flow --- .../app-store/ui/src/components/Header.tsx | 2 +- .../ui/src/components/MirrorSelector.tsx | 2 +- .../ui/src/components/NotificationBay.tsx | 2 +- .../ui/src/components/PackageSelector.tsx | 2 +- .../ui/src/components/ResetButton.tsx | 2 +- .../app-store/ui/src/pages/AppPage.tsx | 27 ++++++++++++++++--- .../app-store/ui/src/pages/DownloadPage.tsx | 2 +- .../app-store/ui/src/pages/MyAppsPage.tsx | 2 +- .../app-store/ui/src/pages/PublishPage.tsx | 2 +- .../app-store/ui/src/pages/StorePage.tsx | 19 +++++++------ .../src/store/{index.ts => appStoreStore.ts} | 23 +++++++++++++--- .../app-store/ui/src/types/messages.ts | 12 +++++++++ .../homepage/ui/src/components/Home/index.tsx | 7 ++--- 13 files changed, 77 insertions(+), 27 deletions(-) rename hyperdrive/packages/app-store/ui/src/store/{index.ts => appStoreStore.ts} (96%) create mode 100644 hyperdrive/packages/app-store/ui/src/types/messages.ts diff --git a/hyperdrive/packages/app-store/ui/src/components/Header.tsx b/hyperdrive/packages/app-store/ui/src/components/Header.tsx index 785da8e90..0c7d2a17f 100644 --- a/hyperdrive/packages/app-store/ui/src/components/Header.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/Header.tsx @@ -3,7 +3,7 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'; import { STORE_PATH, PUBLISH_PATH, MY_APPS_PATH, APP_PAGE_PATH, DOWNLOAD_PATH } from '../constants/path'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import NotificationBay from './NotificationBay'; -import useAppsStore from '../store'; +import useAppsStore from '../store/appStoreStore'; import classNames from 'classnames'; import { BsLightning, BsLayers, BsCloudArrowUp } from 'react-icons/bs'; const Header: React.FC = () => { diff --git a/hyperdrive/packages/app-store/ui/src/components/MirrorSelector.tsx b/hyperdrive/packages/app-store/ui/src/components/MirrorSelector.tsx index a84fa11fd..4a3c191ed 100644 --- a/hyperdrive/packages/app-store/ui/src/components/MirrorSelector.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/MirrorSelector.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; interface MirrorSelectorProps { packageId: string | undefined; diff --git a/hyperdrive/packages/app-store/ui/src/components/NotificationBay.tsx b/hyperdrive/packages/app-store/ui/src/components/NotificationBay.tsx index 97c26671a..6295c712c 100644 --- a/hyperdrive/packages/app-store/ui/src/components/NotificationBay.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/NotificationBay.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useState } from 'react'; import { FaBell, FaChevronDown, FaChevronUp, FaTrash, FaTimes } from 'react-icons/fa'; -import useAppsStore from '../store'; +import useAppsStore from '../store/appStoreStore'; import { Notification, NotificationAction } from '../types/Apps'; import { useNavigate } from 'react-router-dom'; import classNames from 'classnames'; diff --git a/hyperdrive/packages/app-store/ui/src/components/PackageSelector.tsx b/hyperdrive/packages/app-store/ui/src/components/PackageSelector.tsx index 647291e59..265b94e81 100644 --- a/hyperdrive/packages/app-store/ui/src/components/PackageSelector.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/PackageSelector.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo } from 'react'; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; interface PackageSelectorProps { onPackageSelect: (packageName: string, publisherId: string) => void; diff --git a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx index 0984e256e..9c8ff2844 100644 --- a/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx +++ b/hyperdrive/packages/app-store/ui/src/components/ResetButton.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { FaExclamationTriangle } from 'react-icons/fa'; -import useAppsStore from '../store'; +import useAppsStore from '../store/appStoreStore'; import { BsArrowClockwise } from 'react-icons/bs'; import { Modal } from './Modal'; import classNames from 'classnames' diff --git a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx index 0294375c5..929bb42b1 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback, useMemo } from "react"; import { useParams, useLocation } from "react-router-dom"; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; import { AppListing, PackageState, ManifestResponse } from "../types/Apps"; import { compareVersions } from "../utils/compareVersions"; import { MirrorSelector, ManifestDisplay } from '../components'; @@ -231,7 +231,9 @@ export default function AppPage() { } const downloads = await fetchDownloadsForApp(id); + console.log({ downloads }); const download = downloads.find(d => d.File?.name === `${versionData.hash}.zip`); + console.log({ download }); if (download?.File?.manifest) { const manifest_response: ManifestResponse = { @@ -364,42 +366,59 @@ export default function AppPage() { const searchParams = new URLSearchParams(location.search); const intent = searchParams.get('intent'); + console.log({ intent, app, id }); + if (!intent || !app || !id) { - setHasProcessedIntent(true); + console.log('no intent or app or id; returning'); return; } - if (hasProcessedIntent) return; + if (hasProcessedIntent) { + console.log('has processed intent; returning'); + return; + } // For install intent, ensure all required data is loaded before proceeding if (intent === 'install' && !installedApp) { + console.log('install intent; waiting for selectedVersion to be set'); // Wait for selectedVersion to be set (indicates app data is fully loaded) - if (!selectedVersion || isLoading) return; + if (!selectedVersion || isLoading) { + console.log('selectedVersion or isLoading; returning'); + return; + } } + console.log('setting hasProcessedIntent to true'); setHasProcessedIntent(true); // Auto-trigger actions based on intent if (intent === 'launch' && canLaunch) { // Automatically launch the app + console.log('launch intent; navigating to app'); setTimeout(() => { navigateToApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`); }, 500); // Small delay to ensure UI is ready } else if (intent === 'install' && !installedApp) { // Automatically trigger install modal + console.log('install intent; triggering install modal'); setTimeout(() => { if (!isDownloaded) { // Need to download first if (!selectedMirror || isMirrorOnline === null) { + console.log('no selectedMirror or isMirrorOnline; setting attemptedDownload to true'); setAttemptedDownload(true); } else { + console.log('selectedMirror and isMirrorOnline; triggering install flow'); handleInstallFlow(true); } } else { // Already downloaded, just install + console.log('already downloaded; triggering install flow'); handleInstallFlow(false); } }, 500); // Small delay to ensure UI is ready + } else { + console.log('unknown intent; returning'); } }, [location.search, app, id, canLaunch, installedApp, isDownloaded, selectedMirror, isMirrorOnline, handleInstallFlow, hasProcessedIntent, selectedVersion, isLoading]); diff --git a/hyperdrive/packages/app-store/ui/src/pages/DownloadPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/DownloadPage.tsx index 1a55f3d08..f3050356e 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/DownloadPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/DownloadPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { useParams } from "react-router-dom"; import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa"; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; import { MirrorSelector, ManifestDisplay } from '../components'; import { ManifestResponse } from "../types/Apps"; import { Modal } from "../components/Modal"; diff --git a/hyperdrive/packages/app-store/ui/src/pages/MyAppsPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/MyAppsPage.tsx index cc13c60f6..7559cee24 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/MyAppsPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/MyAppsPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck, FaTrash, FaExclamationTriangle, FaTimesCircle, FaChevronDown, FaChevronRight } from "react-icons/fa"; import { useNavigate } from "react-router-dom"; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; import { ResetButton } from "../components"; import { DownloadItem, PackageManifestEntry, PackageState, Updates, DownloadError, UpdateInfo } from "../types/Apps"; import { BsTrash, BsX } from "react-icons/bs"; diff --git a/hyperdrive/packages/app-store/ui/src/pages/PublishPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/PublishPage.tsx index a5e0a981f..d1659cbce 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/PublishPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/PublishPage.tsx @@ -5,7 +5,7 @@ import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit'; import { keccak256, toBytes } from 'viem'; import { mechAbi, HYPERMAP, encodeIntoMintCall, encodeMulticalls, hypermapAbi, MULTICALL } from "../abis"; import { hyperhash } from '../utils/hyperhash'; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; import { PackageSelector } from "../components"; import { Tooltip } from '../components/Tooltip'; import { FaCircleNotch, FaInfo, FaWallet } from "react-icons/fa6"; diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index 81bbb0633..f86bfc661 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import useAppsStore from "../store"; +import useAppsStore from "../store/appStoreStore"; import { AppListing } from "../types/Apps"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; import { ResetButton } from "../components"; @@ -9,9 +9,8 @@ import classNames from "classnames"; import { useLocation, useNavigate } from "react-router-dom"; export default function StorePage() { - const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, fetchHomepageApps, getLaunchUrl, navigateToApp } = useAppsStore(); + const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, homepageApps, fetchHomepageApps, getLaunchUrl, navigateToApp } = useAppsStore(); const [searchQuery, setSearchQuery] = useState(""); - const [launchableApps, setLaunchableApps] = useState([]); const [appsNotInstalled, setAppsNotInstalled] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -29,7 +28,6 @@ export default function StorePage() { }, [location]); const onInputChange = (e: React.ChangeEvent) => { - console.log(e.target.value, searchQuery); setSearchQuery(e.target.value); } @@ -48,17 +46,16 @@ export default function StorePage() { useEffect(() => { if (listings) { - setLaunchableApps(Object.values(listings).filter((app) => getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`))); - // Check if app is installed by looking in the installed state const notInstalledApps = Object.values(listings).filter((app) => { const appId = `${app.package_id.package_name}:${app.package_id.publisher_node}`; + console.log({ appId, installed, listings }); return !installed[appId]; }); console.log({ notInstalledApps, installedKeys: Object.keys(installed) }); setAppsNotInstalled(notInstalledApps); } - }, [listings, installed, getLaunchUrl]); + }, [listings, installed]); // extensive temp null handling due to weird prod bug const filteredApps = React.useMemo(() => { @@ -105,7 +102,13 @@ export default function StorePage() { label="Install" onClick={() => navigate(`/app/${app.package_id.package_name}:${app.package_id.publisher_node}?intent=install`)} /> - : launchableApps.includes(app) + : homepageApps.find((hpa) => { + const appId = `${app.package_id.package_name}:${app.package_id.publisher_node}`; + const hpaId = hpa.id; + const path = hpa.path || ''; + console.log({ appId, hpaId, path }); + return hpaId.endsWith(appId) && path + }) ? navigateToApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`)} diff --git a/hyperdrive/packages/app-store/ui/src/store/index.ts b/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts similarity index 96% rename from hyperdrive/packages/app-store/ui/src/store/index.ts rename to hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts index c3a68291c..86c5dc7da 100644 --- a/hyperdrive/packages/app-store/ui/src/store/index.ts +++ b/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts @@ -5,6 +5,7 @@ import { HTTP_STATUS } from '../constants/http' import HyperwareClientApi from "@hyperware-ai/client-api" import { WEBSOCKET_URL } from '../utils/ws' import { toast } from 'react-toastify'; +import { IframeMessageType } from '../types/messages' const BASE_URL = '/main:app-store:sys' @@ -133,6 +134,7 @@ const useAppsStore = create()((set, get) => ({ acc[`${pkg.package_id.package_name}:${pkg.package_id.publisher_node}`] = pkg; return acc; }, {} as Record); + console.log({ installedMap }); set({ installed: installedMap }); } } catch (error) { @@ -205,6 +207,7 @@ const useAppsStore = create()((set, get) => ({ const data = await res.json(); const apps = data.GetApps || []; set({ homepageApps: apps }); + console.log({ homepageApps: apps }); } } catch (error) { console.error("Error fetching homepage apps:", error); @@ -473,10 +476,22 @@ const useAppsStore = create()((set, get) => ({ }, navigateToApp: (id: string) => { - window.parent.postMessage({ - type: 'OPEN_APP', - id - }, '*'); + console.log('navigateToApp', id); + if (window.location.hostname.endsWith('.localhost')) { + console.log('localhost nav'); + const app = get().homepageApps.find(app => `${app.package_name}:${app.publisher}` === id); + if (app) { + const path = app.path || ''; + console.log('path', path); + window.location.href = path; + } + } else { + console.log('non-localhost nav') + window.parent.postMessage({ + type: IframeMessageType.OPEN_APP, + id + }, '*'); + } }, resetStore: async () => { diff --git a/hyperdrive/packages/app-store/ui/src/types/messages.ts b/hyperdrive/packages/app-store/ui/src/types/messages.ts new file mode 100644 index 000000000..a9519e59f --- /dev/null +++ b/hyperdrive/packages/app-store/ui/src/types/messages.ts @@ -0,0 +1,12 @@ +export enum IframeMessageType { + OPEN_APP = 'OPEN_APP', + APP_LINK_CLICKED = 'APP_LINK_CLICKED' +} + +export type IframeMessage = + | { type: IframeMessageType.OPEN_APP, id: string } + | { type: IframeMessageType.APP_LINK_CLICKED, url: string } + +export function isIframeMessage(message: any): message is IframeMessage { + return message.type in IframeMessageType; +} \ No newline at end of file diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx index 77cd48ba9..1124176ab 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx @@ -11,7 +11,7 @@ import InstallPrompt from '../InstallPrompt'; import './styles/animations.css'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; -import { isIframeMessage } from '../../types/messages'; +import { IframeMessageType, isIframeMessage } from '../../types/messages'; dayjs.extend(relativeTime); export default function Home() { @@ -35,6 +35,7 @@ export default function Home() { const handleMessage = (event: MessageEvent) => { if (!isIframeMessage(event.data)) { // ignore other iframe messages e.g. metamask + console.log('ignoring message', { event }); return; } @@ -91,7 +92,7 @@ export default function Home() { allGood = false; } - if (event.data.type !== 'OPEN_APP') { + if (event.data.type !== IframeMessageType.OPEN_APP) { console.log('expected OPEN_APP, got:', event.data.type); allGood = false; } @@ -101,7 +102,7 @@ export default function Home() { return; } - if (event.data.type === 'OPEN_APP') { + if (event.data.type === IframeMessageType.OPEN_APP) { const { id } = event.data; const appMatches = apps.filter(app => app.id.endsWith(':' + id)); if (appMatches.length > 1) { From 3c6bcd5398d9aa2fd353218bf646a457ee586304 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Thu, 21 Aug 2025 16:07:42 -0400 Subject: [PATCH 31/65] proper intent processing and retrying for homepage and appstore when certain data hasnt made its way to either place yet --- .../app-store/ui/src/pages/StorePage.tsx | 2 +- .../homepage/ui/src/components/Home/index.tsx | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index f86bfc661..dc26205be 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -9,7 +9,7 @@ import classNames from "classnames"; import { useLocation, useNavigate } from "react-router-dom"; export default function StorePage() { - const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, homepageApps, fetchHomepageApps, getLaunchUrl, navigateToApp } = useAppsStore(); + const { listings, installed, fetchListings, fetchInstalled, fetchUpdates, homepageApps, fetchHomepageApps, navigateToApp } = useAppsStore(); const [searchQuery, setSearchQuery] = useState(""); const [appsNotInstalled, setAppsNotInstalled] = useState([]); const [currentPage, setCurrentPage] = useState(1); diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx index 1124176ab..964f58e0f 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/index.tsx @@ -32,7 +32,7 @@ export default function Home() { const [loading, setLoading] = useState(true); useEffect(() => { - const handleMessage = (event: MessageEvent) => { + const handleMessage = async (event: MessageEvent) => { if (!isIframeMessage(event.data)) { // ignore other iframe messages e.g. metamask console.log('ignoring message', { event }); @@ -103,14 +103,18 @@ export default function Home() { } if (event.data.type === IframeMessageType.OPEN_APP) { + console.log({ openApp: event }); const { id } = event.data; - const appMatches = apps.filter(app => app.id.endsWith(':' + id)); - if (appMatches.length > 1) { + const apps = await fetchApps() as any[]; + console.log({ apps }); + const appMatches = apps?.filter(app => app.id.endsWith(':' + id)); + console.log({ appMatches }); + if (appMatches?.length > 1) { console.error('Multiple apps found with the same id:', { id, apps }); } else if (appMatches.length === 0) { console.error('App not found:', { id, apps }); } - const app = appMatches[0]; + const app = appMatches?.[0]; if (app) { openApp(app); } else { @@ -166,29 +170,33 @@ export default function Home() { return () => window.removeEventListener('keydown', handleKeyPress); }, [runningApps, isRecentAppsOpen, isAppDrawerOpen, toggleRecentApps, toggleAppDrawer, switchToApp, closeAllOverlays]); + const fetchApps = async () => { + try { + const res = await fetch('/apps', { credentials: 'include' }) + const apps = await res.json() as any[] + setApps(apps); + setLoading(false); + return apps; + } catch (error) { + console.warn('Failed to fetch apps from backend:', error); + // Fallback demo apps for development + setApps([ + { id: '1', process: 'settings', package_name: 'settings', publisher: 'sys', path: '/app:settings:sys.os/', label: 'Settings', order: 1, favorite: true }, + { id: '2', process: 'files', package_name: 'files', publisher: 'sys', path: '/app:files:sys.os/', label: 'Files', order: 2, favorite: false }, + { id: '3', process: 'terminal', package_name: 'terminal', publisher: 'sys', path: '/app:terminal:sys.os/', label: 'Terminal', order: 3, favorite: false }, + { id: '4', process: 'browser', package_name: 'browser', publisher: 'sys', path: '/app:browser:sys.os/', label: 'Browser', order: 4, favorite: true }, + { id: '5', process: 'app-store', package_name: 'app-store', publisher: 'sys', path: '/main:app-store:sys/', label: 'App Store', order: 5, favorite: false, widget: 'true' }, + ]); + setLoading(false); + }; + } + // Fetch apps from backend and initialize browser back handling useEffect(() => { // Initialize browser back button handling initBrowserBackHandling(); - fetch('/apps', { credentials: 'include' }) - .then(res => res.json()) - .then(data => { - setApps(data); - setLoading(false); - }) - .catch((error) => { - console.warn('Failed to fetch apps from backend:', error); - // Fallback demo apps for development - setApps([ - { id: '1', process: 'settings', package_name: 'settings', publisher: 'sys', path: '/app:settings:sys.os/', label: 'Settings', order: 1, favorite: true }, - { id: '2', process: 'files', package_name: 'files', publisher: 'sys', path: '/app:files:sys.os/', label: 'Files', order: 2, favorite: false }, - { id: '3', process: 'terminal', package_name: 'terminal', publisher: 'sys', path: '/app:terminal:sys.os/', label: 'Terminal', order: 3, favorite: false }, - { id: '4', process: 'browser', package_name: 'browser', publisher: 'sys', path: '/app:browser:sys.os/', label: 'Browser', order: 4, favorite: true }, - { id: '5', process: 'app-store', package_name: 'app-store', publisher: 'sys', path: '/main:app-store:sys/', label: 'App Store', order: 5, favorite: false, widget: 'true' }, - ]); - setLoading(false); - }); + fetchApps(); }, [setApps, initBrowserBackHandling]); if (loading) { From cdaefa65881a3f7f2dd7cc14b4c501e2775cd260 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 14:47:11 -0700 Subject: [PATCH 32/65] terminal: add clear-state script --- Cargo.lock | 17 ++++++++-- Cargo.toml | 4 +-- hyperdrive/packages/terminal/Cargo.lock | 11 +++++++ hyperdrive/packages/terminal/Cargo.toml | 1 + .../packages/terminal/clear-state/Cargo.toml | 20 +++++++++++ .../packages/terminal/clear-state/src/lib.rs | 33 +++++++++++++++++++ .../packages/terminal/pkg/manifest.json | 1 + hyperdrive/packages/terminal/pkg/scripts.json | 12 +++++++ .../packages/terminal/terminal/src/lib.rs | 4 +++ 9 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 hyperdrive/packages/terminal/clear-state/Cargo.toml create mode 100644 hyperdrive/packages/terminal/clear-state/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 12af32c87..0f4b53458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1591,6 +1591,17 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "clear-state" +version = "0.1.0" +dependencies = [ + "anyhow", + "hyperware_process_lib 2.1.0", + "serde", + "serde_json", + "wit-bindgen 0.42.1", +] + [[package]] name = "cmake" version = "0.1.54" @@ -3291,7 +3302,7 @@ dependencies = [ [[package]] name = "hyperdrive" -version = "1.6.0" +version = "1.6.1" dependencies = [ "aes-gcm", "alloy", @@ -3346,7 +3357,7 @@ dependencies = [ [[package]] name = "hyperdrive_lib" -version = "1.6.0" +version = "1.6.1" dependencies = [ "lib", ] @@ -3972,7 +3983,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lib" -version = "1.6.0" +version = "1.6.1" dependencies = [ "alloy", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 798d6772a..7a714c422 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,8 @@ members = [ "hyperdrive/packages/hypermap-cacher/start-providing", "hyperdrive/packages/hypermap-cacher/stop-providing", "hyperdrive/packages/sign/sign", "hyperdrive/packages/terminal/terminal", "hyperdrive/packages/terminal/add-node-provider", "hyperdrive/packages/terminal/add-rpcurl-provider", - "hyperdrive/packages/terminal/alias", "hyperdrive/packages/terminal/cat", "hyperdrive/packages/terminal/echo", "hyperdrive/packages/terminal/get-providers", - "hyperdrive/packages/terminal/help", "hyperdrive/packages/terminal/hfetch", "hyperdrive/packages/terminal/hi", + "hyperdrive/packages/terminal/alias", "hyperdrive/packages/terminal/cat", "hyperdrive/packages/terminal/clear-state", "hyperdrive/packages/terminal/echo", + "hyperdrive/packages/terminal/get-providers", "hyperdrive/packages/terminal/help", "hyperdrive/packages/terminal/hfetch", "hyperdrive/packages/terminal/hi", "hyperdrive/packages/terminal/kill", "hyperdrive/packages/terminal/m", "hyperdrive/packages/terminal/top", "hyperdrive/packages/terminal/net-diagnostics", "hyperdrive/packages/terminal/peer", "hyperdrive/packages/terminal/peers", "hyperdrive/packages/terminal/remove-provider", "hyperdrive/packages/tester/tester", diff --git a/hyperdrive/packages/terminal/Cargo.lock b/hyperdrive/packages/terminal/Cargo.lock index 2860ebbc2..19afe4693 100644 --- a/hyperdrive/packages/terminal/Cargo.lock +++ b/hyperdrive/packages/terminal/Cargo.lock @@ -1033,6 +1033,17 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clear-state" +version = "0.1.0" +dependencies = [ + "anyhow", + "hyperware_process_lib", + "serde", + "serde_json", + "wit-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.3" diff --git a/hyperdrive/packages/terminal/Cargo.toml b/hyperdrive/packages/terminal/Cargo.toml index 7a6d7f5c5..3a57261ec 100644 --- a/hyperdrive/packages/terminal/Cargo.toml +++ b/hyperdrive/packages/terminal/Cargo.toml @@ -5,6 +5,7 @@ members = [ "add-rpcurl-provider", "alias", "cat", + "clear-state", "echo", "get-providers", "help", diff --git a/hyperdrive/packages/terminal/clear-state/Cargo.toml b/hyperdrive/packages/terminal/clear-state/Cargo.toml new file mode 100644 index 000000000..580f64180 --- /dev/null +++ b/hyperdrive/packages/terminal/clear-state/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "clear-state" +version = "0.1.0" +edition = "2021" + +[features] +simulation-mode = [] + +[dependencies] +anyhow = "1.0" +hyperware_process_lib = "2.1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +wit-bindgen = "0.42.1" + +[lib] +crate-type = ["cdylib"] + +[package.metadata.component] +package = "hyperware:process" diff --git a/hyperdrive/packages/terminal/clear-state/src/lib.rs b/hyperdrive/packages/terminal/clear-state/src/lib.rs new file mode 100644 index 000000000..5f0a63ccc --- /dev/null +++ b/hyperdrive/packages/terminal/clear-state/src/lib.rs @@ -0,0 +1,33 @@ +use hyperware_process_lib::{ + kernel_types::StateAction, println, script, Address, ProcessId, Request, +}; + +wit_bindgen::generate!({ + path: "../target/wit", + world: "process-v1", +}); + +const USAGE: &str = "\x1b[1mUsage:\x1b[0m clear-state "; +const STATE_PROCESS_ID: (&str, &str, &str) = ("state", "distro", "sys"); + +script!(init); +fn init(_our: Address, args: String) -> String { + if args.is_empty() { + return format!("Clear the state of the given process.\n{USAGE}"); + } + + let Ok(ref process_id) = args.parse::() else { + return format!( + "'{args}' is not a process-id (e.g. `process-name:package-name:publisher.os`)\n{USAGE}" + ); + }; + + let Ok(Ok(_)) = Request::to(("our", STATE_PROCESS_ID)) + .body(serde_json::to_vec(&StateAction::DeleteState(process_id.clone())).unwrap()) + .send_and_await_response(5) + else { + return format!("Failed to delete state for process {process_id}"); + }; + + format!("Deleted state of process {process_id}") +} diff --git a/hyperdrive/packages/terminal/pkg/manifest.json b/hyperdrive/packages/terminal/pkg/manifest.json index 8238109e0..4b98add19 100644 --- a/hyperdrive/packages/terminal/pkg/manifest.json +++ b/hyperdrive/packages/terminal/pkg/manifest.json @@ -30,6 +30,7 @@ "net:distro:sys", "sign:sign:sys", "sqlite:distro:sys", + "state:distro:sys", "timer:distro:sys", "vfs:distro:sys", { diff --git a/hyperdrive/packages/terminal/pkg/scripts.json b/hyperdrive/packages/terminal/pkg/scripts.json index 1b2c17825..6d12decba 100644 --- a/hyperdrive/packages/terminal/pkg/scripts.json +++ b/hyperdrive/packages/terminal/pkg/scripts.json @@ -61,6 +61,18 @@ "grant_capabilities": [], "wit_version": 1 }, + "clear-state.wasm": { + "root": false, + "public": false, + "request_networking": false, + "request_capabilities": [ + "state:distro:sys" + ], + "grant_capabilities": [ + "state:distro:sys" + ], + "wit_version": 1 + }, "echo.wasm": { "root": false, "public": false, diff --git a/hyperdrive/packages/terminal/terminal/src/lib.rs b/hyperdrive/packages/terminal/terminal/src/lib.rs index babd24ceb..c1831f6ae 100644 --- a/hyperdrive/packages/terminal/terminal/src/lib.rs +++ b/hyperdrive/packages/terminal/terminal/src/lib.rs @@ -76,6 +76,10 @@ impl VersionedState { "cat".to_string(), ProcessId::new(Some("cat"), "terminal", "sys"), ), + ( + "clear-state".to_string(), + ProcessId::new(Some("clear-state"), "terminal", "sys"), + ), ( "echo".to_string(), ProcessId::new(Some("echo"), "terminal", "sys"), From 02531babc7042fe1849b7dbc94b2c047205aa3e3 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 14:52:34 -0700 Subject: [PATCH 33/65] update docs --- README.md | 77 +++++++++++++------- hyperdrive/packages/terminal/help/src/lib.rs | 3 +- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a606e377c..8d6fb4916 100644 --- a/README.md +++ b/README.md @@ -251,36 +251,57 @@ Subsequent use of the shorthand will then be interpolated as the process ID. A list of the terminal scripts included in this distro: -- `alias `: create an alias for a script. - - Example: `alias get_block get-block:hns-indexer:sys` +add-node-provider [--trusted ]: add a node provider to the providers configuration. + - Examples: + add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000 (defaults to trusted=false) + add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true + +add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]: add an RPC URL provider to the providers configuration. + - Examples: + add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key (defaults to chain-id=8453, trusted=true) + add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1 + add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false + add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token + +alias : create an alias for a script. + - Example: alias get-block get-block:hns-indexer:sys - note: all of these listed commands are just default aliases for terminal scripts. -- `cat `: print the contents of a file in the terminal. - - Example: `cat /terminal:sys/pkg/scripts.json` -- `echo `: print text to the terminal. - - Example: `echo foo` -- `help `: print the help message for a command. - Leave the command blank to print the help message for all commands. -- `hi `: send a text message to another node's command line. - - Example: `hi mothu.hypr hello world` -- `hfetch`: print system information a la neofetch. - No arguments. -- `kill `: terminate a running process. - This will bypass any restart behavior–use judiciously. - - Example: `kill chess:chess:template.os` -- `m
''`: send an inter-process message. -
is formatted as @. - is formatted as ::. - JSON containing spaces must be wrapped in single-quotes (`''`). - - Example: `m our@eth:distro:sys "SetPublic" -a 5` + +cat : print the contents of a file in the terminal. + - Example: cat /terminal:sys/pkg/scripts.json + +clear-state : clear the state of the given process. + +echo : print text to the terminal. + - Example: echo foo + +get-providers: display the providers configuration. + +hi : send a text message to another node's command line. + - Example: hi mothu.hypr hello world + +kfetch: print system information a la neofetch. No arguments. + +kill : terminate a running process. This will bypass any restart behavior; use judiciously. + - Example: kill chess:chess:sys + +m
'': send an inter-process message.
is formatted as @. is formatted as ::. JSON containing spaces must be wrapped in single-quotes (''). + - Example: m our@eth:distro:sys "SetPublic" -a 5 - the '-a' flag is used to expect a response with a given timeout - - `our` will always be interpolated by the system as your node's name -- `net-diagnostics`: print some useful networking diagnostic data. -- `peer `: print the peer's PKI info, if it exists. -- `peers`: print the peers the node currently hold connections with. -- `top `: display kernel debugging info about a process. - Leave the process ID blank to display info about all processes and get the total number of running processes. - - Example: `top net:distro:sys` - - Example: `top` + - our will always be interpolated by the system as your node's name + +net-diagnostics: print some useful networking diagnostic data. + +peer : print the peer's PKI info, if it exists. + +peers: print the peers the node currently hold connections with. + +remove-provider : remove a provider from the providers configuration. + - Example: remove-provider 8453 wss://base-mainnet.infura.io/ws/v3/your-key + +top : display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes. + - Example: top net:distro:sys + - Example: top ## Running as a Docker container diff --git a/hyperdrive/packages/terminal/help/src/lib.rs b/hyperdrive/packages/terminal/help/src/lib.rs index 999e32d93..3b26ff75e 100644 --- a/hyperdrive/packages/terminal/help/src/lib.rs +++ b/hyperdrive/packages/terminal/help/src/lib.rs @@ -5,11 +5,12 @@ wit_bindgen::generate!({ world: "process-v1", }); -const HELP_MESSAGES: [[&str; 2]; 15] = [ +const HELP_MESSAGES: [[&str; 2]; 16] = [ ["add-node-provider", "\n\x1b[1madd-node-provider\x1b[0m [--trusted ]: add a node provider to the providers configuration.\n - Examples:\n \x1b[1madd-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000\x1b[0m (defaults to trusted=false)\n \x1b[1madd-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true\x1b[0m"], ["add-rpcurl-provider", "\n\x1b[1madd-rpcurl-provider\x1b[0m [--chain-id ] [--trusted ] [--auth-type --auth-value ]: add an RPC URL provider to the providers configuration.\n - Examples:\n \x1b[1madd-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key\x1b[0m (defaults to chain-id=8453, trusted=true)\n \x1b[1madd-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1\x1b[0m\n \x1b[1madd-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false\x1b[0m\n \x1b[1madd-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token\x1b[0m"], ["alias", "\n\x1b[1malias\x1b[0m : create an alias for a script.\n - Example: \x1b[1malias get-block get-block:hns-indexer:sys\x1b[0m\n - note: all of these listed commands are just default aliases for terminal scripts."], ["cat", "\n\x1b[1mcat\x1b[0m : print the contents of a file in the terminal.\n - Example: \x1b[1mcat /terminal:sys/pkg/scripts.json\x1b[0m"], + ["clear-state", "\n\x1b[1mclear-state\x1b[0m : clear the state of the given process."], ["echo", "\n\x1b[1mecho\x1b[0m : print text to the terminal.\n - Example: \x1b[1mecho foo\x1b[0m"], ["get-providers", "\n\x1b[1mget-providers\x1b[0m: display the providers configuration."], ["hi", "\n\x1b[1mhi\x1b[0m : send a text message to another node's command line.\n - Example: \x1b[1mhi mothu.hypr hello world\x1b[0m"], From 45ac80d70704dcaca75c69fd790ea15242c5ff3b Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 14:54:10 -0700 Subject: [PATCH 34/65] update readme formatting --- README.md | 47 ++++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 8d6fb4916..080938c81 100644 --- a/README.md +++ b/README.md @@ -251,55 +251,40 @@ Subsequent use of the shorthand will then be interpolated as the process ID. A list of the terminal scripts included in this distro: -add-node-provider [--trusted ]: add a node provider to the providers configuration. +- add-node-provider [--trusted ]: add a node provider to the providers configuration. - Examples: add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000 (defaults to trusted=false) add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true - -add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]: add an RPC URL provider to the providers configuration. +- add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]: add an RPC URL provider to the providers configuration. - Examples: add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key (defaults to chain-id=8453, trusted=true) add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1 add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token - -alias : create an alias for a script. +- alias : create an alias for a script. - Example: alias get-block get-block:hns-indexer:sys - note: all of these listed commands are just default aliases for terminal scripts. - -cat : print the contents of a file in the terminal. +- cat : print the contents of a file in the terminal. - Example: cat /terminal:sys/pkg/scripts.json - -clear-state : clear the state of the given process. - -echo : print text to the terminal. +- clear-state : clear the state of the given process. +- echo : print text to the terminal. - Example: echo foo - -get-providers: display the providers configuration. - -hi : send a text message to another node's command line. +- get-providers: display the providers configuration. +- hi : send a text message to another node's command line. - Example: hi mothu.hypr hello world - -kfetch: print system information a la neofetch. No arguments. - -kill : terminate a running process. This will bypass any restart behavior; use judiciously. +- kfetch: print system information a la neofetch. No arguments. +- kill : terminate a running process. This will bypass any restart behavior; use judiciously. - Example: kill chess:chess:sys - -m
'': send an inter-process message.
is formatted as @. is formatted as ::. JSON containing spaces must be wrapped in single-quotes (''). +- m
'': send an inter-process message.
is formatted as @. is formatted as ::. JSON containing spaces must be wrapped in single-quotes (''). - Example: m our@eth:distro:sys "SetPublic" -a 5 - the '-a' flag is used to expect a response with a given timeout - our will always be interpolated by the system as your node's name - -net-diagnostics: print some useful networking diagnostic data. - -peer : print the peer's PKI info, if it exists. - -peers: print the peers the node currently hold connections with. - -remove-provider : remove a provider from the providers configuration. +- net-diagnostics: print some useful networking diagnostic data. +- peer : print the peer's PKI info, if it exists. +- peers: print the peers the node currently hold connections with. +- remove-provider : remove a provider from the providers configuration. - Example: remove-provider 8453 wss://base-mainnet.infura.io/ws/v3/your-key - -top : display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes. +- top : display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes. - Example: top net:distro:sys - Example: top From 0d1e41222c9fd7b6a6eab5c9838defe8d395df6e Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 14:57:48 -0700 Subject: [PATCH 35/65] update readme --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 080938c81..b64de80f7 100644 --- a/README.md +++ b/README.md @@ -251,42 +251,42 @@ Subsequent use of the shorthand will then be interpolated as the process ID. A list of the terminal scripts included in this distro: -- add-node-provider [--trusted ]: add a node provider to the providers configuration. +- `add-node-provider [--trusted ]`: add a node provider to the providers configuration. - Examples: - add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000 (defaults to trusted=false) - add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true -- add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]: add an RPC URL provider to the providers configuration. + `add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000` (defaults to trusted=false) + `add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true` +- `add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]`: add an RPC URL provider to the providers configuration. - Examples: - add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key (defaults to chain-id=8453, trusted=true) - add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1 - add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false - add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token -- alias : create an alias for a script. - - Example: alias get-block get-block:hns-indexer:sys + `add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key` (defaults to chain-id=8453, trusted=true) + `add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1` + `add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false` + `add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token` +- `alias `: create an alias for a script. + - Example: `alias get-block get-block:hns-indexer:sys` - note: all of these listed commands are just default aliases for terminal scripts. -- cat : print the contents of a file in the terminal. - - Example: cat /terminal:sys/pkg/scripts.json -- clear-state : clear the state of the given process. -- echo : print text to the terminal. - - Example: echo foo -- get-providers: display the providers configuration. -- hi : send a text message to another node's command line. - - Example: hi mothu.hypr hello world -- kfetch: print system information a la neofetch. No arguments. -- kill : terminate a running process. This will bypass any restart behavior; use judiciously. - - Example: kill chess:chess:sys -- m
'': send an inter-process message.
is formatted as @. is formatted as ::. JSON containing spaces must be wrapped in single-quotes (''). - - Example: m our@eth:distro:sys "SetPublic" -a 5 - - the '-a' flag is used to expect a response with a given timeout - - our will always be interpolated by the system as your node's name -- net-diagnostics: print some useful networking diagnostic data. -- peer : print the peer's PKI info, if it exists. -- peers: print the peers the node currently hold connections with. -- remove-provider : remove a provider from the providers configuration. - - Example: remove-provider 8453 wss://base-mainnet.infura.io/ws/v3/your-key -- top : display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes. - - Example: top net:distro:sys - - Example: top +- `cat `: print the contents of a file in the terminal. + - Example: `cat /terminal:sys/pkg/scripts.json` +- `clear-state `: clear the state of the given process. +- `echo : print text to the terminal. + - Example: `echo foo` +- `get-providers`: display the providers configuration. +- `hi `: send a text message to another node's command line. + - Example: `hi mothu.hypr hello world` +- `kfetch`: print system information a la neofetch. No arguments. +- `kill `: terminate a running process. This will bypass any restart behavior; use judiciously. + - Example: `kill chess:chess:sys` +- `m
''`: send an inter-process message. `
` is formatted as `@`. `` is formatted as `::`. JSON containing spaces must be wrapped in single-quotes (''). + - Example: `m our@eth:distro:sys "SetPublic" -a 5` + - the `-a` flag is used to expect a response with a given timeout + - `our` will always be interpolated by the system as your node's name +- `net-diagnostics`: print some useful networking diagnostic data. +- `peer `: print the peer's PKI info, if it exists. +- `peers`: print the peers the node currently hold connections with. +- `remove-provider `: remove a provider from the providers configuration. + - Example: `remove-provider 8453 wss://base-mainnet.infura.io/ws/v3/your-key` +- `top `: display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes. + - Example: `top net:distro:sys` + - Example: `top` ## Running as a Docker container From f001eac1bb63f5d011f2f6c1899d7db52de239e3 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 14:58:52 -0700 Subject: [PATCH 36/65] update readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b64de80f7..845b43c1b 100644 --- a/README.md +++ b/README.md @@ -253,14 +253,14 @@ A list of the terminal scripts included in this distro: - `add-node-provider [--trusted ]`: add a node provider to the providers configuration. - Examples: - `add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000` (defaults to trusted=false) - `add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true` + - `add-node-provider 8453 other-node.hypr abc123pubkey 192.168.1.1 9000` (defaults to trusted=false) + - `add-node-provider 1 other-node.hypr abc123pubkey 192.168.1.1 9000 --trusted true` - `add-rpcurl-provider [--chain-id ] [--trusted ] [--auth-type --auth-value ]`: add an RPC URL provider to the providers configuration. - Examples: - `add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key` (defaults to chain-id=8453, trusted=true) - `add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1` - `add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false` - `add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token` + - `add-rpcurl-provider wss://base-mainnet.infura.io/v3/your-key` (defaults to chain-id=8453, trusted=true) + - `add-rpcurl-provider wss://mainnet.infura.io/v3/your-key --chain-id 1` + - `add-rpcurl-provider wss://base-mainnet.infura.io/ws/v3/your-key --trusted false` + - `add-rpcurl-provider wss://rpc.example.com --auth-type bearer --auth-value your-token` - `alias `: create an alias for a script. - Example: `alias get-block get-block:hns-indexer:sys` - note: all of these listed commands are just default aliases for terminal scripts. From d320ba3af49b2b8271c2c41369f46ea00ba4a2d4 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 22 Aug 2025 15:46:19 -0700 Subject: [PATCH 37/65] state: do not allow arbitrary processes to alter state --- hyperdrive/src/state.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/hyperdrive/src/state.rs b/hyperdrive/src/state.rs index 611800df8..2a1f5ac32 100644 --- a/hyperdrive/src/state.rs +++ b/hyperdrive/src/state.rs @@ -118,6 +118,20 @@ pub async fn state_sender( continue; } + if (km.source.process.package() != "distro" && km.source.process.package() != "terminal") || km.source.process.publisher() != "sys" { + Printout::new( + 1, + STATE_PROCESS_ID.clone(), + format!( + "state: got request from {}, but requests must come from kernel or terminal", + km.source.process + ), + ) + .send(&send_to_terminal) + .await; + continue; + } + let queue = process_queues .get(&km.source.process) .cloned() From fb77cc243b4f08910f041b88fdcf661163b3053a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:46:42 +0000 Subject: [PATCH 38/65] Format Rust code using rustfmt --- hyperdrive/src/state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hyperdrive/src/state.rs b/hyperdrive/src/state.rs index 2a1f5ac32..710886bf9 100644 --- a/hyperdrive/src/state.rs +++ b/hyperdrive/src/state.rs @@ -118,7 +118,9 @@ pub async fn state_sender( continue; } - if (km.source.process.package() != "distro" && km.source.process.package() != "terminal") || km.source.process.publisher() != "sys" { + if (km.source.process.package() != "distro" && km.source.process.package() != "terminal") + || km.source.process.publisher() != "sys" + { Printout::new( 1, STATE_PROCESS_ID.clone(), From bd5e6043b0c03deb7ae11b82348c17ad6f2b7d6e Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Mon, 25 Aug 2025 15:08:55 -0400 Subject: [PATCH 39/65] localhost app linking --- Cargo.lock | 23 +++- .../app-store/ui/src/pages/AppPage.tsx | 92 ++++++++++++---- .../app-store/ui/src/pages/StorePage.tsx | 30 +++++- .../app-store/ui/src/store/appStoreStore.ts | 37 +++---- hyperdrive/packages/file-explorer/Cargo.toml | 1 + .../file-explorer/explorer/Cargo.toml | 3 + hyperdrive/packages/homepage/ui/public/sw.js | 36 +++---- .../components/Home/components/HomeScreen.tsx | 10 +- .../homepage/ui/src/stores/navigationStore.ts | 101 +++--------------- 9 files changed, 166 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12af32c87..406ddf5e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1323,6 +1323,22 @@ dependencies = [ "serde", ] +[[package]] +name = "caller-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "futures-util", + "hyperware_app_common", + "once_cell", + "process_macros", + "serde", + "serde_json", + "uuid 1.17.0", + "wit-bindgen 0.41.0", +] + [[package]] name = "camino" version = "1.1.10" @@ -2484,6 +2500,7 @@ name = "explorer" version = "0.1.0" dependencies = [ "anyhow", + "caller-utils", "hyperprocess_macro", "hyperware_app_common", "md5", @@ -3291,7 +3308,7 @@ dependencies = [ [[package]] name = "hyperdrive" -version = "1.6.0" +version = "1.6.1" dependencies = [ "aes-gcm", "alloy", @@ -3346,7 +3363,7 @@ dependencies = [ [[package]] name = "hyperdrive_lib" -version = "1.6.0" +version = "1.6.1" dependencies = [ "lib", ] @@ -3972,7 +3989,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lib" -version = "1.6.0" +version = "1.6.1" dependencies = [ "alloy", "anyhow", diff --git a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx index 929bb42b1..f86d5fc91 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/AppPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useCallback, useMemo } from "react"; import { useParams, useLocation } from "react-router-dom"; import useAppsStore from "../store/appStoreStore"; -import { AppListing, PackageState, ManifestResponse } from "../types/Apps"; +import { AppListing, PackageState, ManifestResponse, HomepageApp } from "../types/Apps"; import { compareVersions } from "../utils/compareVersions"; import { MirrorSelector, ManifestDisplay } from '../components'; import { FaChevronDown, FaChevronRight, FaCheck, FaCircleNotch, FaPlay } from "react-icons/fa6"; @@ -19,7 +19,7 @@ const MOCK_APP: AppListing = { }, metadata: { name: 'Mock App with an Unreasonably Long Name for Testing Wrapping, Obviously, why else would you have a name this long?', - description: `This is a mock app. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page.`, + description: `to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page. I have written an incredibly long description to test the app page.`, image: 'https://via.placeholder.com/150', properties: { code_hashes: [['1.0.0', '1234567890']], @@ -46,6 +46,7 @@ export default function AppPage() { uninstallApp, setAutoUpdate, getLaunchUrl, + homepageApps, fetchHomepageApps, downloadApp, downloads, @@ -81,6 +82,8 @@ export default function AppPage() { const [backtickPressCount, setBacktickPressCount] = useState(0); const [detailExpanded, setDetailExpanded] = useState(false); const [hasProcessedIntent, setHasProcessedIntent] = useState(false); + const [awaitPresenceInHomepageApps, setAwaitPresenceInHomepageApps] = useState(false); + const [launchUrl, setLaunchUrl] = useState(null); useEffect(() => { const backTickCounter = (e: KeyboardEvent) => { @@ -135,10 +138,8 @@ export default function AppPage() { setError(null); try { - const [appData, installedAppData] = await Promise.all([ - isDevMode ? Promise.resolve(MOCK_APP) : fetchListing(id), - fetchInstalledApp(id) - ]); + const appData = isDevMode ? MOCK_APP : await fetchListing(id); + const installedAppData = await fetchInstalledApp(id); if (!appData) { setError("App not found"); @@ -166,8 +167,7 @@ export default function AppPage() { } } - await fetchHomepageApps(); - setCanLaunch(!!getLaunchUrl(`${appData.package_id.package_name}:${appData.package_id.publisher_node}`)); + setAwaitPresenceInHomepageApps(true); } catch (err) { setError("Failed to load app details. Please try again."); console.error(err); @@ -176,6 +176,36 @@ export default function AppPage() { } }, [id, fetchListing, fetchInstalledApp, fetchHomepageApps, getLaunchUrl]); + const calculateCanLaunch = useCallback((appData: AppListing) => { + const { foundApp, path } = getLaunchUrl(`${appData?.package_id.package_name}:${appData?.package_id.publisher_node}`); + setCanLaunch(foundApp); + setLaunchUrl(path); + if (foundApp) { + setAwaitPresenceInHomepageApps(false); + } + }, [getLaunchUrl]); + + useEffect(() => { + const interval = setInterval(async () => { + if (awaitPresenceInHomepageApps) { + const homepageApps = await fetchHomepageApps(); + console.log('[app-page] checking for presence in homepageApps', { id, homepageApps }); + if (homepageApps.find(app => app.id.endsWith(`:${app.package_name}:${app.publisher}`))) { + console.log('[app-page] found in homepageApps'); + const appData = id && await fetchListing(id); + console.log('[app-page] appData', { appData }); + if (appData) { + setApp(appData); + calculateCanLaunch(appData); + } + } else { + console.log('[app-page] not found in homepageApps'); + } + } + }, 1000); + return () => clearInterval(interval); + }, [awaitPresenceInHomepageApps, fetchHomepageApps, calculateCanLaunch]); + const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => { setSelectedMirror(mirror); setIsMirrorOnline(status === 'http' ? true : status); @@ -262,21 +292,20 @@ export default function AppPage() { if (!id || !selectedVersion || !app) return; const versionData = sortedVersions.find(v => v.version === selectedVersion); - if (!versionData) return; - + if (!versionData) { + console.error('Installation flow failed to find version data', { sortedVersions }); + addNotification({ + id: `installation-flow-failed-versionData`, + timestamp: Date.now(), + type: 'error', + message: `Installation flow failed to obtain correct version data.`, + }); + return; + } try { setIsInstalling(true); await installApp(id, versionData.hash); - // Refresh all relevant data after 3 seconds - setTimeout(async () => { - setShowCapApproval(false); - setManifestResponse(null); - setIsInstalling(false); - await Promise.all([ - fetchHomepageApps(), - loadData() - ]); - }, 3000); + await loadData(); } catch (error) { console.error('Installation failed:', error); const errorString = Object.keys(error).length > 0 ? `: ${JSON.stringify(error).slice(0, 100)}...` : ''; @@ -286,6 +315,10 @@ export default function AppPage() { type: 'error', message: `Installation failed${errorString ? ': ' + errorString : ''}`, }); + } finally { + setShowCapApproval(false); + setManifestResponse(null); + setIsInstalling(false); } }, [id, selectedVersion, app, sortedVersions, installApp, fetchHomepageApps, loadData]); @@ -391,6 +424,11 @@ export default function AppPage() { console.log('setting hasProcessedIntent to true'); setHasProcessedIntent(true); + // after processing intent, remove the intent parameter from the URL + const url = new URL(window.location.href); + url.searchParams.delete('intent'); + window.history.replaceState({}, '', url.toString()); + // Auto-trigger actions based on intent if (intent === 'launch' && canLaunch) { // Automatically launch the app @@ -464,12 +502,20 @@ export default function AppPage() { : } Updates {app.auto_update ? " ON" : " OFF"} - {(canLaunch || isDevMode) && ( + {(awaitPresenceInHomepageApps || canLaunch || isDevMode) && ( )} } diff --git a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx index dc26205be..f529516f3 100644 --- a/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx +++ b/hyperdrive/packages/app-store/ui/src/pages/StorePage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import useAppsStore from "../store/appStoreStore"; import { AppListing } from "../types/Apps"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; @@ -68,6 +68,24 @@ export default function StorePage() { }); }, [listings, searchQuery]); + const getLocalhostLink = useCallback((app: AppListing) => { + const isLocalhost = window.location.hostname.endsWith('.localhost'); + console.log({ isLocalhost }); + if (!isLocalhost) return null; + let path = homepageApps.find((hpa) => hpa.id.match(new RegExp(`${app.package_id.package_name}:${app.package_id.publisher_node}(\/)?$`)))?.path; + if (path?.[0] !== '/') path = `/${path}`; + console.log({ path }); + if (!path) return null; + const href = `${window.location.protocol}//localhost${window.location.port ? `:${window.location.port}` : ''}${path}`.replace(/\/$/, ''); + console.log({ href }); + return + }, [homepageApps]); + return (
@@ -112,7 +130,10 @@ export default function StorePage() { ? navigateToApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`)} - /> + className="relative" + > + {getLocalhostLink(app) || null} + : } ))} @@ -161,12 +182,13 @@ const ActionChip: React.FC<{ label: string; className?: string; onClick?: () => void; -}> = ({ label, className, onClick }) => { + children?: React.ReactNode; +}> = ({ label, className, onClick, children }) => { return
{label} + }, className)}>{label}{children}
} diff --git a/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts b/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts index 86c5dc7da..9e698d4e5 100644 --- a/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts +++ b/hyperdrive/packages/app-store/ui/src/store/appStoreStore.ts @@ -32,8 +32,8 @@ interface AppsStore { checkMirror: (id: string, node: string) => Promise resetStore: () => Promise - fetchHomepageApps: () => Promise - getLaunchUrl: (id: string) => string | null + fetchHomepageApps: () => Promise + getLaunchUrl: (id: string) => { foundApp: boolean, path: string | null } addNotification: (notification: Notification) => void; removeNotification: (id: string) => void; @@ -200,7 +200,7 @@ const useAppsStore = create()((set, get) => ({ return []; }, - fetchHomepageApps: async () => { + fetchHomepageApps: async (): Promise => { try { const res = await fetch(`${BASE_URL}/homepageapps`); if (res.status === HTTP_STATUS.OK) { @@ -208,16 +208,23 @@ const useAppsStore = create()((set, get) => ({ const apps = data.GetApps || []; set({ homepageApps: apps }); console.log({ homepageApps: apps }); + return apps; } } catch (error) { console.error("Error fetching homepage apps:", error); set({ homepageApps: [] }); } + return []; }, getLaunchUrl: (id: string) => { - const app = get().homepageApps?.find(app => `${app.package_name}:${app.publisher}` === id); - return app?.path || null; + const { homepageApps } = get(); + const app = homepageApps.find(app => `${app.package_name}:${app.publisher}` === id); + console.log('getLaunchUrl', { id, app, homepageApps }); + return { + foundApp: !!app, + path: app?.path || null + }; }, checkMirror: async (id: string, node: string) => { @@ -476,22 +483,10 @@ const useAppsStore = create()((set, get) => ({ }, navigateToApp: (id: string) => { - console.log('navigateToApp', id); - if (window.location.hostname.endsWith('.localhost')) { - console.log('localhost nav'); - const app = get().homepageApps.find(app => `${app.package_name}:${app.publisher}` === id); - if (app) { - const path = app.path || ''; - console.log('path', path); - window.location.href = path; - } - } else { - console.log('non-localhost nav') - window.parent.postMessage({ - type: IframeMessageType.OPEN_APP, - id - }, '*'); - } + window.parent.postMessage({ + type: IframeMessageType.OPEN_APP, + id + }, '*'); }, resetStore: async () => { diff --git a/hyperdrive/packages/file-explorer/Cargo.toml b/hyperdrive/packages/file-explorer/Cargo.toml index 299a6ba1d..4aa010ccf 100644 --- a/hyperdrive/packages/file-explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/Cargo.toml @@ -6,5 +6,6 @@ panic = "abort" [workspace] members = [ "explorer", + "target/caller-utils", ] resolver = "2" diff --git a/hyperdrive/packages/file-explorer/explorer/Cargo.toml b/hyperdrive/packages/file-explorer/explorer/Cargo.toml index 3b36f1446..0218351a2 100644 --- a/hyperdrive/packages/file-explorer/explorer/Cargo.toml +++ b/hyperdrive/packages/file-explorer/explorer/Cargo.toml @@ -7,6 +7,9 @@ serde_urlencoded = "0.7" tracing = "0.1.37" wit-bindgen = "0.42.1" +[dependencies.caller-utils] +path = "../target/caller-utils" + [dependencies.hyperprocess_macro] git = "https://github.com/hyperware-ai/hyperprocess-macro" rev = "9836e2a" diff --git a/hyperdrive/packages/homepage/ui/public/sw.js b/hyperdrive/packages/homepage/ui/public/sw.js index 686454f23..e1c0538f7 100644 --- a/hyperdrive/packages/homepage/ui/public/sw.js +++ b/hyperdrive/packages/homepage/ui/public/sw.js @@ -38,36 +38,28 @@ self.addEventListener('activate', (event) => { // Fetch event - network first strategy (online-only app) self.addEventListener('fetch', (event) => { - // Skip non-GET requests - if (event.request.method !== 'GET') { - return; - } + // Only handle same-origin, GET requests for the app shell. Let the browser + // handle everything else (prevents returning undefined Responses). + if (event.request.method !== 'GET') return; + if (!event.request.url.startsWith(self.location.origin)) return; - // Skip cross-origin requests - if (!event.request.url.startsWith(self.location.origin)) { - return; - } + const url = new URL(event.request.url); + const isAppShellRequest = urlsToCache.includes(url.pathname); + + if (!isAppShellRequest) return; // don't intercept non-app-shell routes event.respondWith( - // Try network first fetch(event.request) .then((response) => { - // Clone the response before using it const responseToCache = response.clone(); - - // Update cache with fresh response for app shell files - if (urlsToCache.includes(new URL(event.request.url).pathname)) { - caches.open(CACHE_NAME) - .then((cache) => { - cache.put(event.request, responseToCache); - }); - } - + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); return response; }) - .catch(() => { - // If network fails, try cache (only for app shell) - return caches.match(event.request); + .catch(async () => { + const cached = await caches.match(event.request); + return cached || new Response('', { status: 504, statusText: 'Gateway Timeout' }); }) ); }); diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx index 52eb7b138..7f1aa8d45 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx @@ -72,8 +72,8 @@ export const HomeScreen: React.FC = () => { }; const handleDockDrop = (e: React.DragEvent, index: number) => { - try {e.preventDefault();} catch {} - try {e.stopPropagation();} catch {} + try { e.preventDefault(); } catch { } + try { e.stopPropagation(); } catch { } const appId = e.dataTransfer.getData('appId'); if (appId) { // Add to dock at the specified index @@ -299,7 +299,7 @@ export const HomeScreen: React.FC = () => { {floatingApps .filter(app => { return !app.id.includes('homepage:homepage:sys') // don't show the clock icon because it does nothing. - // && (!searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) + // && (!searchQuery || app.label.toLowerCase().includes(searchQuery.toLowerCase())) }) .map((app, index, allApps) => { const position = appPositions[app.id] || calculateAppIconPosition(app.id, index, allApps.length); @@ -620,11 +620,11 @@ export const HomeScreen: React.FC = () => { + )} + + {permission === 'granted' && !isSubscribed && ( + + )} + + {permission === 'granted' && isSubscribed && ( + <> + + + + + )} + + {permission === 'denied' && ( +

+ Notifications are blocked. Please enable them in your browser settings. +

+ )} +
+ + {onClose && ( + + )} +
+ ); +}; + +export default NotificationSettings; diff --git a/hyperdrive/packages/homepage/ui/src/main.tsx b/hyperdrive/packages/homepage/ui/src/main.tsx index 2fb308b8a..3e89ef8e7 100644 --- a/hyperdrive/packages/homepage/ui/src/main.tsx +++ b/hyperdrive/packages/homepage/ui/src/main.tsx @@ -3,13 +3,168 @@ import ReactDOM from 'react-dom/client' import Home from './components/Home' import './index.css' +// Helper function to convert base64 to Uint8Array +function urlBase64ToUint8Array(base64String: string) { + const padding = '='.repeat((4 - base64String.length % 4) % 4); + const base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + +// Initialize push notifications +async function initializePushNotifications(registration: ServiceWorkerRegistration) { + try { + console.log('[Init Push] Starting push notification initialization'); + + // Check if push notifications are supported + if (!('PushManager' in window)) { + console.log('[Init Push] Push notifications not supported'); + return; + } + + // Check current permission status + let permission = Notification.permission; + console.log('[Init Push] Current permission status:', permission); + + if (permission === 'denied') { + console.log('[Init Push] Push notifications permission denied'); + return; + } + + // Request permission if not granted + if (permission === 'default') { + console.log('[Init Push] Requesting notification permission...'); + permission = await Notification.requestPermission(); + console.log('[Init Push] Permission result:', permission); + + if (permission !== 'granted') { + console.log('[Init Push] Permission not granted'); + return; + } + } + + // Get VAPID public key from server + console.log('[Init Push] Fetching VAPID public key...'); + const vapidResponse = await fetch('/api/notifications/vapid-key'); + console.log('[Init Push] VAPID response status:', vapidResponse.status); + + if (!vapidResponse.ok) { + const errorText = await vapidResponse.text(); + console.error('[Init Push] Failed to get VAPID public key:', errorText); + return; + } + + const responseData = await vapidResponse.json(); + console.log('[Init Push] VAPID response data:', responseData); + const { publicKey } = responseData; + + if (!publicKey) { + console.error('[Init Push] No VAPID public key available in response'); + return; + } + + console.log('[Init Push] Got VAPID public key:', publicKey); + + // Check if already subscribed + console.log('[Init Push] Checking existing subscription...'); + let subscription = await registration.pushManager.getSubscription(); + console.log('[Init Push] Existing subscription:', subscription); + + if (!subscription && permission === 'granted') { + // Subscribe if we have permission but no subscription + console.log('[Init Push] Permission granted, attempting to subscribe...'); + + try { + // Convert the public key + console.log('[Init Push] Converting public key to Uint8Array...'); + console.log('[Init Push] Public key string length:', publicKey.length); + const applicationServerKey = urlBase64ToUint8Array(publicKey); + console.log('[Init Push] Converted key length:', applicationServerKey.length, 'bytes'); + console.log('[Init Push] First byte should be 0x04 for uncompressed:', applicationServerKey[0]); + + // Validate the key format + if (applicationServerKey.length !== 65) { + console.error('[Init Push] Invalid key length! Expected 65 bytes for P-256, got:', applicationServerKey.length); + } + if (applicationServerKey[0] !== 0x04) { + console.error('[Init Push] Invalid key format! First byte should be 0x04 for uncompressed, got:', applicationServerKey[0]); + } + + // Subscribe to push notifications + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey + }); + + console.log('[Init Push] Successfully subscribed:', subscription); + console.log('[Init Push] Subscription endpoint:', subscription.endpoint); + } catch (subscribeError: any) { + console.error('[Init Push] Subscribe error:', subscribeError); + console.error('[Init Push] Error name:', subscribeError?.name); + console.error('[Init Push] Error message:', subscribeError?.message); + + if (subscribeError?.name === 'AbortError') { + console.error('[Init Push] Push service registration was aborted - this usually means the VAPID key is invalid or malformed'); + console.error('[Init Push] VAPID key that failed:', publicKey); + } + + // Return early if subscription failed + return; + } + + // Send subscription to server (only if we have a subscription) + if (subscription) { + const response = await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(subscription.toJSON()) + }); + + if (response.ok) { + console.log('Push notification subscription successful'); + } else { + console.error('Failed to save subscription on server'); + } + } + } else { + console.log('Already subscribed to push notifications'); + + // Optionally update subscription on server to ensure it's current + if (subscription) { + await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(subscription.toJSON()) + }); + } + } + } catch (error) { + console.error('Error initializing push notifications:', error); + } +} + // Register service worker for PWA if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') - .then((registration) => { + .then(async (registration) => { console.log('SW registered:', registration); + // Initialize push notifications + await initializePushNotifications(registration); + // Check for updates periodically setInterval(() => { registration.update(); diff --git a/hyperdrive/src/main.rs b/hyperdrive/src/main.rs index e9bb037f4..bd087a5ba 100644 --- a/hyperdrive/src/main.rs +++ b/hyperdrive/src/main.rs @@ -23,6 +23,7 @@ mod kernel; mod keygen; mod kv; mod net; +mod notifications; #[cfg(not(feature = "simulation-mode"))] mod register; mod sol; @@ -246,6 +247,9 @@ async fn main() { // fd_manager makes sure we don't overrun the `ulimit -n`: max number of file descriptors let (fd_manager_sender, fd_manager_receiver): (MessageSender, MessageReceiver) = mpsc::channel(FD_MANAGER_CHANNEL_CAPACITY); + // notifications handles web push notifications + let (notifications_sender, notifications_receiver): (MessageSender, MessageReceiver) = + mpsc::channel(VFS_CHANNEL_CAPACITY); // terminal receives prints via this channel, all other modules send prints let (print_sender, print_receiver): (PrintSender, PrintReceiver) = mpsc::channel(TERMINAL_CHANNEL_CAPACITY); @@ -342,7 +346,7 @@ async fn main() { ), ( ProcessId::new(Some("state"), "distro", "sys"), - state_sender, + state_sender.clone(), None, false, ), @@ -364,6 +368,12 @@ async fn main() { None, false, ), + ( + ProcessId::new(Some("notifications"), "distro", "sys"), + notifications_sender, + None, + false, + ), ]; /* @@ -499,13 +509,20 @@ async fn main() { print_sender.clone(), )); tasks.spawn(vfs::vfs( - our_name_arc, + our_name_arc.clone(), kernel_message_sender.clone(), print_sender.clone(), vfs_message_receiver, caps_oracle_sender.clone(), home_directory_path.clone(), )); + tasks.spawn(notifications::notifications( + our_name_arc, + kernel_message_sender.clone(), + print_sender.clone(), + notifications_receiver, + state_sender.clone(), + )); // if a runtime task exits, try to recover it, // unless it was terminal signaling a quit diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs new file mode 100644 index 000000000..eae478325 --- /dev/null +++ b/hyperdrive/src/notifications.rs @@ -0,0 +1,445 @@ +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use lib::types::core::{ + KernelMessage, LazyLoadBlob, Message, MessageReceiver, MessageSender, + PrintSender, Printout, ProcessId, Request, Response, NOTIFICATIONS_PROCESS_ID, +}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::RwLock; +use web_push::{ + ContentEncoding, IsahcWebPushClient, SubscriptionInfo, VapidSignatureBuilder, + WebPushMessageBuilder, +}; +use web_push::WebPushClient; + +// Import our types from lib +use lib::notifications::{ + NotificationsAction, NotificationsError, NotificationsResponse, +}; +use lib::core::StateAction; + +/// VAPID keys for web push notifications +#[derive(Serialize, Deserialize, Clone)] +pub struct VapidKeys { + pub public_key: String, + pub private_key: String, +} + +impl VapidKeys { + /// Generate a new pair of VAPID keys + pub fn generate() -> Result { + // Use a simple method to generate compatible keys + // Generate random bytes for private key (32 bytes for P-256) + let mut private_key_bytes = [0u8; 32]; + use rand::RngCore; + rand::thread_rng().fill_bytes(&mut private_key_bytes); + + // Use p256 crate to generate proper keys + use p256::{ + ecdsa::SigningKey, + PublicKey, + }; + + let signing_key = SigningKey::from_bytes(&private_key_bytes.into()) + .map_err(|e| NotificationsError::KeyGenerationError { + error: format!("Failed to create signing key: {:?}", e), + })?; + + let verifying_key = signing_key.verifying_key(); + let public_key_point = verifying_key.to_encoded_point(false); // false = uncompressed + let public_key_bytes = public_key_point.as_bytes(); + + if public_key_bytes.len() != 65 || public_key_bytes[0] != 0x04 { + return Err(NotificationsError::KeyGenerationError { + error: format!("Invalid public key format: len={}, first_byte=0x{:02x}", + public_key_bytes.len(), + if public_key_bytes.len() > 0 { public_key_bytes[0] } else { 0 }), + }); + } + + // Encode keys for storage + let public_key = URL_SAFE_NO_PAD.encode(public_key_bytes); + let private_key = URL_SAFE_NO_PAD.encode(&private_key_bytes); + + println!("notifications: Generated public key: {}", public_key); + println!("notifications: Public key length: {} bytes", public_key_bytes.len()); + + Ok(VapidKeys { + public_key, + private_key, + }) + } +} + +pub struct NotificationsState { + vapid_keys: Option, +} + +pub async fn notifications( + our_node: Arc, + send_to_loop: MessageSender, + send_to_terminal: PrintSender, + mut recv_notifications: MessageReceiver, + send_to_state: MessageSender, +) -> Result<(), anyhow::Error> { + println!("notifications: starting notifications module"); + + let state = Arc::new(RwLock::new(NotificationsState { + vapid_keys: None, + })); + + // Try to load existing keys from state + println!("notifications: loading keys from state"); + load_keys_from_state(&our_node, &mut recv_notifications, &send_to_state, &send_to_loop, &state).await; + println!("notifications: finished loading keys from state"); + + while let Some(km) = recv_notifications.recv().await { + if *our_node != km.source.node { + Printout::new( + 1, + NOTIFICATIONS_PROCESS_ID.clone(), + format!( + "notifications: got request from {}, but requests must come from our node {our_node}", + km.source.node + ), + ) + .send(&send_to_terminal) + .await; + continue; + } + + let state = state.clone(); + let our_node = our_node.clone(); + let send_to_loop = send_to_loop.clone(); + let send_to_terminal = send_to_terminal.clone(); + let send_to_state = send_to_state.clone(); + + tokio::spawn(async move { + if let Err(e) = handle_request( + &our_node, + km, + &send_to_loop, + &send_to_terminal, + &send_to_state, + &state, + ) + .await + { + println!("notifications: error handling request: {:?}", e); + } + }); + } + + Ok(()) +} + +async fn load_keys_from_state( + our_node: &str, + recv_notifications: &mut MessageReceiver, + send_to_state: &MessageSender, + send_to_loop: &MessageSender, + state: &Arc>, +) { + let request_id = rand::random::(); + + let km = KernelMessage::builder() + .id(request_id) + .source((our_node, NOTIFICATIONS_PROCESS_ID.clone())) + .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) + .message(Message::Request(Request { + inherit: false, + expects_response: Some(5), + body: serde_json::to_vec(&StateAction::GetState(NOTIFICATIONS_PROCESS_ID.clone())).unwrap(), + metadata: None, + capabilities: vec![], + })) + .build() + .unwrap(); + + km.send(send_to_state).await; + + // Wait for response with timeout + let timeout = tokio::time::sleep(tokio::time::Duration::from_secs(5)); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = &mut timeout => { + // Timeout reached, keys not found in state + println!("notifications: no saved keys found in state, will generate on first use"); + break; + } + Some(km) = recv_notifications.recv() => { + // Check if this is our response + if km.id == request_id { + if let Message::Response((response, _context)) = km.message { + // Check if we got the state successfully + if let Ok(state_response) = serde_json::from_slice::(&response.body) { + match state_response { + lib::core::StateResponse::GetState => { + // We got the state, deserialize the keys from context + if let Some(blob) = km.lazy_load_blob { + if let Ok(keys) = serde_json::from_slice::(&blob.bytes) { + let mut state_guard = state.write().await; + state_guard.vapid_keys = Some(keys); + println!("notifications: loaded existing VAPID keys from state"); + } + } + } + _ => {} + } + } + } + break; + } else { + // Not our response, put it back for main loop to handle + km.send(send_to_loop).await; + } + } + } + } +} + +async fn handle_request( + our_node: &str, + km: KernelMessage, + send_to_loop: &MessageSender, + send_to_terminal: &PrintSender, + send_to_state: &MessageSender, + state: &Arc>, +) -> Result<(), NotificationsError> { + let KernelMessage { + id, + source, + rsvp, + message, + .. + } = km; + + let Message::Request(Request { + expects_response, + body, + .. + }) = message + else { + return Err(NotificationsError::BadRequest { + error: "not a request".into(), + }); + }; + + let action: NotificationsAction = serde_json::from_slice(&body).map_err(|e| { + NotificationsError::BadJson { + error: format!("parse into NotificationsAction failed: {:?}", e), + } + })?; + + let response = match action { + NotificationsAction::InitializeKeys => { + println!("notifications: InitializeKeys action received"); + let keys = VapidKeys::generate()?; + + // Save keys to state + save_keys_to_state(our_node, send_to_state, &keys).await?; + + // Update our state + let mut state_guard = state.write().await; + state_guard.vapid_keys = Some(keys); + + println!("notifications: Keys initialized successfully"); + NotificationsResponse::KeysInitialized + } + NotificationsAction::GetPublicKey => { + println!("notifications: GetPublicKey action received from {:?}", source); + let state_guard = state.read().await; + match &state_guard.vapid_keys { + Some(keys) => { + println!("notifications: returning existing public key: {}", keys.public_key); + NotificationsResponse::PublicKey(keys.public_key.clone()) + } + None => { + println!("notifications: no keys found, generating new ones"); + // Try to initialize keys + drop(state_guard); + let keys = VapidKeys::generate()?; + println!("notifications: generated new keys, public key: {}", keys.public_key); + save_keys_to_state(our_node, send_to_state, &keys).await?; + + let mut state_guard = state.write().await; + let public_key = keys.public_key.clone(); + state_guard.vapid_keys = Some(keys); + + println!("notifications: returning new public key: {}", public_key); + NotificationsResponse::PublicKey(public_key) + } + } + } + NotificationsAction::SendNotification { + subscription, + title, + body, + icon, + data, + } => { + let state_guard = state.read().await; + let keys = state_guard.vapid_keys.as_ref().ok_or(NotificationsError::KeysNotInitialized)?; + + // Create subscription info for web-push + let subscription_info = SubscriptionInfo::new( + &subscription.endpoint, + &subscription.keys.p256dh, + &subscription.keys.auth, + ); + + // Build the notification payload + let payload = serde_json::json!({ + "title": title, + "body": body, + "icon": icon, + "data": data, + }); + + // Convert raw private key bytes to PEM format for web-push + println!("notifications: Starting VAPID signature creation for endpoint: {}", subscription.endpoint); + + let private_key_bytes = URL_SAFE_NO_PAD.decode(&keys.private_key) + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to decode private key: {:?}", e), + })?; + + // Convert Vec to fixed-size array + let private_key_array: [u8; 32] = private_key_bytes.try_into() + .map_err(|_| NotificationsError::WebPushError { + error: "Invalid private key length".to_string(), + })?; + + // Create PEM from raw bytes using p256 + use p256::ecdsa::SigningKey; + use p256::pkcs8::EncodePrivateKey; + + let signing_key = SigningKey::from_bytes(&private_key_array.into()) + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to create signing key: {:?}", e), + })?; + + let pem_content = signing_key.to_pkcs8_pem(p256::pkcs8::LineEnding::LF) + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to convert to PEM: {:?}", e), + })? + .to_string(); + + println!("notifications: PEM content length: {} chars", pem_content.len()); + println!("notifications: PEM header check: starts with BEGIN PRIVATE KEY: {}", + pem_content.contains("BEGIN PRIVATE KEY")); + + // Create VAPID signature from PEM + let mut sig_builder = VapidSignatureBuilder::from_pem( + pem_content.as_bytes(), + &subscription_info, + ) + .map_err(|e| { + println!("notifications: ERROR creating VAPID signature builder: {:?}", e); + NotificationsError::WebPushError { + error: format!("Failed to create VAPID signature: {:?}", e), + } + })?; + + // Add required subject claim for VAPID + sig_builder.add_claim("sub", "mailto:admin@hyperware.ai"); + + let sig_builder = sig_builder.build() + .map_err(|e| { + println!("notifications: ERROR building VAPID signature: {:?}", e); + NotificationsError::WebPushError { + error: format!("Failed to build VAPID signature: {:?}", e), + } + })?; + + println!("notifications: VAPID signature built successfully"); + + // Build the web push message + let mut message_builder = WebPushMessageBuilder::new(&subscription_info); + let payload = payload.to_string(); + message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes()); + message_builder.set_vapid_signature(sig_builder); + + let message = message_builder.build().map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to build message: {:?}", e), + })?; + + // Send the notification using IsahcWebPushClient + let client = IsahcWebPushClient::new().map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to create web push client: {:?}", e), + })?; + + client + .send(message) + .await + .map_err(|e| NotificationsError::SendError { + error: format!("Failed to send notification: {:?}", e), + })?; + + NotificationsResponse::NotificationSent + } + }; + + // Send response if expected + if let Some(target) = rsvp.or_else(|| expects_response.map(|_| source)) { + println!("notifications: sending response {:?} to {:?}", response, target); + let response_bytes = serde_json::to_vec(&response).unwrap(); + println!("notifications: response serialized to {} bytes", response_bytes.len()); + + KernelMessage::builder() + .id(id) + .source((our_node, NOTIFICATIONS_PROCESS_ID.clone())) + .target(target) + .message(Message::Response(( + Response { + inherit: false, + body: response_bytes, + metadata: None, + capabilities: vec![], + }, + None, + ))) + .build() + .unwrap() + .send(send_to_loop) + .await; + + println!("notifications: response sent"); + } + + Ok(()) +} + +async fn save_keys_to_state( + our_node: &str, + send_to_state: &MessageSender, + keys: &VapidKeys, +) -> Result<(), NotificationsError> { + let keys_bytes = serde_json::to_vec(keys).map_err(|e| NotificationsError::StateError { + error: format!("Failed to serialize keys: {:?}", e), + })?; + + KernelMessage::builder() + .id(rand::random()) + .source((our_node, NOTIFICATIONS_PROCESS_ID.clone())) + .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) + .message(Message::Request(Request { + inherit: false, + expects_response: Some(5), + body: serde_json::to_vec(&StateAction::SetState(NOTIFICATIONS_PROCESS_ID.clone())).unwrap(), + metadata: None, + capabilities: vec![], + })) + .lazy_load_blob(Some(LazyLoadBlob { + mime: Some("application/octet-stream".into()), + bytes: keys_bytes, + })) + .build() + .unwrap() + .send(send_to_state) + .await; + + Ok(()) +} diff --git a/lib/src/core.rs b/lib/src/core.rs index 308bc3b1b..8d72cb246 100644 --- a/lib/src/core.rs +++ b/lib/src/core.rs @@ -13,6 +13,7 @@ lazy_static::lazy_static! { pub static ref KERNEL_PROCESS_ID: ProcessId = ProcessId::new(Some("kernel"), "distro", "sys"); pub static ref KV_PROCESS_ID: ProcessId = ProcessId::new(Some("kv"), "distro", "sys"); pub static ref NET_PROCESS_ID: ProcessId = ProcessId::new(Some("net"), "distro", "sys"); + pub static ref NOTIFICATIONS_PROCESS_ID: ProcessId = ProcessId::new(Some("notifications"), "distro", "sys"); pub static ref STATE_PROCESS_ID: ProcessId = ProcessId::new(Some("state"), "distro", "sys"); pub static ref SQLITE_PROCESS_ID: ProcessId = ProcessId::new(Some("sqlite"), "distro", "sys"); pub static ref TERMINAL_PROCESS_ID: ProcessId = ProcessId::new(Some("terminal"), "terminal", "sys"); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f779da572..0373ac41c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -5,6 +5,7 @@ mod http; mod kernel; mod kv; mod net; +pub mod notifications; mod sqlite; mod state; mod timer; diff --git a/lib/src/notifications.rs b/lib/src/notifications.rs new file mode 100644 index 000000000..6ab7818f8 --- /dev/null +++ b/lib/src/notifications.rs @@ -0,0 +1,77 @@ +use crate::types::core::ProcessId; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// IPC Requests for the notifications:distro:sys runtime module. +#[derive(Serialize, Deserialize, Debug)] +pub enum NotificationsAction { + /// Send a push notification + SendNotification { + subscription: PushSubscription, + title: String, + body: String, + icon: Option, + data: Option, + }, + /// Get the public key for VAPID authentication + GetPublicKey, + /// Initialize or regenerate VAPID keys + InitializeKeys, +} + +/// Push subscription information from the client +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PushSubscription { + pub endpoint: String, + pub keys: SubscriptionKeys, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SubscriptionKeys { + pub p256dh: String, + pub auth: String, +} + +/// Responses for the notifications:distro:sys runtime module. +#[derive(Serialize, Deserialize, Debug)] +pub enum NotificationsResponse { + NotificationSent, + PublicKey(String), + KeysInitialized, + Err(NotificationsError), +} + +#[derive(Error, Debug, Serialize, Deserialize)] +pub enum NotificationsError { + #[error("failed to send notification: {error}")] + SendError { error: String }, + #[error("failed to generate VAPID keys: {error}")] + KeyGenerationError { error: String }, + #[error("failed to load keys from state: {error}")] + StateError { error: String }, + #[error("bad request error: {error}")] + BadRequest { error: String }, + #[error("Bad JSON blob: {error}")] + BadJson { error: String }, + #[error("VAPID keys not initialized")] + KeysNotInitialized, + #[error("web push error: {error}")] + WebPushError { error: String }, + #[error("unauthorized request from {process}")] + Unauthorized { process: ProcessId }, +} + +impl NotificationsError { + pub fn kind(&self) -> &str { + match *self { + NotificationsError::SendError { .. } => "SendError", + NotificationsError::KeyGenerationError { .. } => "KeyGenerationError", + NotificationsError::StateError { .. } => "StateError", + NotificationsError::BadRequest { .. } => "BadRequest", + NotificationsError::BadJson { .. } => "BadJson", + NotificationsError::KeysNotInitialized => "KeysNotInitialized", + NotificationsError::WebPushError { .. } => "WebPushError", + NotificationsError::Unauthorized { .. } => "Unauthorized", + } + } +} From 599061410b110e3176b99013c8ea94bf93d3e2fe Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Mon, 1 Sep 2025 21:59:44 -0700 Subject: [PATCH 45/65] notifications: get it working, on desktop and iOS --- hyperdrive/packages/homepage/ui/public/sw.js | 21 ++- .../components/Home/components/HomeScreen.tsx | 36 +++- .../Home/components/NotificationMenu.tsx | 157 ++++++++++++++++++ hyperdrive/packages/homepage/ui/src/main.tsx | 26 +++ .../ui/src/stores/notificationStore.ts | 127 ++++++++++++++ 5 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 hyperdrive/packages/homepage/ui/src/components/Home/components/NotificationMenu.tsx create mode 100644 hyperdrive/packages/homepage/ui/src/stores/notificationStore.ts diff --git a/hyperdrive/packages/homepage/ui/public/sw.js b/hyperdrive/packages/homepage/ui/public/sw.js index e8a87bacb..60d077608 100644 --- a/hyperdrive/packages/homepage/ui/public/sw.js +++ b/hyperdrive/packages/homepage/ui/public/sw.js @@ -99,8 +99,27 @@ self.addEventListener('push', (event) => { renotify: true }; + // Send notification data to all clients event.waitUntil( - self.registration.showNotification(title, options) + Promise.all([ + self.registration.showNotification(title, options), + // Notify all windows about the new notification + self.clients.matchAll({ type: 'window' }).then(clients => { + clients.forEach(client => { + client.postMessage({ + type: 'PUSH_NOTIFICATION_RECEIVED', + notification: { + title, + body: options.body, + icon: options.icon, + data: notificationData.data, + appId: notificationData.data?.appId, + appLabel: notificationData.data?.appLabel + } + }); + }); + }) + ]) ); }); diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx index b6ea5d5ab..1c39973f2 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx @@ -2,11 +2,13 @@ import React, { useMemo, useEffect } from 'react'; import { useAppStore } from '../../../stores/appStore'; import { usePersistenceStore } from '../../../stores/persistenceStore'; import { useNavigationStore } from '../../../stores/navigationStore'; +import { useNotificationStore } from '../../../stores/notificationStore'; import { Draggable } from './Draggable'; import { AppIcon } from './AppIcon'; import { Widget } from './Widget'; +import { NotificationMenu } from './NotificationMenu'; import type { HomepageApp } from '../../../types/app.types'; -import { BsCheck, BsClock, BsEnvelope, BsGridFill, BsImage, BsLayers, BsPencilSquare, BsSearch, BsX } from 'react-icons/bs'; +import { BsBell, BsCheck, BsClock, BsEnvelope, BsGridFill, BsImage, BsLayers, BsPencilSquare, BsSearch, BsX } from 'react-icons/bs'; import classNames from 'classnames'; import { Modal } from './Modal'; @@ -32,15 +34,33 @@ export const HomeScreen: React.FC = () => { } = usePersistenceStore(); const { isEditMode, setEditMode } = useAppStore(); const { openApp, toggleAppDrawer, toggleRecentApps } = useNavigationStore(); + const { + getUnreadCount, + menuOpen, + setMenuOpen, + permissionGranted, + setPermissionGranted + } = useNotificationStore(); const [draggedAppId, setDraggedAppId] = React.useState(null); const [touchDragPosition, setTouchDragPosition] = React.useState<{ x: number; y: number } | null>(null); const [showBackgroundSettings, setShowBackgroundSettings] = React.useState(false); const [showWidgetSettings, setShowWidgetSettings] = React.useState(false); const [showOnboarding, setShowOnboarding] = React.useState(!doNotShowOnboardingAgain); const [showWidgetOnboarding, setShowWidgetOnboarding] = React.useState(!doNotShowOnboardingAgain); + const unreadCount = getUnreadCount(); // console.log({ widgetSettings, appPositions }) + const handleNotificationClick = async () => { + // Request permission if not granted + if (!permissionGranted && 'Notification' in window) { + const permission = await Notification.requestPermission(); + setPermissionGranted(permission === 'granted'); + } + // Toggle menu + setMenuOpen(!menuOpen); + }; + useEffect(() => { console.log('isInitialized', isInitialized); if (isInitialized) return; @@ -624,6 +644,20 @@ export const HomeScreen: React.FC = () => { > +
+ + +
+ + + )} + +
+
+ +
+ {displayNotifications.length === 0 ? ( +
+ +

No new notifications

+
+ ) : ( +
+ {displayNotifications.map((notification) => ( +
markAsRead(notification.id)} + > +
+ {notification.icon ? ( + {notification.appLabel} + ) : ( +
+ +
+ )} +
+
+
+

+ {notification.title} +

+

+ {notification.body} +

+

+ {notification.appLabel} • {formatTimestamp(notification.timestamp)} +

+
+ {!notification.read && ( +
+ )} +
+
+
+
+ ))} +
+ )} +
+
+ ); +}; + +function formatTimestamp(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) return `${days}d ago`; + if (hours > 0) return `${hours}h ago`; + if (minutes > 0) return `${minutes}m ago`; + return 'Just now'; +} diff --git a/hyperdrive/packages/homepage/ui/src/main.tsx b/hyperdrive/packages/homepage/ui/src/main.tsx index 3e89ef8e7..591a36eac 100644 --- a/hyperdrive/packages/homepage/ui/src/main.tsx +++ b/hyperdrive/packages/homepage/ui/src/main.tsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import Home from './components/Home' import './index.css' +import { useNotificationStore } from './stores/notificationStore' // Helper function to convert base64 to Uint8Array function urlBase64ToUint8Array(base64String: string) { @@ -155,6 +156,24 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati } } +// Listen for push notification messages from service worker +if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data && event.data.type === 'PUSH_NOTIFICATION_RECEIVED') { + const notification = event.data.notification; + + // Add to notification store + useNotificationStore.getState().addNotification({ + appId: notification.appId || 'system', + appLabel: notification.appLabel || 'System', + title: notification.title, + body: notification.body, + icon: notification.icon, + }); + } + }); +} + // Register service worker for PWA if ('serviceWorker' in navigator) { window.addEventListener('load', () => { @@ -165,6 +184,13 @@ if ('serviceWorker' in navigator) { // Initialize push notifications await initializePushNotifications(registration); + // Update permission state in store + if ('Notification' in window) { + useNotificationStore.getState().setPermissionGranted( + Notification.permission === 'granted' + ); + } + // Check for updates periodically setInterval(() => { registration.update(); diff --git a/hyperdrive/packages/homepage/ui/src/stores/notificationStore.ts b/hyperdrive/packages/homepage/ui/src/stores/notificationStore.ts new file mode 100644 index 000000000..edfca67fd --- /dev/null +++ b/hyperdrive/packages/homepage/ui/src/stores/notificationStore.ts @@ -0,0 +1,127 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface AppNotification { + id: string; + appId: string; + appLabel: string; + title: string; + body: string; + icon?: string; + timestamp: number; + read: boolean; + seen: boolean; +} + +interface NotificationStore { + notifications: AppNotification[]; + permissionGranted: boolean; + menuOpen: boolean; + + // Actions + addNotification: (notification: Omit) => void; + markAsRead: (id: string) => void; + markAllAsRead: () => void; + markAsSeen: (id: string) => void; + markAllAsSeen: () => void; + clearNotifications: () => void; + setPermissionGranted: (granted: boolean) => void; + setMenuOpen: (open: boolean) => void; + + // Computed + getUnreadCount: () => number; + getUnseenCount: () => number; + getUnreadNotifications: () => AppNotification[]; + getUnseenNotifications: () => AppNotification[]; +} + +export const useNotificationStore = create()( + persist( + (set, get) => ({ + notifications: [], + permissionGranted: false, + menuOpen: false, + + addNotification: (notification) => { + const newNotification: AppNotification = { + ...notification, + id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: Date.now(), + read: false, + seen: false, + }; + + set((state) => ({ + notifications: [newNotification, ...state.notifications].slice(0, 100), // Keep max 100 notifications + })); + }, + + markAsRead: (id) => { + set((state) => ({ + notifications: state.notifications.map((n) => + n.id === id ? { ...n, read: true } : n + ), + })); + }, + + markAllAsRead: () => { + set((state) => ({ + notifications: state.notifications.map((n) => ({ ...n, read: true })), + })); + }, + + markAsSeen: (id) => { + set((state) => ({ + notifications: state.notifications.map((n) => + n.id === id ? { ...n, seen: true } : n + ), + })); + }, + + markAllAsSeen: () => { + set((state) => ({ + notifications: state.notifications.map((n) => ({ ...n, seen: true })), + })); + }, + + clearNotifications: () => { + set({ notifications: [] }); + }, + + setPermissionGranted: (granted) => { + set({ permissionGranted: granted }); + }, + + setMenuOpen: (open) => { + set({ menuOpen: open }); + // When closing menu, mark all as seen + if (!open) { + get().markAllAsSeen(); + } + }, + + getUnreadCount: () => { + return get().notifications.filter((n) => !n.read).length; + }, + + getUnseenCount: () => { + return get().notifications.filter((n) => !n.seen).length; + }, + + getUnreadNotifications: () => { + return get().notifications.filter((n) => !n.read); + }, + + getUnseenNotifications: () => { + return get().notifications.filter((n) => !n.seen); + }, + }), + { + name: 'notification-storage', + partialize: (state) => ({ + notifications: state.notifications, + permissionGranted: state.permissionGranted, + }), + } + ) +); From 4ec9a89dd59e8ae501571ac040592d6e45690c28 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 05:00:10 +0000 Subject: [PATCH 46/65] Format Rust code using rustfmt --- hyperdrive/src/notifications.rs | 176 ++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 65 deletions(-) diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs index eae478325..f15600195 100644 --- a/hyperdrive/src/notifications.rs +++ b/hyperdrive/src/notifications.rs @@ -1,22 +1,20 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use lib::types::core::{ - KernelMessage, LazyLoadBlob, Message, MessageReceiver, MessageSender, - PrintSender, Printout, ProcessId, Request, Response, NOTIFICATIONS_PROCESS_ID, + KernelMessage, LazyLoadBlob, Message, MessageReceiver, MessageSender, PrintSender, Printout, + ProcessId, Request, Response, NOTIFICATIONS_PROCESS_ID, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; +use web_push::WebPushClient; use web_push::{ ContentEncoding, IsahcWebPushClient, SubscriptionInfo, VapidSignatureBuilder, WebPushMessageBuilder, }; -use web_push::WebPushClient; // Import our types from lib -use lib::notifications::{ - NotificationsAction, NotificationsError, NotificationsResponse, -}; use lib::core::StateAction; +use lib::notifications::{NotificationsAction, NotificationsError, NotificationsResponse}; /// VAPID keys for web push notifications #[derive(Serialize, Deserialize, Clone)] @@ -35,15 +33,13 @@ impl VapidKeys { rand::thread_rng().fill_bytes(&mut private_key_bytes); // Use p256 crate to generate proper keys - use p256::{ - ecdsa::SigningKey, - PublicKey, - }; + use p256::{ecdsa::SigningKey, PublicKey}; - let signing_key = SigningKey::from_bytes(&private_key_bytes.into()) - .map_err(|e| NotificationsError::KeyGenerationError { + let signing_key = SigningKey::from_bytes(&private_key_bytes.into()).map_err(|e| { + NotificationsError::KeyGenerationError { error: format!("Failed to create signing key: {:?}", e), - })?; + } + })?; let verifying_key = signing_key.verifying_key(); let public_key_point = verifying_key.to_encoded_point(false); // false = uncompressed @@ -51,9 +47,15 @@ impl VapidKeys { if public_key_bytes.len() != 65 || public_key_bytes[0] != 0x04 { return Err(NotificationsError::KeyGenerationError { - error: format!("Invalid public key format: len={}, first_byte=0x{:02x}", + error: format!( + "Invalid public key format: len={}, first_byte=0x{:02x}", public_key_bytes.len(), - if public_key_bytes.len() > 0 { public_key_bytes[0] } else { 0 }), + if public_key_bytes.len() > 0 { + public_key_bytes[0] + } else { + 0 + } + ), }); } @@ -62,7 +64,10 @@ impl VapidKeys { let private_key = URL_SAFE_NO_PAD.encode(&private_key_bytes); println!("notifications: Generated public key: {}", public_key); - println!("notifications: Public key length: {} bytes", public_key_bytes.len()); + println!( + "notifications: Public key length: {} bytes", + public_key_bytes.len() + ); Ok(VapidKeys { public_key, @@ -84,13 +89,18 @@ pub async fn notifications( ) -> Result<(), anyhow::Error> { println!("notifications: starting notifications module"); - let state = Arc::new(RwLock::new(NotificationsState { - vapid_keys: None, - })); + let state = Arc::new(RwLock::new(NotificationsState { vapid_keys: None })); // Try to load existing keys from state println!("notifications: loading keys from state"); - load_keys_from_state(&our_node, &mut recv_notifications, &send_to_state, &send_to_loop, &state).await; + load_keys_from_state( + &our_node, + &mut recv_notifications, + &send_to_state, + &send_to_loop, + &state, + ) + .await; println!("notifications: finished loading keys from state"); while let Some(km) = recv_notifications.recv().await { @@ -149,7 +159,8 @@ async fn load_keys_from_state( .message(Message::Request(Request { inherit: false, expects_response: Some(5), - body: serde_json::to_vec(&StateAction::GetState(NOTIFICATIONS_PROCESS_ID.clone())).unwrap(), + body: serde_json::to_vec(&StateAction::GetState(NOTIFICATIONS_PROCESS_ID.clone())) + .unwrap(), metadata: None, capabilities: vec![], })) @@ -227,11 +238,10 @@ async fn handle_request( }); }; - let action: NotificationsAction = serde_json::from_slice(&body).map_err(|e| { - NotificationsError::BadJson { + let action: NotificationsAction = + serde_json::from_slice(&body).map_err(|e| NotificationsError::BadJson { error: format!("parse into NotificationsAction failed: {:?}", e), - } - })?; + })?; let response = match action { NotificationsAction::InitializeKeys => { @@ -249,11 +259,17 @@ async fn handle_request( NotificationsResponse::KeysInitialized } NotificationsAction::GetPublicKey => { - println!("notifications: GetPublicKey action received from {:?}", source); + println!( + "notifications: GetPublicKey action received from {:?}", + source + ); let state_guard = state.read().await; match &state_guard.vapid_keys { Some(keys) => { - println!("notifications: returning existing public key: {}", keys.public_key); + println!( + "notifications: returning existing public key: {}", + keys.public_key + ); NotificationsResponse::PublicKey(keys.public_key.clone()) } None => { @@ -261,7 +277,10 @@ async fn handle_request( // Try to initialize keys drop(state_guard); let keys = VapidKeys::generate()?; - println!("notifications: generated new keys, public key: {}", keys.public_key); + println!( + "notifications: generated new keys, public key: {}", + keys.public_key + ); save_keys_to_state(our_node, send_to_state, &keys).await?; let mut state_guard = state.write().await; @@ -281,7 +300,10 @@ async fn handle_request( data, } => { let state_guard = state.read().await; - let keys = state_guard.vapid_keys.as_ref().ok_or(NotificationsError::KeysNotInitialized)?; + let keys = state_guard + .vapid_keys + .as_ref() + .ok_or(NotificationsError::KeysNotInitialized)?; // Create subscription info for web-push let subscription_info = SubscriptionInfo::new( @@ -299,55 +321,68 @@ async fn handle_request( }); // Convert raw private key bytes to PEM format for web-push - println!("notifications: Starting VAPID signature creation for endpoint: {}", subscription.endpoint); + println!( + "notifications: Starting VAPID signature creation for endpoint: {}", + subscription.endpoint + ); - let private_key_bytes = URL_SAFE_NO_PAD.decode(&keys.private_key) - .map_err(|e| NotificationsError::WebPushError { + let private_key_bytes = URL_SAFE_NO_PAD.decode(&keys.private_key).map_err(|e| { + NotificationsError::WebPushError { error: format!("Failed to decode private key: {:?}", e), - })?; + } + })?; // Convert Vec to fixed-size array - let private_key_array: [u8; 32] = private_key_bytes.try_into() - .map_err(|_| NotificationsError::WebPushError { - error: "Invalid private key length".to_string(), - })?; + let private_key_array: [u8; 32] = + private_key_bytes + .try_into() + .map_err(|_| NotificationsError::WebPushError { + error: "Invalid private key length".to_string(), + })?; // Create PEM from raw bytes using p256 use p256::ecdsa::SigningKey; use p256::pkcs8::EncodePrivateKey; - let signing_key = SigningKey::from_bytes(&private_key_array.into()) - .map_err(|e| NotificationsError::WebPushError { + let signing_key = SigningKey::from_bytes(&private_key_array.into()).map_err(|e| { + NotificationsError::WebPushError { error: format!("Failed to create signing key: {:?}", e), - })?; + } + })?; - let pem_content = signing_key.to_pkcs8_pem(p256::pkcs8::LineEnding::LF) + let pem_content = signing_key + .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) .map_err(|e| NotificationsError::WebPushError { error: format!("Failed to convert to PEM: {:?}", e), })? .to_string(); - println!("notifications: PEM content length: {} chars", pem_content.len()); - println!("notifications: PEM header check: starts with BEGIN PRIVATE KEY: {}", - pem_content.contains("BEGIN PRIVATE KEY")); + println!( + "notifications: PEM content length: {} chars", + pem_content.len() + ); + println!( + "notifications: PEM header check: starts with BEGIN PRIVATE KEY: {}", + pem_content.contains("BEGIN PRIVATE KEY") + ); // Create VAPID signature from PEM - let mut sig_builder = VapidSignatureBuilder::from_pem( - pem_content.as_bytes(), - &subscription_info, - ) - .map_err(|e| { - println!("notifications: ERROR creating VAPID signature builder: {:?}", e); - NotificationsError::WebPushError { - error: format!("Failed to create VAPID signature: {:?}", e), - } - })?; + let mut sig_builder = + VapidSignatureBuilder::from_pem(pem_content.as_bytes(), &subscription_info) + .map_err(|e| { + println!( + "notifications: ERROR creating VAPID signature builder: {:?}", + e + ); + NotificationsError::WebPushError { + error: format!("Failed to create VAPID signature: {:?}", e), + } + })?; // Add required subject claim for VAPID sig_builder.add_claim("sub", "mailto:admin@hyperware.ai"); - let sig_builder = sig_builder.build() - .map_err(|e| { + let sig_builder = sig_builder.build().map_err(|e| { println!("notifications: ERROR building VAPID signature: {:?}", e); NotificationsError::WebPushError { error: format!("Failed to build VAPID signature: {:?}", e), @@ -362,14 +397,18 @@ async fn handle_request( message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes()); message_builder.set_vapid_signature(sig_builder); - let message = message_builder.build().map_err(|e| NotificationsError::WebPushError { - error: format!("Failed to build message: {:?}", e), - })?; + let message = + message_builder + .build() + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to build message: {:?}", e), + })?; // Send the notification using IsahcWebPushClient - let client = IsahcWebPushClient::new().map_err(|e| NotificationsError::WebPushError { - error: format!("Failed to create web push client: {:?}", e), - })?; + let client = + IsahcWebPushClient::new().map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to create web push client: {:?}", e), + })?; client .send(message) @@ -384,9 +423,15 @@ async fn handle_request( // Send response if expected if let Some(target) = rsvp.or_else(|| expects_response.map(|_| source)) { - println!("notifications: sending response {:?} to {:?}", response, target); + println!( + "notifications: sending response {:?} to {:?}", + response, target + ); let response_bytes = serde_json::to_vec(&response).unwrap(); - println!("notifications: response serialized to {} bytes", response_bytes.len()); + println!( + "notifications: response serialized to {} bytes", + response_bytes.len() + ); KernelMessage::builder() .id(id) @@ -428,7 +473,8 @@ async fn save_keys_to_state( .message(Message::Request(Request { inherit: false, expects_response: Some(5), - body: serde_json::to_vec(&StateAction::SetState(NOTIFICATIONS_PROCESS_ID.clone())).unwrap(), + body: serde_json::to_vec(&StateAction::SetState(NOTIFICATIONS_PROCESS_ID.clone())) + .unwrap(), metadata: None, capabilities: vec![], })) From a879ab650fda6ec3f45be6a31052a76ff16dca13 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 2 Sep 2025 16:38:12 -0700 Subject: [PATCH 47/65] move notifs to notifications:distro:sys and out of homepage --- .../packages/homepage/homepage/src/lib.rs | 306 ++++++++++++---- .../components/Home/components/HomeScreen.tsx | 4 - .../Home/components/NotificationMenu.tsx | 91 ++++- hyperdrive/packages/homepage/ui/src/main.tsx | 39 +-- hyperdrive/src/notifications.rs | 328 +++++++++++++----- lib/src/notifications.rs | 16 +- 6 files changed, 560 insertions(+), 224 deletions(-) diff --git a/hyperdrive/packages/homepage/homepage/src/lib.rs b/hyperdrive/packages/homepage/homepage/src/lib.rs index 183bf215e..d4dd979db 100644 --- a/hyperdrive/packages/homepage/homepage/src/lib.rs +++ b/hyperdrive/packages/homepage/homepage/src/lib.rs @@ -31,7 +31,6 @@ struct SubscriptionKeys { #[derive(Serialize, Deserialize, Debug)] enum NotificationsAction { SendNotification { - subscription: PushSubscription, title: String, body: String, icon: Option, @@ -39,6 +38,13 @@ enum NotificationsAction { }, GetPublicKey, InitializeKeys, + AddSubscription { + subscription: PushSubscription, + }, + RemoveSubscription { + endpoint: String, + }, + ClearSubscriptions, } #[derive(Serialize, Deserialize, Debug)] @@ -46,6 +52,9 @@ enum NotificationsResponse { NotificationSent, PublicKey(String), KeysInitialized, + SubscriptionAdded, + SubscriptionRemoved, + SubscriptionsCleared, Err(String), } @@ -61,7 +70,6 @@ fn init(our: Address) { println!("started"); let mut app_data: BTreeMap = BTreeMap::new(); - let mut push_subscription: Option = None; let mut http_server = server::HttpServer::new(5); let http_config = server::HttpBindingConfig::default(); @@ -273,8 +281,8 @@ fn init(our: Address) { .bind_http_path("/api/notifications/unsubscribe", http_config.clone()) .expect("failed to bind /api/notifications/unsubscribe"); http_server - .bind_http_path("/api/notifications/get-subscription", http_config.clone()) - .expect("failed to bind /api/notifications/get-subscription"); + .bind_http_path("/api/notifications/unsubscribe-all", http_config.clone()) + .expect("failed to bind /api/notifications/unsubscribe-all"); http_server .bind_http_path("/api/notifications/test-vapid", http_config) .expect("failed to bind /api/notifications/test-vapid"); @@ -291,14 +299,7 @@ fn init(our: Address) { hyperware_process_lib::get_typed_state(|bytes| serde_json::from_slice(bytes)) .unwrap_or(PersistedAppOrder::new()); - // Load persisted push subscription - push_subscription = hyperware_process_lib::vfs::File { - path: "/homepage:sys/push_subscription.json".to_string(), - timeout: 5, - } - .read() - .ok() - .and_then(|data| serde_json::from_slice::(&data).ok()); + // No longer loading push subscription from disk - it's now stored in notifications server loop { let Ok(ref message) = await_message() else { @@ -406,14 +407,12 @@ fn init(our: Address) { (server::HttpResponse::new(http::StatusCode::OK), None) } "/api/notifications/vapid-key" => { - println!("homepage: received request for VAPID key"); // Get VAPID public key from notifications server let notifications_address = Address::new( &our.node, ProcessId::new(Some("notifications"), "distro", "sys"), ); - println!("homepage: sending GetPublicKey request to notifications:distro:sys"); match Request::to(notifications_address) .body( serde_json::to_vec(&NotificationsAction::GetPublicKey) @@ -422,14 +421,11 @@ fn init(our: Address) { .send_and_await_response(5) { Ok(Ok(response)) => { - println!("homepage: received response from notifications module"); let response_body = response.body(); - println!("homepage: response body bytes: {:?}", response_body.len()); // Try to deserialize and log the result match serde_json::from_slice::(response_body) { Ok(NotificationsResponse::PublicKey(key)) => { - println!("homepage: successfully got public key: {}", key); ( server::HttpResponse::new(http::StatusCode::OK), Some(LazyLoadBlob::new( @@ -456,7 +452,6 @@ fn init(our: Address) { } Err(e) => { println!("homepage: failed to deserialize response: {}", e); - println!("homepage: raw response: {:?}", String::from_utf8_lossy(response_body)); ( server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), Some(LazyLoadBlob::new( @@ -523,58 +518,230 @@ fn init(our: Address) { ); }; - // Save subscription to VFS - if let Ok(data) = serde_json::to_vec(&subscription) { - let _ = hyperware_process_lib::vfs::File { - path: "/homepage:sys/push_subscription.json".to_string(), - timeout: 5, + // Send subscription to notifications server + let notifications_address = Address::new( + &our.node, + ProcessId::new(Some("notifications"), "distro", "sys"), + ); + + match Request::to(notifications_address) + .body( + serde_json::to_vec(&NotificationsAction::AddSubscription { + subscription, + }) + .unwrap(), + ) + .send_and_await_response(5) + { + Ok(Ok(response)) => { + match serde_json::from_slice::(response.body()) { + Ok(NotificationsResponse::SubscriptionAdded) => { + ( + server::HttpResponse::new(http::StatusCode::OK), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "success": true + })) + .unwrap(), + )), + ) + } + Ok(NotificationsResponse::Err(e)) => { + println!("homepage: notifications server error: {}", e); + ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": format!("Failed to add subscription: {}", e) + })) + .unwrap(), + )), + ) + } + _ => { + ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Unexpected response from notifications service" + })) + .unwrap(), + )), + ) + } + } } - .write(&data); + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Failed to contact notifications server" + })) + .unwrap(), + )), + ), } + } + "/api/notifications/unsubscribe" => { + let Ok(http::Method::POST) = incoming.method() else { + return ( + server::HttpResponse::new( + http::StatusCode::METHOD_NOT_ALLOWED, + ), + None, + ); + }; + let Some(body) = get_blob() else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + None, + ); + }; - push_subscription = Some(subscription); + // Parse the endpoint from the request body + let Ok(request_data) = serde_json::from_slice::(&body.bytes) else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + None, + ); + }; - ( - server::HttpResponse::new(http::StatusCode::OK), - Some(LazyLoadBlob::new( - Some("application/json"), - serde_json::to_vec(&serde_json::json!({ - "success": true - })) + let Some(endpoint) = request_data.get("endpoint").and_then(|e| e.as_str()) else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Missing endpoint in request body" + })) + .unwrap(), + )), + ); + }; + + // Remove specific subscription from notifications server + let notifications_address = Address::new( + &our.node, + ProcessId::new(Some("notifications"), "distro", "sys"), + ); + + match Request::to(notifications_address) + .body( + serde_json::to_vec(&NotificationsAction::RemoveSubscription { + endpoint: endpoint.to_string(), + }) .unwrap(), - )), - ) + ) + .send_and_await_response(5) + { + Ok(Ok(response)) => { + match serde_json::from_slice::(response.body()) { + Ok(NotificationsResponse::SubscriptionRemoved) => { + ( + server::HttpResponse::new(http::StatusCode::OK), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "success": true + })) + .unwrap(), + )), + ) + } + Ok(NotificationsResponse::Err(e)) => { + println!("homepage: notifications server error: {}", e); + ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": format!("Failed to remove subscription: {}", e) + })) + .unwrap(), + )), + ) + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Unexpected response from notifications service" + })) + .unwrap(), + )), + ), + } + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Failed to contact notifications server" + })) + .unwrap(), + )), + ), + } } - "/api/notifications/unsubscribe" => { - push_subscription = None; + "/api/notifications/unsubscribe-all" => { + // Clear all subscriptions from notifications server + let notifications_address = Address::new( + &our.node, + ProcessId::new(Some("notifications"), "distro", "sys"), + ); - // Remove subscription from VFS by writing empty content - let _ = hyperware_process_lib::vfs::File { - path: "/homepage:sys/push_subscription.json".to_string(), - timeout: 5, - } - .write(b""); - - ( - server::HttpResponse::new(http::StatusCode::OK), - Some(LazyLoadBlob::new( - Some("application/json"), - serde_json::to_vec(&serde_json::json!({ - "success": true - })) + match Request::to(notifications_address) + .body( + serde_json::to_vec(&NotificationsAction::ClearSubscriptions) .unwrap(), - )), - ) + ) + .send_and_await_response(5) + { + Ok(Ok(response)) => { + match serde_json::from_slice::(response.body()) { + Ok(NotificationsResponse::SubscriptionsCleared) => { + ( + server::HttpResponse::new(http::StatusCode::OK), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "success": true + })) + .unwrap(), + )), + ) + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Failed to clear subscriptions" + })) + .unwrap(), + )), + ), + } + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Failed to contact notifications server" + })) + .unwrap(), + )), + ), + } } - "/api/notifications/get-subscription" => ( - server::HttpResponse::new(http::StatusCode::OK), - Some(LazyLoadBlob::new( - Some("application/json"), - serde_json::to_vec(&push_subscription).unwrap(), - )), - ), "/api/notifications/test-vapid" => { - println!("homepage: test-vapid endpoint called"); // Get VAPID public key from notifications server let notifications_address = Address::new( @@ -705,21 +872,9 @@ fn init(our: Address) { .unwrap(); } homepage::Request::GetPushSubscription => { - // Return current push subscription if available - let resp = if let Some(ref sub) = push_subscription { - homepage::Response::PushSubscription(Some( - serde_json::to_string(&serde_json::json!({ - "endpoint": sub.endpoint, - "keys": { - "p256dh": sub.keys.p256dh, - "auth": sub.keys.auth - } - })) - .unwrap(), - )) - } else { - homepage::Response::PushSubscription(None) - }; + // Subscriptions are no longer stored in homepage - they're in the notifications server + // Return None to indicate no subscription available from homepage + let resp = homepage::Response::PushSubscription(None); Response::new() .body(serde_json::to_vec(&resp).unwrap()) .send() @@ -751,7 +906,6 @@ fn init(our: Address) { new_stylesheet_string.into(), ) .expect("failed to bind /hyperware.css"); - println!("updated hyperware.css!"); } } } diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx index 1c39973f2..a0688536a 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx @@ -49,7 +49,6 @@ export const HomeScreen: React.FC = () => { const [showWidgetOnboarding, setShowWidgetOnboarding] = React.useState(!doNotShowOnboardingAgain); const unreadCount = getUnreadCount(); - // console.log({ widgetSettings, appPositions }) const handleNotificationClick = async () => { // Request permission if not granted @@ -62,7 +61,6 @@ export const HomeScreen: React.FC = () => { }; useEffect(() => { - console.log('isInitialized', isInitialized); if (isInitialized) return; // add appstore, contacts, and settings to the homepage on initial load setIsInitialized(true); @@ -229,7 +227,6 @@ export const HomeScreen: React.FC = () => { }, [apps, homeScreenApps]); const widgetApps = useMemo(() => { - console.log({ homeApps }); return homeApps.filter(app => app.widget); }, [homeApps]); @@ -261,7 +258,6 @@ export const HomeScreen: React.FC = () => { const x = index * (screenPortion + spacing) + screenPortion / 4 // ensure position sets so future movements do not cause a jump moveItem(appId, { x, y }); - console.log('autosetting position', appId, { x, y }); return { x, y } } diff --git a/hyperdrive/packages/homepage/ui/src/components/Home/components/NotificationMenu.tsx b/hyperdrive/packages/homepage/ui/src/components/Home/components/NotificationMenu.tsx index bd48b13aa..9f615ccd4 100644 --- a/hyperdrive/packages/homepage/ui/src/components/Home/components/NotificationMenu.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/Home/components/NotificationMenu.tsx @@ -5,6 +5,11 @@ import classNames from 'classnames'; export const NotificationMenu: React.FC = () => { const menuRef = useRef(null); + const [position, setPosition] = React.useState<{ left: string; top: string; width: string }>({ + left: '0', + top: '0', + width: '320px' + }); const { notifications, menuOpen, @@ -22,6 +27,73 @@ export const NotificationMenu: React.FC = () => { : notifications // Show all if no unseen : []; + // Calculate position when menu opens + useEffect(() => { + if (menuOpen) { + const calculatePosition = () => { + const button = document.querySelector('[data-notification-button]'); + if (!button) return; + + const buttonRect = button.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const desiredWidth = 320; // Desired menu width + const margin = 16; // Minimum margin from screen edges + + // Calculate maximum possible width + const maxWidth = Math.min(viewportWidth - (2 * margin), desiredWidth); + const menuWidth = maxWidth; + + // For desktop: position menu to the left of the button, aligned with right edge + // For mobile: center the menu or align it to avoid cutoff + let leftPosition; + + if (viewportWidth > 640) { + // Desktop: align menu's right edge with button's right edge + leftPosition = buttonRect.right - menuWidth; + + // If menu would go off the left edge, shift it right + if (leftPosition < margin) { + leftPosition = margin; + } + } else { + // Mobile: ensure the menu is fully visible + // Try to center it, but ensure it doesn't go off screen + leftPosition = (viewportWidth - menuWidth) / 2; + + // Ensure it doesn't go off left edge + if (leftPosition < margin) { + leftPosition = margin; + } + + // Ensure it doesn't go off right edge + if (leftPosition + menuWidth > viewportWidth - margin) { + leftPosition = viewportWidth - margin - menuWidth; + } + } + + // Calculate top position to place menu below the button + const topPosition = buttonRect.bottom + 8; // 8px gap below button + + setPosition({ + left: `${leftPosition}px`, + top: `${topPosition}px`, + width: `${menuWidth}px` + }); + }; + + calculatePosition(); + + // Recalculate on resize and scroll to handle position changes + window.addEventListener('resize', calculatePosition); + window.addEventListener('scroll', calculatePosition); + + return () => { + window.removeEventListener('resize', calculatePosition); + window.removeEventListener('scroll', calculatePosition); + }; + } + }, [menuOpen]); + // Handle click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -44,16 +116,17 @@ export const NotificationMenu: React.FC = () => { return (

Notifications - {displayNotifications.length > 0 && ( - - ({displayNotifications.filter(n => !n.read).length} unread) - - )}

{displayNotifications.length > 0 && ( @@ -74,12 +147,6 @@ export const NotificationMenu: React.FC = () => { )} -
diff --git a/hyperdrive/packages/homepage/ui/src/main.tsx b/hyperdrive/packages/homepage/ui/src/main.tsx index 591a36eac..f39a65ca8 100644 --- a/hyperdrive/packages/homepage/ui/src/main.tsx +++ b/hyperdrive/packages/homepage/ui/src/main.tsx @@ -23,39 +23,29 @@ function urlBase64ToUint8Array(base64String: string) { // Initialize push notifications async function initializePushNotifications(registration: ServiceWorkerRegistration) { try { - console.log('[Init Push] Starting push notification initialization'); - // Check if push notifications are supported if (!('PushManager' in window)) { - console.log('[Init Push] Push notifications not supported'); return; } // Check current permission status let permission = Notification.permission; - console.log('[Init Push] Current permission status:', permission); if (permission === 'denied') { - console.log('[Init Push] Push notifications permission denied'); return; } // Request permission if not granted if (permission === 'default') { - console.log('[Init Push] Requesting notification permission...'); permission = await Notification.requestPermission(); - console.log('[Init Push] Permission result:', permission); if (permission !== 'granted') { - console.log('[Init Push] Permission not granted'); return; } } // Get VAPID public key from server - console.log('[Init Push] Fetching VAPID public key...'); const vapidResponse = await fetch('/api/notifications/vapid-key'); - console.log('[Init Push] VAPID response status:', vapidResponse.status); if (!vapidResponse.ok) { const errorText = await vapidResponse.text(); @@ -64,7 +54,6 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati } const responseData = await vapidResponse.json(); - console.log('[Init Push] VAPID response data:', responseData); const { publicKey } = responseData; if (!publicKey) { @@ -72,32 +61,15 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati return; } - console.log('[Init Push] Got VAPID public key:', publicKey); - // Check if already subscribed - console.log('[Init Push] Checking existing subscription...'); let subscription = await registration.pushManager.getSubscription(); - console.log('[Init Push] Existing subscription:', subscription); if (!subscription && permission === 'granted') { // Subscribe if we have permission but no subscription - console.log('[Init Push] Permission granted, attempting to subscribe...'); - try { // Convert the public key - console.log('[Init Push] Converting public key to Uint8Array...'); - console.log('[Init Push] Public key string length:', publicKey.length); const applicationServerKey = urlBase64ToUint8Array(publicKey); - console.log('[Init Push] Converted key length:', applicationServerKey.length, 'bytes'); - console.log('[Init Push] First byte should be 0x04 for uncompressed:', applicationServerKey[0]); - // Validate the key format - if (applicationServerKey.length !== 65) { - console.error('[Init Push] Invalid key length! Expected 65 bytes for P-256, got:', applicationServerKey.length); - } - if (applicationServerKey[0] !== 0x04) { - console.error('[Init Push] Invalid key format! First byte should be 0x04 for uncompressed, got:', applicationServerKey[0]); - } // Subscribe to push notifications subscription = await registration.pushManager.subscribe({ @@ -105,8 +77,6 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati applicationServerKey: applicationServerKey }); - console.log('[Init Push] Successfully subscribed:', subscription); - console.log('[Init Push] Subscription endpoint:', subscription.endpoint); } catch (subscribeError: any) { console.error('[Init Push] Subscribe error:', subscribeError); console.error('[Init Push] Error name:', subscribeError?.name); @@ -114,7 +84,6 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati if (subscribeError?.name === 'AbortError') { console.error('[Init Push] Push service registration was aborted - this usually means the VAPID key is invalid or malformed'); - console.error('[Init Push] VAPID key that failed:', publicKey); } // Return early if subscription failed @@ -131,14 +100,11 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati body: JSON.stringify(subscription.toJSON()) }); - if (response.ok) { - console.log('Push notification subscription successful'); - } else { + if (!response.ok) { console.error('Failed to save subscription on server'); } } } else { - console.log('Already subscribed to push notifications'); // Optionally update subscription on server to ensure it's current if (subscription) { @@ -179,7 +145,6 @@ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(async (registration) => { - console.log('SW registered:', registration); // Initialize push notifications await initializePushNotifications(registration); @@ -197,7 +162,7 @@ if ('serviceWorker' in navigator) { }, 60 * 60 * 1000); // Check every hour }) .catch((error) => { - console.log('SW registration failed:', error); + console.error('SW registration failed:', error); }); }); } diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs index f15600195..72ef04813 100644 --- a/hyperdrive/src/notifications.rs +++ b/hyperdrive/src/notifications.rs @@ -4,6 +4,7 @@ use lib::types::core::{ ProcessId, Request, Response, NOTIFICATIONS_PROCESS_ID, }; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::sync::Arc; use tokio::sync::RwLock; use web_push::WebPushClient; @@ -14,7 +15,7 @@ use web_push::{ // Import our types from lib use lib::core::StateAction; -use lib::notifications::{NotificationsAction, NotificationsError, NotificationsResponse}; +use lib::notifications::{NotificationsAction, NotificationsError, NotificationsResponse, PushSubscription, SubscriptionKeys}; /// VAPID keys for web push notifications #[derive(Serialize, Deserialize, Clone)] @@ -78,6 +79,7 @@ impl VapidKeys { pub struct NotificationsState { vapid_keys: Option, + subscriptions: Vec, } pub async fn notifications( @@ -89,7 +91,10 @@ pub async fn notifications( ) -> Result<(), anyhow::Error> { println!("notifications: starting notifications module"); - let state = Arc::new(RwLock::new(NotificationsState { vapid_keys: None })); + let state = Arc::new(RwLock::new(NotificationsState { + vapid_keys: None, + subscriptions: Vec::new(), + })); // Try to load existing keys from state println!("notifications: loading keys from state"); @@ -150,6 +155,7 @@ async fn load_keys_from_state( send_to_loop: &MessageSender, state: &Arc>, ) { + // Load VAPID keys let request_id = rand::random::(); let km = KernelMessage::builder() @@ -209,6 +215,67 @@ async fn load_keys_from_state( } } } + + // Load subscriptions + let request_id = rand::random::(); + + let km = KernelMessage::builder() + .id(request_id) + .source((our_node, NOTIFICATIONS_PROCESS_ID.clone())) + .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) + .message(Message::Request(Request { + inherit: false, + expects_response: Some(5), + body: serde_json::to_vec(&StateAction::GetState(ProcessId::new(Some("notifications-subscriptions"), "distro", "sys"))) + .unwrap(), + metadata: None, + capabilities: vec![], + })) + .build() + .unwrap(); + + km.send(send_to_state).await; + + // Wait for response with timeout + let timeout = tokio::time::sleep(tokio::time::Duration::from_secs(5)); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = &mut timeout => { + // Timeout reached, no saved subscriptions + println!("notifications: no saved subscriptions found in state"); + break; + } + Some(km) = recv_notifications.recv() => { + // Check if this is our response + if km.id == request_id { + if let Message::Response((response, _context)) = km.message { + // Check if we got the state successfully + if let Ok(state_response) = serde_json::from_slice::(&response.body) { + match state_response { + lib::core::StateResponse::GetState => { + // We got the state, deserialize the subscriptions from context + if let Some(blob) = km.lazy_load_blob { + if let Ok(subscriptions) = serde_json::from_slice::>(&blob.bytes) { + let mut state_guard = state.write().await; + state_guard.subscriptions = subscriptions; + println!("notifications: loaded {} existing subscriptions from state", state_guard.subscriptions.len()); + } + } + } + _ => {} + } + } + } + break; + } else { + // Not our response, put it back for main loop to handle + km.send(send_to_loop).await; + } + } + } + } } async fn handle_request( @@ -293,7 +360,6 @@ async fn handle_request( } } NotificationsAction::SendNotification { - subscription, title, body, icon, @@ -305,12 +371,10 @@ async fn handle_request( .as_ref() .ok_or(NotificationsError::KeysNotInitialized)?; - // Create subscription info for web-push - let subscription_info = SubscriptionInfo::new( - &subscription.endpoint, - &subscription.keys.p256dh, - &subscription.keys.auth, - ); + if state_guard.subscriptions.is_empty() { + println!("notifications: No subscriptions available to send notification"); + return Ok(()); + } // Build the notification payload let payload = serde_json::json!({ @@ -320,104 +384,149 @@ async fn handle_request( "data": data, }); - // Convert raw private key bytes to PEM format for web-push - println!( - "notifications: Starting VAPID signature creation for endpoint: {}", - subscription.endpoint - ); + println!("notifications: Sending notification to {} devices", state_guard.subscriptions.len()); - let private_key_bytes = URL_SAFE_NO_PAD.decode(&keys.private_key).map_err(|e| { - NotificationsError::WebPushError { - error: format!("Failed to decode private key: {:?}", e), - } - })?; - - // Convert Vec to fixed-size array - let private_key_array: [u8; 32] = - private_key_bytes - .try_into() - .map_err(|_| NotificationsError::WebPushError { - error: "Invalid private key length".to_string(), - })?; + // Send to all subscriptions + let mut send_errors = Vec::new(); + let mut send_count = 0; - // Create PEM from raw bytes using p256 - use p256::ecdsa::SigningKey; - use p256::pkcs8::EncodePrivateKey; + for subscription in &state_guard.subscriptions { + // Create subscription info for web-push + let subscription_info = SubscriptionInfo::new( + &subscription.endpoint, + &subscription.keys.p256dh, + &subscription.keys.auth, + ); - let signing_key = SigningKey::from_bytes(&private_key_array.into()).map_err(|e| { - NotificationsError::WebPushError { - error: format!("Failed to create signing key: {:?}", e), - } - })?; + // Convert raw private key bytes to PEM format for web-push + let private_key_bytes = URL_SAFE_NO_PAD.decode(&keys.private_key).map_err(|e| { + NotificationsError::WebPushError { + error: format!("Failed to decode private key: {:?}", e), + } + })?; - let pem_content = signing_key - .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) - .map_err(|e| NotificationsError::WebPushError { - error: format!("Failed to convert to PEM: {:?}", e), - })? - .to_string(); + // Convert Vec to fixed-size array + let private_key_array: [u8; 32] = + private_key_bytes + .try_into() + .map_err(|_| NotificationsError::WebPushError { + error: "Invalid private key length".to_string(), + })?; + + // Create PEM from raw bytes using p256 + use p256::ecdsa::SigningKey; + use p256::pkcs8::EncodePrivateKey; + + let signing_key = SigningKey::from_bytes(&private_key_array.into()).map_err(|e| { + NotificationsError::WebPushError { + error: format!("Failed to create signing key: {:?}", e), + } + })?; - println!( - "notifications: PEM content length: {} chars", - pem_content.len() - ); - println!( - "notifications: PEM header check: starts with BEGIN PRIVATE KEY: {}", - pem_content.contains("BEGIN PRIVATE KEY") - ); + let pem_content = signing_key + .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to convert to PEM: {:?}", e), + })? + .to_string(); + + // Create VAPID signature from PEM + let mut sig_builder = + VapidSignatureBuilder::from_pem(pem_content.as_bytes(), &subscription_info) + .map_err(|e| { + NotificationsError::WebPushError { + error: format!("Failed to create VAPID signature: {:?}", e), + } + })?; - // Create VAPID signature from PEM - let mut sig_builder = - VapidSignatureBuilder::from_pem(pem_content.as_bytes(), &subscription_info) - .map_err(|e| { - println!( - "notifications: ERROR creating VAPID signature builder: {:?}", - e - ); - NotificationsError::WebPushError { - error: format!("Failed to create VAPID signature: {:?}", e), - } - })?; + // Add required subject claim for VAPID + sig_builder.add_claim("sub", "mailto:admin@hyperware.ai"); - // Add required subject claim for VAPID - sig_builder.add_claim("sub", "mailto:admin@hyperware.ai"); + let sig_builder = sig_builder.build().map_err(|e| { + NotificationsError::WebPushError { + error: format!("Failed to build VAPID signature: {:?}", e), + } + })?; + + // Build the web push message + let mut message_builder = WebPushMessageBuilder::new(&subscription_info); + let payload_str = payload.to_string(); + message_builder.set_payload(ContentEncoding::Aes128Gcm, payload_str.as_bytes()); + message_builder.set_vapid_signature(sig_builder); + + let message = + message_builder + .build() + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to build message: {:?}", e), + })?; + + // Send the notification using IsahcWebPushClient + let client = + IsahcWebPushClient::new().map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to create web push client: {:?}", e), + })?; - let sig_builder = sig_builder.build().map_err(|e| { - println!("notifications: ERROR building VAPID signature: {:?}", e); - NotificationsError::WebPushError { - error: format!("Failed to build VAPID signature: {:?}", e), + match client.send(message).await { + Ok(_) => { + send_count += 1; + } + Err(e) => { + println!("notifications: Failed to send to {}: {:?}", subscription.endpoint, e); + send_errors.push(format!("Failed to send to endpoint: {:?}", e)); + } } - })?; + } - println!("notifications: VAPID signature built successfully"); + println!("notifications: Sent to {}/{} devices", send_count, state_guard.subscriptions.len()); - // Build the web push message - let mut message_builder = WebPushMessageBuilder::new(&subscription_info); - let payload = payload.to_string(); - message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes()); - message_builder.set_vapid_signature(sig_builder); + NotificationsResponse::NotificationSent + } + NotificationsAction::AddSubscription { subscription } => { + let mut state_guard = state.write().await; - let message = - message_builder - .build() - .map_err(|e| NotificationsError::WebPushError { - error: format!("Failed to build message: {:?}", e), - })?; + // Check if subscription already exists (by endpoint) + if !state_guard.subscriptions.iter().any(|s| s.endpoint == subscription.endpoint) { + state_guard.subscriptions.push(subscription.clone()); + println!("notifications: Added subscription, total: {}", state_guard.subscriptions.len()); + + // Save subscriptions to state + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + } else { + println!("notifications: Subscription already exists, updating it"); + // Update existing subscription + if let Some(existing) = state_guard.subscriptions.iter_mut().find(|s| s.endpoint == subscription.endpoint) { + *existing = subscription; + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + } + } - // Send the notification using IsahcWebPushClient - let client = - IsahcWebPushClient::new().map_err(|e| NotificationsError::WebPushError { - error: format!("Failed to create web push client: {:?}", e), - })?; + NotificationsResponse::SubscriptionAdded + } + NotificationsAction::RemoveSubscription { endpoint } => { + let mut state_guard = state.write().await; + let initial_len = state_guard.subscriptions.len(); + state_guard.subscriptions.retain(|s| s.endpoint != endpoint); + + if state_guard.subscriptions.len() < initial_len { + println!("notifications: Removed subscription, remaining: {}", state_guard.subscriptions.len()); + // Save updated subscriptions to state + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + NotificationsResponse::SubscriptionRemoved + } else { + println!("notifications: Subscription not found to remove"); + NotificationsResponse::SubscriptionRemoved + } + } + NotificationsAction::ClearSubscriptions => { + let mut state_guard = state.write().await; + state_guard.subscriptions.clear(); + println!("notifications: Cleared all subscriptions"); - client - .send(message) - .await - .map_err(|e| NotificationsError::SendError { - error: format!("Failed to send notification: {:?}", e), - })?; + // Save empty subscriptions to state + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; - NotificationsResponse::NotificationSent + NotificationsResponse::SubscriptionsCleared } }; @@ -472,7 +581,7 @@ async fn save_keys_to_state( .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) .message(Message::Request(Request { inherit: false, - expects_response: Some(5), + expects_response: None, // Don't expect a response to avoid polluting the main loop body: serde_json::to_vec(&StateAction::SetState(NOTIFICATIONS_PROCESS_ID.clone())) .unwrap(), metadata: None, @@ -489,3 +598,36 @@ async fn save_keys_to_state( Ok(()) } + +async fn save_subscriptions_to_state( + our_node: &str, + send_to_state: &MessageSender, + subscriptions: &[PushSubscription], +) -> Result<(), NotificationsError> { + let subscriptions_bytes = serde_json::to_vec(subscriptions).map_err(|e| NotificationsError::StateError { + error: format!("Failed to serialize subscriptions: {:?}", e), + })?; + + KernelMessage::builder() + .id(rand::random()) + .source((our_node, NOTIFICATIONS_PROCESS_ID.clone())) + .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) + .message(Message::Request(Request { + inherit: false, + expects_response: None, // Don't expect a response to avoid polluting the main loop + body: serde_json::to_vec(&StateAction::SetState(ProcessId::new(Some("notifications-subscriptions"), "distro", "sys"))) + .unwrap(), + metadata: None, + capabilities: vec![], + })) + .lazy_load_blob(Some(LazyLoadBlob { + mime: Some("application/octet-stream".into()), + bytes: subscriptions_bytes, + })) + .build() + .unwrap() + .send(send_to_state) + .await; + + Ok(()) +} diff --git a/lib/src/notifications.rs b/lib/src/notifications.rs index 6ab7818f8..86f8bd9a9 100644 --- a/lib/src/notifications.rs +++ b/lib/src/notifications.rs @@ -5,9 +5,8 @@ use thiserror::Error; /// IPC Requests for the notifications:distro:sys runtime module. #[derive(Serialize, Deserialize, Debug)] pub enum NotificationsAction { - /// Send a push notification + /// Send a push notification to all registered devices SendNotification { - subscription: PushSubscription, title: String, body: String, icon: Option, @@ -17,6 +16,16 @@ pub enum NotificationsAction { GetPublicKey, /// Initialize or regenerate VAPID keys InitializeKeys, + /// Add a push subscription for a device + AddSubscription { + subscription: PushSubscription, + }, + /// Remove a push subscription + RemoveSubscription { + endpoint: String, + }, + /// Clear all subscriptions + ClearSubscriptions, } /// Push subscription information from the client @@ -38,6 +47,9 @@ pub enum NotificationsResponse { NotificationSent, PublicKey(String), KeysInitialized, + SubscriptionAdded, + SubscriptionRemoved, + SubscriptionsCleared, Err(NotificationsError), } From 5cb876c3f122f0bb215425f4a1df81a4c278a095 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 2 Sep 2025 21:38:58 -0700 Subject: [PATCH 48/65] revoke and re-add notifications --- .../packages/homepage/homepage/src/lib.rs | 111 +++++++++++ .../src/components/NotificationSettings.tsx | 173 +++++++++++++++++- hyperdrive/packages/homepage/ui/src/main.tsx | 138 +++++++++----- hyperdrive/src/notifications.rs | 51 +++++- lib/src/notifications.rs | 7 + 5 files changed, 434 insertions(+), 46 deletions(-) diff --git a/hyperdrive/packages/homepage/homepage/src/lib.rs b/hyperdrive/packages/homepage/homepage/src/lib.rs index d4dd979db..d7a0d901a 100644 --- a/hyperdrive/packages/homepage/homepage/src/lib.rs +++ b/hyperdrive/packages/homepage/homepage/src/lib.rs @@ -19,6 +19,7 @@ type PersistedAppOrder = HashMap; struct PushSubscription { endpoint: String, keys: SubscriptionKeys, + created_at: u64, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -45,6 +46,9 @@ enum NotificationsAction { endpoint: String, }, ClearSubscriptions, + GetSubscription { + endpoint: String, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -55,6 +59,7 @@ enum NotificationsResponse { SubscriptionAdded, SubscriptionRemoved, SubscriptionsCleared, + SubscriptionInfo(Option), Err(String), } @@ -280,6 +285,9 @@ fn init(our: Address) { http_server .bind_http_path("/api/notifications/unsubscribe", http_config.clone()) .expect("failed to bind /api/notifications/unsubscribe"); + http_server + .bind_http_path("/api/notifications/subscription-info", http_config.clone()) + .expect("failed to bind /api/notifications/subscription-info"); http_server .bind_http_path("/api/notifications/unsubscribe-all", http_config.clone()) .expect("failed to bind /api/notifications/unsubscribe-all"); @@ -689,6 +697,109 @@ fn init(our: Address) { ), } } + "/api/notifications/subscription-info" => { + let Ok(http::Method::POST) = incoming.method() else { + return ( + server::HttpResponse::new( + http::StatusCode::METHOD_NOT_ALLOWED, + ), + None, + ); + }; + let Some(body) = get_blob() else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + None, + ); + }; + + // Parse the endpoint from the request body + let Ok(request_data) = serde_json::from_slice::(&body.bytes) else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + None, + ); + }; + + let Some(endpoint) = request_data.get("endpoint").and_then(|e| e.as_str()) else { + return ( + server::HttpResponse::new(http::StatusCode::BAD_REQUEST), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Missing endpoint in request body" + })) + .unwrap(), + )), + ); + }; + + // Get subscription info from notifications server + let notifications_address = Address::new( + &our.node, + ProcessId::new(Some("notifications"), "distro", "sys"), + ); + + match Request::to(notifications_address) + .body( + serde_json::to_vec(&NotificationsAction::GetSubscription { + endpoint: endpoint.to_string(), + }) + .unwrap(), + ) + .send_and_await_response(5) + { + Ok(Ok(response)) => { + match serde_json::from_slice::(response.body()) { + Ok(NotificationsResponse::SubscriptionInfo(sub_option)) => { + ( + server::HttpResponse::new(http::StatusCode::OK), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "subscription": sub_option + })) + .unwrap(), + )), + ) + } + Ok(NotificationsResponse::Err(e)) => { + println!("homepage: notifications server error: {}", e); + ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": format!("Failed to get subscription info: {}", e) + })) + .unwrap(), + )), + ) + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Unexpected response from notifications service" + })) + .unwrap(), + )), + ), + } + } + _ => ( + server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR), + Some(LazyLoadBlob::new( + Some("application/json"), + serde_json::to_vec(&serde_json::json!({ + "error": "Failed to contact notifications server" + })) + .unwrap(), + )), + ), + } + } "/api/notifications/unsubscribe-all" => { // Clear all subscriptions from notifications server let notifications_address = Address::new( diff --git a/hyperdrive/packages/homepage/ui/src/components/NotificationSettings.tsx b/hyperdrive/packages/homepage/ui/src/components/NotificationSettings.tsx index 136fc323d..cbb7b1147 100644 --- a/hyperdrive/packages/homepage/ui/src/components/NotificationSettings.tsx +++ b/hyperdrive/packages/homepage/ui/src/components/NotificationSettings.tsx @@ -46,7 +46,19 @@ export const NotificationSettings: React.FC = ({ onCl const subscription = await registration.pushManager.getSubscription(); console.log('Current push subscription:', subscription); - setIsSubscribed(!!subscription); + + if (subscription) { + // Check subscription age and renew if needed + await checkAndRenewSubscription(subscription); + } else if (Notification.permission === 'granted') { + // No subscription exists but we have permission, create one + console.log('No subscription found but permission granted, creating new subscription'); + await createNewSubscription(); + } + + // Re-check subscription status after potential creation/renewal + const updatedSubscription = await registration.pushManager.getSubscription(); + setIsSubscribed(!!updatedSubscription); } catch (error) { console.error('Error checking subscription status:', error); } @@ -103,14 +115,18 @@ export const NotificationSettings: React.FC = ({ onCl console.log('Successfully subscribed:', subscription); console.log('Subscription endpoint:', subscription.endpoint); - // Send subscription to server + // Send subscription to server with timestamp console.log('Sending subscription to server...'); + const subscriptionData = subscription.toJSON(); const response = await fetch('/api/notifications/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(subscription.toJSON()) + body: JSON.stringify({ + ...subscriptionData, + created_at: Date.now() + }) }); console.log('Server response status:', response.status); @@ -188,6 +204,157 @@ export const NotificationSettings: React.FC = ({ onCl } }; + const checkAndRenewSubscription = async (subscription: PushSubscription) => { + try { + console.log('Checking subscription age for endpoint:', subscription.endpoint); + + // Get subscription info from server to check age + const response = await fetch('/api/notifications/subscription-info', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ endpoint: subscription.endpoint }) + }); + + if (!response.ok) { + console.error('Failed to get subscription info'); + // If we can't get info, assume it needs renewal + await renewSubscription(subscription); + return; + } + + const data = await response.json(); + + if (!data.subscription || !data.subscription.created_at) { + console.log('No subscription found on server, creating new one'); + await renewSubscription(subscription); + return; + } + + // Check age + const now = Date.now(); + const createdAt = data.subscription.created_at; + const ageMs = now - createdAt; + const oneWeekMs = 7 * 24 * 60 * 60 * 1000; + const oneMonthMs = 30 * 24 * 60 * 60 * 1000; + + console.log('Subscription age:', Math.floor(ageMs / 1000 / 60 / 60), 'hours'); + + if (ageMs > oneMonthMs) { + console.log('Subscription older than 1 month, removing'); + // Remove old subscription + await subscription.unsubscribe(); + setIsSubscribed(false); + // Server will auto-remove on next interaction + } else if (ageMs > oneWeekMs) { + console.log('Subscription older than 1 week, renewing'); + await renewSubscription(subscription); + } else { + console.log('Subscription is fresh, no renewal needed'); + } + } catch (error) { + console.error('Error checking subscription age:', error); + // On error, try to renew to be safe + await renewSubscription(subscription); + } + }; + + const createNewSubscription = async () => { + try { + console.log('Creating new subscription...'); + + // Get VAPID key + const vapidResponse = await fetch('/api/notifications/vapid-key'); + if (!vapidResponse.ok) { + throw new Error('Failed to get VAPID key'); + } + + const { publicKey } = await vapidResponse.json(); + const applicationServerKey = urlBase64ToUint8Array(publicKey); + + // Create new subscription + const registration = await navigator.serviceWorker.ready; + const newSubscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey + }); + + // Send to server with timestamp + const subscriptionData = newSubscription.toJSON(); + const response = await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...subscriptionData, + created_at: Date.now() + }) + }); + + if (response.ok) { + console.log('Successfully created new subscription'); + setIsSubscribed(true); + return true; + } else { + throw new Error('Server rejected subscription'); + } + } catch (error) { + console.error('Error creating new subscription:', error); + setIsSubscribed(false); + return false; + } + }; + + const renewSubscription = async (oldSubscription: PushSubscription) => { + try { + console.log('Renewing subscription...'); + + // Unsubscribe old + await oldSubscription.unsubscribe(); + + // Get VAPID key + const vapidResponse = await fetch('/api/notifications/vapid-key'); + if (!vapidResponse.ok) { + throw new Error('Failed to get VAPID key'); + } + + const { publicKey } = await vapidResponse.json(); + const applicationServerKey = urlBase64ToUint8Array(publicKey); + + // Create new subscription + const registration = await navigator.serviceWorker.ready; + const newSubscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey + }); + + // Send to server with timestamp + const subscriptionData = newSubscription.toJSON(); + const response = await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...subscriptionData, + created_at: Date.now() + }) + }); + + if (response.ok) { + console.log('Successfully renewed subscription'); + setIsSubscribed(true); + } else { + throw new Error('Server rejected renewed subscription'); + } + } catch (error) { + console.error('Error renewing subscription:', error); + setIsSubscribed(false); + } + }; + const urlBase64ToUint8Array = (base64String: string) => { console.log('Converting base64 string:', base64String); console.log('Base64 string length:', base64String.length); diff --git a/hyperdrive/packages/homepage/ui/src/main.tsx b/hyperdrive/packages/homepage/ui/src/main.tsx index f39a65ca8..bbf5dbc9e 100644 --- a/hyperdrive/packages/homepage/ui/src/main.tsx +++ b/hyperdrive/packages/homepage/ui/src/main.tsx @@ -64,57 +64,107 @@ async function initializePushNotifications(registration: ServiceWorkerRegistrati // Check if already subscribed let subscription = await registration.pushManager.getSubscription(); - if (!subscription && permission === 'granted') { - // Subscribe if we have permission but no subscription - try { - // Convert the public key - const applicationServerKey = urlBase64ToUint8Array(publicKey); - - - // Subscribe to push notifications - subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: applicationServerKey - }); + if (subscription) { + // Check subscription age and renew if needed + console.log('[Init Push] Checking existing subscription age'); - } catch (subscribeError: any) { - console.error('[Init Push] Subscribe error:', subscribeError); - console.error('[Init Push] Error name:', subscribeError?.name); - console.error('[Init Push] Error message:', subscribeError?.message); - - if (subscribeError?.name === 'AbortError') { - console.error('[Init Push] Push service registration was aborted - this usually means the VAPID key is invalid or malformed'); - } - - // Return early if subscription failed - return; - } - - // Send subscription to server (only if we have a subscription) - if (subscription) { - const response = await fetch('/api/notifications/subscribe', { + try { + const response = await fetch('/api/notifications/subscription-info', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(subscription.toJSON()) + body: JSON.stringify({ endpoint: subscription.endpoint }) }); - if (!response.ok) { - console.error('Failed to save subscription on server'); + if (response.ok) { + const data = await response.json(); + + if (!data.subscription || !data.subscription.created_at) { + console.log('[Init Push] No subscription found on server, will create new one'); + // Unsubscribe and create new + await subscription!.unsubscribe(); + subscription = null; + } else { + // Check age + const now = Date.now(); + const createdAt = data.subscription.created_at; + const ageMs = now - createdAt; + const oneWeekMs = 7 * 24 * 60 * 60 * 1000; + const oneMonthMs = 30 * 24 * 60 * 60 * 1000; + + console.log('[Init Push] Subscription age:', Math.floor(ageMs / 1000 / 60 / 60), 'hours'); + + if (ageMs > oneMonthMs) { + console.log('[Init Push] Subscription older than 1 month, removing'); + await subscription!.unsubscribe(); + subscription = null; + } else if (ageMs > oneWeekMs) { + console.log('[Init Push] Subscription older than 1 week, renewing'); + await subscription!.unsubscribe(); + subscription = null; + } + } + } else { + console.log('[Init Push] Could not check subscription age, will create new one'); + await subscription!.unsubscribe(); + subscription = null; + } + } catch (error) { + console.error('[Init Push] Error checking subscription:', error); + // On error, try to create fresh subscription + if (subscription) { + await subscription.unsubscribe(); + subscription = null; } } - } else { + } - // Optionally update subscription on server to ensure it's current - if (subscription) { - await fetch('/api/notifications/subscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(subscription.toJSON()) + if (!subscription && permission === 'granted') { + // Subscribe if we have permission but no subscription + try { + // Convert the public key + const applicationServerKey = urlBase64ToUint8Array(publicKey); + + // Subscribe to push notifications + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey }); + + console.log('[Init Push] Created new subscription'); + } catch (subscribeError: any) { + console.error('[Init Push] Subscribe error:', subscribeError); + console.error('[Init Push] Error name:', subscribeError?.name); + console.error('[Init Push] Error message:', subscribeError?.message); + + if (subscribeError?.name === 'AbortError') { + console.error('[Init Push] Push service registration was aborted - this usually means the VAPID key is invalid or malformed'); + } + + // Return early if subscription failed + return; + } + } + + // Send subscription to server (only if we have a subscription) + if (subscription) { + const subscriptionData = subscription.toJSON(); + const response = await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...subscriptionData, + created_at: Date.now() + }) + }); + + if (!response.ok) { + console.error('[Init Push] Failed to save subscription on server'); + } else { + console.log('[Init Push] Successfully saved subscription on server'); } } } catch (error) { @@ -128,10 +178,14 @@ if ('serviceWorker' in navigator) { if (event.data && event.data.type === 'PUSH_NOTIFICATION_RECEIVED') { const notification = event.data.notification; + // Extract appId and appLabel from data if available + const appId = notification.data?.appId || notification.appId || 'system'; + const appLabel = notification.data?.appLabel || notification.appLabel || 'System'; + // Add to notification store useNotificationStore.getState().addNotification({ - appId: notification.appId || 'system', - appLabel: notification.appLabel || 'System', + appId, + appLabel, title: notification.title, body: notification.body, icon: notification.icon, diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs index 72ef04813..5b06a5040 100644 --- a/hyperdrive/src/notifications.rs +++ b/hyperdrive/src/notifications.rs @@ -482,9 +482,17 @@ async fn handle_request( NotificationsResponse::NotificationSent } - NotificationsAction::AddSubscription { subscription } => { + NotificationsAction::AddSubscription { mut subscription } => { let mut state_guard = state.write().await; + // Set created_at timestamp if not provided (for backward compatibility) + if subscription.created_at == 0 { + subscription.created_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + } + // Check if subscription already exists (by endpoint) if !state_guard.subscriptions.iter().any(|s| s.endpoint == subscription.endpoint) { state_guard.subscriptions.push(subscription.clone()); @@ -501,6 +509,28 @@ async fn handle_request( } } + // Clean up old subscriptions (older than 1 month) + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let one_month_ms = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds + + let initial_count = state_guard.subscriptions.len(); + state_guard.subscriptions.retain(|s| { + let age = now.saturating_sub(s.created_at); + if age > one_month_ms { + println!("notifications: Removing old subscription ({}ms old): {}", age, s.endpoint); + false + } else { + true + } + }); + + if state_guard.subscriptions.len() < initial_count { + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + } + NotificationsResponse::SubscriptionAdded } NotificationsAction::RemoveSubscription { endpoint } => { @@ -528,6 +558,25 @@ async fn handle_request( NotificationsResponse::SubscriptionsCleared } + NotificationsAction::GetSubscription { endpoint } => { + let state_guard = state.read().await; + let subscription = state_guard.subscriptions.iter() + .find(|s| s.endpoint == endpoint) + .cloned(); + + if let Some(ref sub) = subscription { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + let age_ms = now.saturating_sub(sub.created_at); + println!("notifications: Found subscription for endpoint, age: {}ms", age_ms); + } else { + println!("notifications: No subscription found for endpoint: {}", endpoint); + } + + NotificationsResponse::SubscriptionInfo(subscription) + } }; // Send response if expected diff --git a/lib/src/notifications.rs b/lib/src/notifications.rs index 86f8bd9a9..74d692407 100644 --- a/lib/src/notifications.rs +++ b/lib/src/notifications.rs @@ -26,6 +26,10 @@ pub enum NotificationsAction { }, /// Clear all subscriptions ClearSubscriptions, + /// Get subscription info by endpoint + GetSubscription { + endpoint: String, + }, } /// Push subscription information from the client @@ -33,6 +37,8 @@ pub enum NotificationsAction { pub struct PushSubscription { pub endpoint: String, pub keys: SubscriptionKeys, + /// Timestamp when the subscription was created (milliseconds since epoch) + pub created_at: u64, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -50,6 +56,7 @@ pub enum NotificationsResponse { SubscriptionAdded, SubscriptionRemoved, SubscriptionsCleared, + SubscriptionInfo(Option), Err(NotificationsError), } From 2b708fe773b540dd8227fe4e79dd114cc27b5e4d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 2 Sep 2025 22:07:10 -0700 Subject: [PATCH 49/65] notifications: make prints nicer --- hyperdrive/src/notifications.rs | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs index 5b06a5040..63c2c8cce 100644 --- a/hyperdrive/src/notifications.rs +++ b/hyperdrive/src/notifications.rs @@ -64,9 +64,9 @@ impl VapidKeys { let public_key = URL_SAFE_NO_PAD.encode(public_key_bytes); let private_key = URL_SAFE_NO_PAD.encode(&private_key_bytes); - println!("notifications: Generated public key: {}", public_key); + println!("notifications: Generated public key: {}\r", public_key); println!( - "notifications: Public key length: {} bytes", + "notifications: Public key length: {} bytes\r", public_key_bytes.len() ); @@ -89,7 +89,7 @@ pub async fn notifications( mut recv_notifications: MessageReceiver, send_to_state: MessageSender, ) -> Result<(), anyhow::Error> { - println!("notifications: starting notifications module"); + println!("notifications: starting notifications module\r"); let state = Arc::new(RwLock::new(NotificationsState { vapid_keys: None, @@ -97,7 +97,7 @@ pub async fn notifications( })); // Try to load existing keys from state - println!("notifications: loading keys from state"); + println!("notifications: loading keys from state\r"); load_keys_from_state( &our_node, &mut recv_notifications, @@ -106,7 +106,7 @@ pub async fn notifications( &state, ) .await; - println!("notifications: finished loading keys from state"); + println!("notifications: finished loading keys from state\r"); while let Some(km) = recv_notifications.recv().await { if *our_node != km.source.node { @@ -140,7 +140,7 @@ pub async fn notifications( ) .await { - println!("notifications: error handling request: {:?}", e); + println!("notifications: error handling request: {:?}\r", e); } }); } @@ -183,7 +183,7 @@ async fn load_keys_from_state( tokio::select! { _ = &mut timeout => { // Timeout reached, keys not found in state - println!("notifications: no saved keys found in state, will generate on first use"); + println!("notifications: no saved keys found in state, will generate on first use\r"); break; } Some(km) = recv_notifications.recv() => { @@ -199,7 +199,7 @@ async fn load_keys_from_state( if let Ok(keys) = serde_json::from_slice::(&blob.bytes) { let mut state_guard = state.write().await; state_guard.vapid_keys = Some(keys); - println!("notifications: loaded existing VAPID keys from state"); + println!("notifications: loaded existing VAPID keys from state\r"); } } } @@ -244,7 +244,7 @@ async fn load_keys_from_state( tokio::select! { _ = &mut timeout => { // Timeout reached, no saved subscriptions - println!("notifications: no saved subscriptions found in state"); + println!("notifications: no saved subscriptions found in state\r"); break; } Some(km) = recv_notifications.recv() => { @@ -260,7 +260,7 @@ async fn load_keys_from_state( if let Ok(subscriptions) = serde_json::from_slice::>(&blob.bytes) { let mut state_guard = state.write().await; state_guard.subscriptions = subscriptions; - println!("notifications: loaded {} existing subscriptions from state", state_guard.subscriptions.len()); + println!("notifications: loaded {} existing subscriptions from state\r", state_guard.subscriptions.len()); } } } @@ -312,7 +312,7 @@ async fn handle_request( let response = match action { NotificationsAction::InitializeKeys => { - println!("notifications: InitializeKeys action received"); + println!("notifications: InitializeKeys action received\r"); let keys = VapidKeys::generate()?; // Save keys to state @@ -322,30 +322,30 @@ async fn handle_request( let mut state_guard = state.write().await; state_guard.vapid_keys = Some(keys); - println!("notifications: Keys initialized successfully"); + println!("notifications: Keys initialized successfully\r"); NotificationsResponse::KeysInitialized } NotificationsAction::GetPublicKey => { println!( - "notifications: GetPublicKey action received from {:?}", + "notifications: GetPublicKey action received from {:?}\r", source ); let state_guard = state.read().await; match &state_guard.vapid_keys { Some(keys) => { println!( - "notifications: returning existing public key: {}", + "notifications: returning existing public key: {}\r", keys.public_key ); NotificationsResponse::PublicKey(keys.public_key.clone()) } None => { - println!("notifications: no keys found, generating new ones"); + println!("notifications: no keys found, generating new ones\r"); // Try to initialize keys drop(state_guard); let keys = VapidKeys::generate()?; println!( - "notifications: generated new keys, public key: {}", + "notifications: generated new keys, public key: {}\r", keys.public_key ); save_keys_to_state(our_node, send_to_state, &keys).await?; @@ -354,7 +354,7 @@ async fn handle_request( let public_key = keys.public_key.clone(); state_guard.vapid_keys = Some(keys); - println!("notifications: returning new public key: {}", public_key); + println!("notifications: returning new public key: {}\r", public_key); NotificationsResponse::PublicKey(public_key) } } @@ -372,7 +372,7 @@ async fn handle_request( .ok_or(NotificationsError::KeysNotInitialized)?; if state_guard.subscriptions.is_empty() { - println!("notifications: No subscriptions available to send notification"); + println!("notifications: No subscriptions available to send notification\r"); return Ok(()); } @@ -384,7 +384,7 @@ async fn handle_request( "data": data, }); - println!("notifications: Sending notification to {} devices", state_guard.subscriptions.len()); + println!("notifications: Sending notification to {} devices\r", state_guard.subscriptions.len()); // Send to all subscriptions let mut send_errors = Vec::new(); @@ -472,13 +472,13 @@ async fn handle_request( send_count += 1; } Err(e) => { - println!("notifications: Failed to send to {}: {:?}", subscription.endpoint, e); + println!("notifications: Failed to send to {}: {:?}\r", subscription.endpoint, e); send_errors.push(format!("Failed to send to endpoint: {:?}", e)); } } } - println!("notifications: Sent to {}/{} devices", send_count, state_guard.subscriptions.len()); + println!("notifications: Sent to {}/{} devices\r", send_count, state_guard.subscriptions.len()); NotificationsResponse::NotificationSent } @@ -496,12 +496,12 @@ async fn handle_request( // Check if subscription already exists (by endpoint) if !state_guard.subscriptions.iter().any(|s| s.endpoint == subscription.endpoint) { state_guard.subscriptions.push(subscription.clone()); - println!("notifications: Added subscription, total: {}", state_guard.subscriptions.len()); + println!("notifications: Added subscription, total: {}\r", state_guard.subscriptions.len()); // Save subscriptions to state save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; } else { - println!("notifications: Subscription already exists, updating it"); + println!("notifications: Subscription already exists, updating it\r"); // Update existing subscription if let Some(existing) = state_guard.subscriptions.iter_mut().find(|s| s.endpoint == subscription.endpoint) { *existing = subscription; @@ -520,7 +520,7 @@ async fn handle_request( state_guard.subscriptions.retain(|s| { let age = now.saturating_sub(s.created_at); if age > one_month_ms { - println!("notifications: Removing old subscription ({}ms old): {}", age, s.endpoint); + println!("notifications: Removing old subscription ({}ms old): {}\r", age, s.endpoint); false } else { true @@ -539,19 +539,19 @@ async fn handle_request( state_guard.subscriptions.retain(|s| s.endpoint != endpoint); if state_guard.subscriptions.len() < initial_len { - println!("notifications: Removed subscription, remaining: {}", state_guard.subscriptions.len()); + println!("notifications: Removed subscription, remaining: {}\r", state_guard.subscriptions.len()); // Save updated subscriptions to state save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; NotificationsResponse::SubscriptionRemoved } else { - println!("notifications: Subscription not found to remove"); + println!("notifications: Subscription not found to remove\r"); NotificationsResponse::SubscriptionRemoved } } NotificationsAction::ClearSubscriptions => { let mut state_guard = state.write().await; state_guard.subscriptions.clear(); - println!("notifications: Cleared all subscriptions"); + println!("notifications: Cleared all subscriptions\r"); // Save empty subscriptions to state save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; @@ -570,9 +570,9 @@ async fn handle_request( .unwrap() .as_millis() as u64; let age_ms = now.saturating_sub(sub.created_at); - println!("notifications: Found subscription for endpoint, age: {}ms", age_ms); + println!("notifications: Found subscription for endpoint, age: {}ms\r", age_ms); } else { - println!("notifications: No subscription found for endpoint: {}", endpoint); + println!("notifications: No subscription found for endpoint: {}\r", endpoint); } NotificationsResponse::SubscriptionInfo(subscription) @@ -582,12 +582,12 @@ async fn handle_request( // Send response if expected if let Some(target) = rsvp.or_else(|| expects_response.map(|_| source)) { println!( - "notifications: sending response {:?} to {:?}", + "notifications: sending response {:?} to {:?}\r", response, target ); let response_bytes = serde_json::to_vec(&response).unwrap(); println!( - "notifications: response serialized to {} bytes", + "notifications: response serialized to {} bytes\r", response_bytes.len() ); @@ -609,7 +609,7 @@ async fn handle_request( .send(send_to_loop) .await; - println!("notifications: response sent"); + println!("notifications: response sent\r"); } Ok(()) From 648e76e33b1b12835e6a8fb1deaf0dba5a7e2dd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 05:07:36 +0000 Subject: [PATCH 50/65] Format Rust code using rustfmt --- hyperdrive/src/notifications.rs | 136 ++++++++++++++++++++++---------- lib/src/notifications.rs | 12 +-- 2 files changed, 99 insertions(+), 49 deletions(-) diff --git a/hyperdrive/src/notifications.rs b/hyperdrive/src/notifications.rs index 63c2c8cce..82ebadc1b 100644 --- a/hyperdrive/src/notifications.rs +++ b/hyperdrive/src/notifications.rs @@ -15,7 +15,10 @@ use web_push::{ // Import our types from lib use lib::core::StateAction; -use lib::notifications::{NotificationsAction, NotificationsError, NotificationsResponse, PushSubscription, SubscriptionKeys}; +use lib::notifications::{ + NotificationsAction, NotificationsError, NotificationsResponse, PushSubscription, + SubscriptionKeys, +}; /// VAPID keys for web push notifications #[derive(Serialize, Deserialize, Clone)] @@ -226,8 +229,12 @@ async fn load_keys_from_state( .message(Message::Request(Request { inherit: false, expects_response: Some(5), - body: serde_json::to_vec(&StateAction::GetState(ProcessId::new(Some("notifications-subscriptions"), "distro", "sys"))) - .unwrap(), + body: serde_json::to_vec(&StateAction::GetState(ProcessId::new( + Some("notifications-subscriptions"), + "distro", + "sys", + ))) + .unwrap(), metadata: None, capabilities: vec![], })) @@ -384,7 +391,10 @@ async fn handle_request( "data": data, }); - println!("notifications: Sending notification to {} devices\r", state_guard.subscriptions.len()); + println!( + "notifications: Sending notification to {} devices\r", + state_guard.subscriptions.len() + ); // Send to all subscriptions let mut send_errors = Vec::new(); @@ -417,11 +427,12 @@ async fn handle_request( use p256::ecdsa::SigningKey; use p256::pkcs8::EncodePrivateKey; - let signing_key = SigningKey::from_bytes(&private_key_array.into()).map_err(|e| { - NotificationsError::WebPushError { - error: format!("Failed to create signing key: {:?}", e), - } - })?; + let signing_key = + SigningKey::from_bytes(&private_key_array.into()).map_err(|e| { + NotificationsError::WebPushError { + error: format!("Failed to create signing key: {:?}", e), + } + })?; let pem_content = signing_key .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) @@ -433,20 +444,19 @@ async fn handle_request( // Create VAPID signature from PEM let mut sig_builder = VapidSignatureBuilder::from_pem(pem_content.as_bytes(), &subscription_info) - .map_err(|e| { - NotificationsError::WebPushError { - error: format!("Failed to create VAPID signature: {:?}", e), - } + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to create VAPID signature: {:?}", e), })?; // Add required subject claim for VAPID sig_builder.add_claim("sub", "mailto:admin@hyperware.ai"); - let sig_builder = sig_builder.build().map_err(|e| { - NotificationsError::WebPushError { - error: format!("Failed to build VAPID signature: {:?}", e), - } - })?; + let sig_builder = + sig_builder + .build() + .map_err(|e| NotificationsError::WebPushError { + error: format!("Failed to build VAPID signature: {:?}", e), + })?; // Build the web push message let mut message_builder = WebPushMessageBuilder::new(&subscription_info); @@ -472,13 +482,20 @@ async fn handle_request( send_count += 1; } Err(e) => { - println!("notifications: Failed to send to {}: {:?}\r", subscription.endpoint, e); + println!( + "notifications: Failed to send to {}: {:?}\r", + subscription.endpoint, e + ); send_errors.push(format!("Failed to send to endpoint: {:?}", e)); } } } - println!("notifications: Sent to {}/{} devices\r", send_count, state_guard.subscriptions.len()); + println!( + "notifications: Sent to {}/{} devices\r", + send_count, + state_guard.subscriptions.len() + ); NotificationsResponse::NotificationSent } @@ -494,18 +511,35 @@ async fn handle_request( } // Check if subscription already exists (by endpoint) - if !state_guard.subscriptions.iter().any(|s| s.endpoint == subscription.endpoint) { + if !state_guard + .subscriptions + .iter() + .any(|s| s.endpoint == subscription.endpoint) + { state_guard.subscriptions.push(subscription.clone()); - println!("notifications: Added subscription, total: {}\r", state_guard.subscriptions.len()); + println!( + "notifications: Added subscription, total: {}\r", + state_guard.subscriptions.len() + ); // Save subscriptions to state - save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions) + .await?; } else { println!("notifications: Subscription already exists, updating it\r"); // Update existing subscription - if let Some(existing) = state_guard.subscriptions.iter_mut().find(|s| s.endpoint == subscription.endpoint) { + if let Some(existing) = state_guard + .subscriptions + .iter_mut() + .find(|s| s.endpoint == subscription.endpoint) + { *existing = subscription; - save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + save_subscriptions_to_state( + our_node, + send_to_state, + &state_guard.subscriptions, + ) + .await?; } } @@ -520,7 +554,10 @@ async fn handle_request( state_guard.subscriptions.retain(|s| { let age = now.saturating_sub(s.created_at); if age > one_month_ms { - println!("notifications: Removing old subscription ({}ms old): {}\r", age, s.endpoint); + println!( + "notifications: Removing old subscription ({}ms old): {}\r", + age, s.endpoint + ); false } else { true @@ -528,7 +565,8 @@ async fn handle_request( }); if state_guard.subscriptions.len() < initial_count { - save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions) + .await?; } NotificationsResponse::SubscriptionAdded @@ -539,9 +577,13 @@ async fn handle_request( state_guard.subscriptions.retain(|s| s.endpoint != endpoint); if state_guard.subscriptions.len() < initial_len { - println!("notifications: Removed subscription, remaining: {}\r", state_guard.subscriptions.len()); + println!( + "notifications: Removed subscription, remaining: {}\r", + state_guard.subscriptions.len() + ); // Save updated subscriptions to state - save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions) + .await?; NotificationsResponse::SubscriptionRemoved } else { println!("notifications: Subscription not found to remove\r"); @@ -554,13 +596,16 @@ async fn handle_request( println!("notifications: Cleared all subscriptions\r"); // Save empty subscriptions to state - save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions).await?; + save_subscriptions_to_state(our_node, send_to_state, &state_guard.subscriptions) + .await?; NotificationsResponse::SubscriptionsCleared } NotificationsAction::GetSubscription { endpoint } => { let state_guard = state.read().await; - let subscription = state_guard.subscriptions.iter() + let subscription = state_guard + .subscriptions + .iter() .find(|s| s.endpoint == endpoint) .cloned(); @@ -570,9 +615,15 @@ async fn handle_request( .unwrap() .as_millis() as u64; let age_ms = now.saturating_sub(sub.created_at); - println!("notifications: Found subscription for endpoint, age: {}ms\r", age_ms); + println!( + "notifications: Found subscription for endpoint, age: {}ms\r", + age_ms + ); } else { - println!("notifications: No subscription found for endpoint: {}\r", endpoint); + println!( + "notifications: No subscription found for endpoint: {}\r", + endpoint + ); } NotificationsResponse::SubscriptionInfo(subscription) @@ -630,7 +681,7 @@ async fn save_keys_to_state( .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) .message(Message::Request(Request { inherit: false, - expects_response: None, // Don't expect a response to avoid polluting the main loop + expects_response: None, // Don't expect a response to avoid polluting the main loop body: serde_json::to_vec(&StateAction::SetState(NOTIFICATIONS_PROCESS_ID.clone())) .unwrap(), metadata: None, @@ -653,9 +704,10 @@ async fn save_subscriptions_to_state( send_to_state: &MessageSender, subscriptions: &[PushSubscription], ) -> Result<(), NotificationsError> { - let subscriptions_bytes = serde_json::to_vec(subscriptions).map_err(|e| NotificationsError::StateError { - error: format!("Failed to serialize subscriptions: {:?}", e), - })?; + let subscriptions_bytes = + serde_json::to_vec(subscriptions).map_err(|e| NotificationsError::StateError { + error: format!("Failed to serialize subscriptions: {:?}", e), + })?; KernelMessage::builder() .id(rand::random()) @@ -663,9 +715,13 @@ async fn save_subscriptions_to_state( .target((our_node, ProcessId::new(Some("state"), "distro", "sys"))) .message(Message::Request(Request { inherit: false, - expects_response: None, // Don't expect a response to avoid polluting the main loop - body: serde_json::to_vec(&StateAction::SetState(ProcessId::new(Some("notifications-subscriptions"), "distro", "sys"))) - .unwrap(), + expects_response: None, // Don't expect a response to avoid polluting the main loop + body: serde_json::to_vec(&StateAction::SetState(ProcessId::new( + Some("notifications-subscriptions"), + "distro", + "sys", + ))) + .unwrap(), metadata: None, capabilities: vec![], })) diff --git a/lib/src/notifications.rs b/lib/src/notifications.rs index 74d692407..76745ae96 100644 --- a/lib/src/notifications.rs +++ b/lib/src/notifications.rs @@ -17,19 +17,13 @@ pub enum NotificationsAction { /// Initialize or regenerate VAPID keys InitializeKeys, /// Add a push subscription for a device - AddSubscription { - subscription: PushSubscription, - }, + AddSubscription { subscription: PushSubscription }, /// Remove a push subscription - RemoveSubscription { - endpoint: String, - }, + RemoveSubscription { endpoint: String }, /// Clear all subscriptions ClearSubscriptions, /// Get subscription info by endpoint - GetSubscription { - endpoint: String, - }, + GetSubscription { endpoint: String }, } /// Push subscription information from the client From c5f59065abcef5016bed44dc126ea04294053b33 Mon Sep 17 00:00:00 2001 From: Tobias Merkle Date: Wed, 3 Sep 2025 15:49:36 -0400 Subject: [PATCH 51/65] app protocol links + semihelpful refresh --- .../app-store/app-store/src/http_api.rs | 87 +++++++++++++++++-- .../packages/app-store/ui/package-lock.json | 36 ++++++-- hyperdrive/packages/app-store/ui/package.json | 1 + hyperdrive/packages/app-store/ui/src/main.tsx | 1 + .../app-store/ui/src/types/messages.ts | 6 +- .../homepage/ui/src/components/Home/index.tsx | 26 +++++- .../homepage/ui/src/stores/navigationStore.ts | 4 +- .../homepage/ui/src/types/messages.ts | 6 +- 8 files changed, 142 insertions(+), 25 deletions(-) diff --git a/hyperdrive/packages/app-store/app-store/src/http_api.rs b/hyperdrive/packages/app-store/app-store/src/http_api.rs index 64bb86ca2..c08a38693 100644 --- a/hyperdrive/packages/app-store/app-store/src/http_api.rs +++ b/hyperdrive/packages/app-store/app-store/src/http_api.rs @@ -173,6 +173,83 @@ fn make_widget() -> String {

Top Apps

+ + + + + + + Skeleton App - Hyperware + + +
+ + + diff --git a/hyperdrive/packages/spider/ui/package-lock.json b/hyperdrive/packages/spider/ui/package-lock.json new file mode 100644 index 000000000..eee3a35dc --- /dev/null +++ b/hyperdrive/packages/spider/ui/package-lock.json @@ -0,0 +1,4750 @@ +{ + "name": "skeleton-app-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "skeleton-app-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitejs/plugin-react": "^4.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.29.tgz", + "integrity": "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.29", + "@swc/core-darwin-x64": "1.11.29", + "@swc/core-linux-arm-gnueabihf": "1.11.29", + "@swc/core-linux-arm64-gnu": "1.11.29", + "@swc/core-linux-arm64-musl": "1.11.29", + "@swc/core-linux-x64-gnu": "1.11.29", + "@swc/core-linux-x64-musl": "1.11.29", + "@swc/core-win32-arm64-msvc": "1.11.29", + "@swc/core-win32-ia32-msvc": "1.11.29", + "@swc/core-win32-x64-msvc": "1.11.29" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.29.tgz", + "integrity": "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.29.tgz", + "integrity": "sha512-S3eTo/KYFk+76cWJRgX30hylN5XkSmjYtCBnM4jPLYn7L6zWYEPajsFLmruQEiTEDUg0gBEWLMNyUeghtswouw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.29.tgz", + "integrity": "sha512-o9gdshbzkUMG6azldHdmKklcfrcMx+a23d/2qHQHPDLUPAN+Trd+sDQUYArK5Fcm7TlpG4sczz95ghN0DMkM7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.29.tgz", + "integrity": "sha512-sLoaciOgUKQF1KX9T6hPGzvhOQaJn+3DHy4LOHeXhQqvBgr+7QcZ+hl4uixPKTzxk6hy6Hb0QOvQEdBAAR1gXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.29.tgz", + "integrity": "sha512-PwjB10BC0N+Ce7RU/L23eYch6lXFHz7r3NFavIcwDNa/AAqywfxyxh13OeRy+P0cg7NDpWEETWspXeI4Ek8otw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.29.tgz", + "integrity": "sha512-i62vBVoPaVe9A3mc6gJG07n0/e7FVeAvdD9uzZTtGLiuIfVfIBta8EMquzvf+POLycSk79Z6lRhGPZPJPYiQaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.29.tgz", + "integrity": "sha512-YER0XU1xqFdK0hKkfSVX1YIyCvMDI7K07GIpefPvcfyNGs38AXKhb2byySDjbVxkdl4dycaxxhRyhQ2gKSlsFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.29.tgz", + "integrity": "sha512-po+WHw+k9g6FAg5IJ+sMwtA/fIUL3zPQ4m/uJgONBATCVnDDkyW6dBA49uHNVtSEvjvhuD8DVWdFP847YTcITw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.29.tgz", + "integrity": "sha512-h+NjOrbqdRBYr5ItmStmQt6x3tnhqgwbj9YxdGPepbTDamFv7vFnhZR0YfB3jz3UKJ8H3uGJ65Zw1VsC+xpFkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.29", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.29.tgz", + "integrity": "sha512-Q8cs2BDV9wqDvqobkXOYdC+pLUSEpX/KvI0Dgfun1F+LzuLotRFuDhrvkU9ETJA6OnD2+Fn/ieHgloiKA/Mn/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.0.tgz", + "integrity": "sha512-ZmkdHw3wo/o/Rk05YsXZs/DJAfY2CdQ5DUAjoWji+PEr+hYADdGMCGgEAILbiKj+CjspBTuTACBcWDrmC8AUfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.9", + "@swc/core": "^1.11.22" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.203", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", + "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/hyperdrive/packages/spider/ui/package.json b/hyperdrive/packages/spider/ui/package.json new file mode 100644 index 000000000..e8be791f9 --- /dev/null +++ b/hyperdrive/packages/spider/ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "skeleton-app-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:copy": "npm run build && rm -rf ../pkg/ui && cp -r dist ../pkg/ui", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitejs/plugin-react": "^4.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.14", + "typescript": "^5.6.3", + "vite": "^5.4.11" + } +} diff --git a/hyperdrive/packages/spider/ui/public/spider-256-y.png b/hyperdrive/packages/spider/ui/public/spider-256-y.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b80003b74d07fafe83652bf04295d0c994f341 GIT binary patch literal 68261 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'api-keys'); + + useEffect(() => { + initialize().then(() => { + // After initialization, check if we should switch to Chat tab if API keys exist + const store = useSpiderStore.getState(); + if (store.apiKeys.length > 0) { + setActiveTab('chat'); + } + // Otherwise stay on API Keys tab + }); + }, [initialize]); + + // Clear error when switching tabs + const handleTabChange = (tab: TabType) => { + clearError(); + setActiveTab(tab); + }; + + // Export for Conversations component to use + useEffect(() => { + (window as any).switchToChat = () => setActiveTab('chat'); + }, []); + + return ( +
+
+ 🕷️ + +
+ +
+ {activeTab === 'chat' && } + {activeTab === 'api-keys' && } + {activeTab === 'spider-keys' && } + {activeTab === 'mcp-servers' && } + {activeTab === 'conversations' && } + {activeTab === 'settings' && } +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/auth/anthropic.ts b/hyperdrive/packages/spider/ui/src/auth/anthropic.ts new file mode 100644 index 000000000..5b71b0af4 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/auth/anthropic.ts @@ -0,0 +1,85 @@ +import { ApiError, exchange_oauth_token as exchangeOauthToken, refresh_oauth_token as refreshOauthToken } from '@caller-utils'; + +export namespace AuthAnthropic { + const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; + + // Generate PKCE challenge and verifier + async function generatePKCE() { + const generateRandomString = (length: number) => { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return btoa(String.fromCharCode(...array)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + }; + + const verifier = generateRandomString(32); + + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hash = await crypto.subtle.digest('SHA-256', data); + + const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + return { verifier, challenge }; + } + + export async function authorize() { + const pkce = await generatePKCE(); + + const url = new URL("https://claude.ai/oauth/authorize"); + url.searchParams.set("code", "true"); + url.searchParams.set("client_id", CLIENT_ID); + url.searchParams.set("response_type", "code"); + url.searchParams.set("redirect_uri", "https://console.anthropic.com/oauth/code/callback"); + url.searchParams.set("scope", "org:create_api_key user:profile user:inference"); + url.searchParams.set("code_challenge", pkce.challenge); + url.searchParams.set("code_challenge_method", "S256"); + url.searchParams.set("state", pkce.verifier); + + return { + url: url.toString(), + verifier: pkce.verifier, + }; + } + + export async function exchange(code: string, verifier: string) { + try { + // Use the backend proxy to avoid CORS issues + const result = await exchangeOauthToken({ + code: code, + verifier: verifier, + }); + + return result; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + throw new ExchangeFailed(); + } + } + + export async function refresh(refreshToken: string) { + try { + // Use the backend proxy to avoid CORS issues + const result = await refreshOauthToken({ + refreshToken: refreshToken, + }); + + return result; + } catch (error) { + throw new Error("Failed to refresh token"); + } + } + + export class ExchangeFailed extends Error { + constructor() { + super("Exchange failed"); + } + } +} diff --git a/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx b/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx new file mode 100644 index 000000000..b50b1699b --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/ApiKeys.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import { useSpiderStore } from '../store/spider'; +import { ClaudeLogin } from './ClaudeLogin'; + +export default function ApiKeys() { + const { apiKeys, isLoading, error, setApiKey, removeApiKey } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [showClaudeLogin, setShowClaudeLogin] = useState(false); + const [provider, setProvider] = useState('anthropic'); + const [apiKeyValue, setApiKeyValue] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!apiKeyValue.trim()) return; + + await setApiKey(provider, apiKeyValue); + setApiKeyValue(''); + setShowAddForm(false); + }; + + return ( +
+
+

API Keys

+
+ + +
+
+ + {error && ( +
+ {error} +
+ )} + + {showClaudeLogin && ( +
+ { + setShowClaudeLogin(false); + // Refresh API keys list + useSpiderStore.getState().loadApiKeys(); + }} + onCancel={() => setShowClaudeLogin(false)} + /> +
+ )} + + {showAddForm && ( +
+
+ + +
+ +
+ + setApiKeyValue(e.target.value)} + placeholder="sk-..." + required + /> +
+ + +
+ )} + +
+
+ {apiKeys.length === 0 ? ( +

No API keys configured

+ ) : ( + apiKeys.map((key) => ( +
+
+

{key.provider}

+

Key: {key.keyPreview}

+

Created: {new Date(key.createdAt * 1000).toLocaleDateString()}

+ {key.lastUsed && ( +

Last used: {new Date(key.lastUsed * 1000).toLocaleDateString()}

+ )} +
+ +
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Chat.tsx b/hyperdrive/packages/spider/ui/src/components/Chat.tsx new file mode 100644 index 000000000..6c1ad09d4 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Chat.tsx @@ -0,0 +1,313 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; +import ReactMarkdown from 'react-markdown'; +import { webSocketService } from '../services/websocket'; + +interface ToolCall { + id: string; + tool_name: string; + parameters: string; +} + +interface ToolResult { + tool_call_id: string; + result: string; +} + +function ToolCallModal({ toolCall, toolResult, onClose }: { + toolCall: ToolCall; + toolResult?: ToolResult; + onClose: () => void; +}) { + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text).then(() => { + // Could add a toast notification here + }).catch(err => { + console.error('Failed to copy:', err); + }); + }; + + return ( +
+
e.stopPropagation()}> +
+

Tool Call Details: {toolCall.tool_name}

+ +
+
+
+
+

Tool Call

+ +
+
+              {JSON.stringify(toolCall, null, 2)}
+            
+
+ {toolResult && ( +
+
+

Tool Result

+ +
+
+                {JSON.stringify(toolResult, null, 2)}
+              
+
+ )} +
+
+
+ ); +} + +export default function Chat() { + const { + activeConversation, + isLoading, + error, + sendMessage, + clearActiveConversation, + cancelRequest, + wsConnected, + useWebSocket + } = useSpiderStore(); + const [message, setMessage] = useState(''); + const [selectedToolCall, setSelectedToolCall] = useState<{call: ToolCall, result?: ToolResult} | null>(null); + const abortControllerRef = useRef(null); + const messagesEndRef = useRef(null); + const chatMessagesRef = useRef(null); + const inputRef = useRef(null); + + // Auto-scroll to bottom when new messages arrive + const scrollToBottom = (smooth: boolean = true) => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'end' }); + } + }; + + // Scroll on new messages or loading state changes + useEffect(() => { + // Use a small delay to ensure DOM has updated + const timer = setTimeout(() => { + scrollToBottom(); + }, 100); + return () => clearTimeout(timer); + }, [activeConversation?.messages?.length, isLoading]); + + // Scroll immediately when conversation changes + useEffect(() => { + scrollToBottom(false); + }, [activeConversation?.id]); + + // Auto-focus input when loading completes (assistant finishes responding) + useEffect(() => { + if (!isLoading && inputRef.current) { + inputRef.current.focus(); + } + }, [isLoading]); + + // Helper to get tool emoji - always use the same emoji + const getToolEmoji = (toolName: string) => { + return '🔧'; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim() || isLoading) return; + + const controller = new AbortController(); + abortControllerRef.current = controller; + + try { + await sendMessage(message, controller.signal); + setMessage(''); + // Scroll after sending message + setTimeout(() => scrollToBottom(), 50); + } catch (err: any) { + if (err.name !== 'AbortError') { + console.error('Failed to send message:', err); + } + } finally { + abortControllerRef.current = null; + } + }; + + const handleCancel = () => { + // Cancel HTTP request if using HTTP + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + + // Cancel WebSocket request if using WebSocket + if (useWebSocket && wsConnected) { + webSocketService.sendCancel(); + } + + // Update store state + if (cancelRequest) { + cancelRequest(); + } + }; + + const handleNewConversation = () => { + clearActiveConversation(); + }; + + return ( +
+
+

Chat

+
+ {useWebSocket && ( + + {wsConnected ? '🟢' : '🔴'} {wsConnected ? 'Live' : 'HTTP'} + + )} + +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ {activeConversation?.messages.map((msg, index) => { + const toolCalls = msg.toolCallsJson ? JSON.parse(msg.toolCallsJson) as ToolCall[] : null; + const toolResults = msg.toolResultsJson ? JSON.parse(msg.toolResultsJson) as ToolResult[] : null; + const nextMsg = activeConversation.messages[index + 1]; + const hasToolResult = nextMsg?.role === 'tool' && nextMsg.toolResultsJson; + + return ( + + {msg.role !== 'tool' && (msg.content && msg.content.trim() && msg.content !== '[Tool calls pending]') && ( +
+
+ {msg.content} +
+
+ )} + + {/* Display tool calls as separate message-like items */} + {toolCalls && toolCalls.map((toolCall, toolIndex) => { + // Find the corresponding tool result in the next message if it's a tool message + const toolResultFromNext = nextMsg?.role === 'tool' && nextMsg.toolResultsJson + ? (JSON.parse(nextMsg.toolResultsJson) as ToolResult[])?.find(r => r.tool_call_id === toolCall.id) + : null; + + const isLastMessage = index === activeConversation.messages.length - 1; + const isWaitingForResult = isLastMessage && isLoading && !toolResultFromNext; + + return ( +
+ {getToolEmoji(toolCall.tool_name)} + {isWaitingForResult ? ( + <> + {toolCall.tool_name} + + + ) : ( + + )} +
+ ); + })} +
+ ); + }) || ( +
+

Start a conversation by typing a message below

+
+ )} + {isLoading && activeConversation && ( +
+
+
+ + Thinking... +
+
+
+ )} +
+
+ + {selectedToolCall && ( + setSelectedToolCall(null)} + /> + )} + +
+ setMessage(e.target.value)} + placeholder={isLoading ? "Thinking..." : "Type your message..."} + disabled={isLoading} + className={`chat-input ${isLoading ? 'chat-input-thinking' : ''}`} + /> + {isLoading ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx b/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx new file mode 100644 index 000000000..420fced48 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/ClaudeLogin.tsx @@ -0,0 +1,147 @@ +import React, { useState } from 'react'; +import { AuthAnthropic } from '../auth/anthropic'; +import { useSpiderStore } from '../store/spider'; + +interface ClaudeLoginProps { + onSuccess?: () => void; + onCancel?: () => void; +} + +export const ClaudeLogin: React.FC = ({ onSuccess, onCancel }) => { + const [isLoading, setIsLoading] = useState(false); + const [authUrl, setAuthUrl] = useState(null); + const [verifier, setVerifier] = useState(null); + const [authCode, setAuthCode] = useState(''); + const [error, setError] = useState(null); + + const handleStartAuth = async () => { + setIsLoading(true); + setError(null); + + try { + const { url, verifier: v } = await AuthAnthropic.authorize(); + setAuthUrl(url); + setVerifier(v); + + // Open auth URL in new window + window.open(url, '_blank', 'width=600,height=700'); + } catch (err) { + setError('Failed to start authentication'); + console.error(err); + } finally { + setIsLoading(false); + } + }; + + const handleExchangeCode = async () => { + if (!authCode || !verifier) { + setError('Please enter the authorization code'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const tokens = await AuthAnthropic.exchange(authCode, verifier); + + // Store tokens in localStorage + localStorage.setItem('claude_oauth', JSON.stringify({ + refresh: tokens.refresh, + access: tokens.access, + expires: tokens.expires, + })); + + // Store as API key in Spider + const store = useSpiderStore.getState(); + await store.setApiKey('anthropic-oauth', tokens.access); + + onSuccess?.(); + } catch (err) { + setError('Invalid authorization code. Please try again.'); + console.error(err); + } finally { + setIsLoading(false); + } + }; + + + return ( +
+
+
+

Login with Claude

+

+ Authenticate with your Claude.ai account to use your subscription for chat. +

+
+ + + {!authUrl ? ( +
+ +
+ ) : ( +
+ + +
+ + setAuthCode(e.target.value)} + placeholder="Paste the code here" + className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + +
+ )} + + {error && ( +
+ {error} +
+ )} + + {onCancel && ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Conversations.tsx b/hyperdrive/packages/spider/ui/src/components/Conversations.tsx new file mode 100644 index 000000000..d58baf443 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Conversations.tsx @@ -0,0 +1,57 @@ +import { useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function Conversations() { + const { conversations, loadConversations, loadConversation, isLoading } = useSpiderStore(); + + useEffect(() => { + loadConversations(); + }, [loadConversations]); + + const handleSelectConversation = async (id: string) => { + await loadConversation(id); + // Switch to Chat tab after loading conversation + if ((window as any).switchToChat) { + (window as any).switchToChat(); + } + }; + + return ( +
+
+

Conversation History

+ +
+ +
+
+ {conversations.length === 0 ? ( +

No conversations yet

+ ) : ( + conversations.map((conv) => ( +
handleSelectConversation(conv.id)} + > +
+

Conversation {conv.id.substring(0, 8)}...

+

Client: {conv.metadata.client}

+

Started: {conv.metadata.start_time}

+

Messages: {conv.messages.length}

+

Provider: {conv.llm_provider}

+
+
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/McpServers.tsx b/hyperdrive/packages/spider/ui/src/components/McpServers.tsx new file mode 100644 index 000000000..b3990be06 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/McpServers.tsx @@ -0,0 +1,321 @@ +import { useState, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function McpServers() { + const { + mcpServers, + isLoading, + error, + addMcpServer, + connectMcpServer, + disconnectMcpServer, + removeMcpServer, + loadMcpServers + } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [serverName, setServerName] = useState(''); + const [transportType, setTransportType] = useState('websocket'); + const [url, setUrl] = useState('ws://localhost:10125'); + const [hypergridUrl, setHypergridUrl] = useState('http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp'); + const [hypergridToken, setHypergridToken] = useState(''); + const [hypergridClientId, setHypergridClientId] = useState(''); + const [hypergridNode, setHypergridNode] = useState(''); + const [connectingServers, setConnectingServers] = useState>(new Set()); + + // Periodically refresh server status + useEffect(() => { + const interval = setInterval(() => { + loadMcpServers(); + }, 5000); // Refresh every 5 seconds + + return () => clearInterval(interval); + }, [loadMcpServers]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + let transport: any = { + transportType: transportType, + command: null, + args: null, + url: null, + hypergridToken: null, + hypergridClientId: null, + hypergridNode: null + }; + + if (transportType === 'websocket') { + transport.url = url; + } else if (transportType === 'hypergrid') { + transport.url = hypergridUrl; + transport.hypergridToken = hypergridToken; + transport.hypergridClientId = hypergridClientId; + transport.hypergridNode = hypergridNode; + } + + await addMcpServer(serverName, transport); + setServerName(''); + setTransportType('websocket'); + setUrl('ws://localhost:10125'); + setHypergridUrl('http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp'); + setHypergridToken(''); + setHypergridClientId(''); + setHypergridNode(''); + setShowAddForm(false); + + // Refresh servers list after adding + setTimeout(() => loadMcpServers(), 500); + }; + + const handleConnect = async (serverId: string) => { + setConnectingServers(prev => new Set(prev).add(serverId)); + try { + await connectMcpServer(serverId); + // Poll for connection status update + let attempts = 0; + const pollInterval = setInterval(async () => { + await loadMcpServers(); + attempts++; + const server = mcpServers.find(s => s.id === serverId); + if (server?.connected || attempts > 10) { + clearInterval(pollInterval); + setConnectingServers(prev => { + const next = new Set(prev); + next.delete(serverId); + return next; + }); + } + }, 500); + } catch (error) { + setConnectingServers(prev => { + const next = new Set(prev); + next.delete(serverId); + return next; + }); + } + }; + + const handleDisconnect = async (serverId: string) => { + await disconnectMcpServer(serverId); + // Refresh servers list after disconnecting + setTimeout(() => loadMcpServers(), 500); + }; + + const handleRemove = async (serverId: string) => { + if (confirm('Are you sure you want to remove this MCP server?')) { + await removeMcpServer(serverId); + // Refresh servers list after removing + setTimeout(() => loadMcpServers(), 500); + } + }; + + return ( +
+
+

MCP Servers

+ +
+ + {error && ( +
+ {error} +
+ )} + + {showAddForm && ( +
+
+ + setServerName(e.target.value)} + placeholder="My MCP Server" + required + /> +
+ +
+ + +
+ + {transportType === 'websocket' && ( +
+ + setUrl(e.target.value)} + placeholder="ws://localhost:10125" + required + /> + + URL of the WebSocket MCP server or ws-mcp wrapper + +
+ )} + + {transportType === 'hypergrid' && ( + <> +
+ + setHypergridUrl(e.target.value)} + placeholder="http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp" + required + /> + + Base URL for the Hypergrid API endpoint + +
+ +
+ + setHypergridToken(e.target.value)} + placeholder="Enter your hypergrid token (optional for initial connection)" + /> + + Token for authenticating with the Hypergrid network + +
+ +
+ + setHypergridClientId(e.target.value)} + placeholder="Enter your client ID" + required + /> + + Unique identifier for this client + +
+ +
+ + setHypergridNode(e.target.value)} + placeholder="Enter your Hyperware node name" + required + /> + + Name of your Hyperware node + +
+ + )} + + +
+ )} + +
+
+ {mcpServers.length === 0 ? ( +

No MCP servers configured

+ ) : ( + mcpServers.map((server) => { + const isConnecting = connectingServers.has(server.id); + return ( +
+
+

{server.name}

+

+ Status: { + isConnecting ? '🟡 Connecting...' : + server.connected ? '🟢 Connected' : + '🔴 Disconnected' + } +

+

+ Transport: {server.transport.transportType === 'hypergrid' ? + `Hypergrid - ${server.transport.hypergridNode || 'Not configured'}` : + `WebSocket - ${server.transport.url || 'No URL specified'}` + } +

+

Tools: {server.tools.length}

+ {server.tools.length > 0 && ( +
+ Available Tools +
    + {server.tools.map((tool, index) => ( +
  • + {tool.name}: {tool.description} + {tool.inputSchemaJson && (✓ Schema)} +
  • + ))} +
+
+ )} +
+
+ {!server.connected && !isConnecting && ( + + )} + {server.connected && ( + + )} + +
+
+ ); + }) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/Settings.tsx b/hyperdrive/packages/spider/ui/src/components/Settings.tsx new file mode 100644 index 000000000..a08a4d019 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/Settings.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function Settings() { + const { config, isLoading, error, updateConfig } = useSpiderStore(); + const [provider, setProvider] = useState(config.defaultLlmProvider); + const [model, setModel] = useState(''); + const [maxTokens, setMaxTokens] = useState(config.maxTokens); + const [temperature, setTemperature] = useState(config.temperature); + + // Model options based on provider + const modelOptions = { + anthropic: [ + { value: 'claude-opus-4-1-20250805', label: 'Claude 4.1 Opus' }, + { value: 'claude-sonnet-4-20250514', label: 'Claude 4 Sonnet' } + ], + openai: [ + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + { value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }, + { value: 'gpt-4', label: 'GPT-4' }, + { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' } + ], + google: [ + { value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash (Experimental)' }, + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, + { value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' }, + { value: 'gemini-pro', label: 'Gemini Pro' } + ] + }; + + useEffect(() => { + setProvider(config.defaultLlmProvider); + setMaxTokens(config.maxTokens); + setTemperature(config.temperature); + }, [config]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await updateConfig({ + defaultLlmProvider: provider, + maxTokens: maxTokens, + temperature: temperature, + }); + }; + + return ( +
+
+

Settings

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + +
+ +
+ + +
+
+ +
+ + setMaxTokens(Number(e.target.value))} + min="1" + max="100000" + /> +
+ +
+ + setTemperature(Number(e.target.value))} + min="0" + max="2" + step="0.1" + /> +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx b/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx new file mode 100644 index 000000000..bd414e732 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/components/SpiderKeys.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { useSpiderStore } from '../store/spider'; + +export default function SpiderKeys() { + const { spiderKeys, isLoading, error, createSpiderKey, revokeSpiderKey } = useSpiderStore(); + const [showAddForm, setShowAddForm] = useState(false); + const [keyName, setKeyName] = useState(''); + const [permissions, setPermissions] = useState(['read']); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!keyName.trim()) return; + + await createSpiderKey(keyName, permissions); + setKeyName(''); + setPermissions(['read']); + setShowAddForm(false); + }; + + const togglePermission = (perm: string) => { + setPermissions(prev => + prev.includes(perm) + ? prev.filter(p => p !== perm) + : [...prev, perm] + ); + }; + + return ( +
+
+

Spider API Keys

+ +
+ + {error && ( +
+ {error} +
+ )} + + {showAddForm && ( +
+
+ + setKeyName(e.target.value)} + placeholder="My API Key" + required + /> +
+ +
+ +
+ {['read', 'write', 'chat', 'admin'].map(perm => ( + + ))} +
+
+ + +
+ )} + +
+
+ {spiderKeys.length === 0 ? ( +

No Spider API keys generated

+ ) : ( + spiderKeys.map((key) => ( +
+
+

{key.name}

+

Key: {key.key}

+

Permissions: {key.permissions.join(', ')}

+

Created: {new Date(key.createdAt * 1000).toLocaleDateString()}

+
+ +
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/index.css b/hyperdrive/packages/spider/ui/src/index.css new file mode 100644 index 000000000..baa2da425 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/index.css @@ -0,0 +1,665 @@ +/* Root CSS for Skeleton App */ + +:root { + /* Color scheme - customize these for your app */ + --primary-color: #646cff; + --primary-hover: #535bf2; + --background: #242424; + --surface: #1a1a1a; + --text-primary: rgba(255, 255, 255, 0.87); + --text-secondary: rgba(255, 255, 255, 0.6); + --border-color: rgba(255, 255, 255, 0.1); + --error-color: #ff6b6b; + --success-color: #51cf66; + + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: var(--text-primary); + background-color: var(--background); + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + min-width: 320px; + min-height: 100vh; + overflow: hidden; +} + +#root { + width: 100%; + height: 100vh; + margin: 0; + padding: 0; + overflow: hidden; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: var(--surface); + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: var(--primary-color); +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +input, textarea { + padding: 0.6em; + font-size: 1em; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: var(--surface); + color: var(--text-primary); + font-family: inherit; +} + +input:focus, textarea:focus { + outline: none; + border-color: var(--primary-color); +} + +/* Utility classes */ +.error { + color: var(--error-color); + padding: 1em; + border-radius: 4px; + background-color: rgba(255, 107, 107, 0.1); + border: 1px solid var(--error-color); +} + +.success { + color: var(--success-color); + padding: 1em; + border-radius: 4px; + background-color: rgba(81, 207, 102, 0.1); + border: 1px solid var(--success-color); +} + +.loading { + opacity: 0.6; + pointer-events: none; +} + +/* Chat Styles */ +.chat-container { + display: flex; + flex-direction: column; + height: 600px; + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); + background-color: var(--surface); +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.message { + padding: 0.15rem 0.75rem; + border-radius: 8px; + max-width: 80%; + margin-bottom: 0.05rem; +} + +.message-user { + align-self: flex-end; + background-color: var(--primary-color); + color: white; +} + +.message-assistant { + align-self: flex-start; + background-color: var(--surface); + border: 1px solid var(--border-color); +} + +.message-thinking { + opacity: 0.7; +} + +.message-role { + font-size: 0.8rem; + font-weight: bold; + margin-bottom: 0.5rem; + text-transform: capitalize; +} + +.message-content { + line-height: 1.25; +} + +.message-content h1, +.message-content h2, +.message-content h3, +.message-content h4, +.message-content h5, +.message-content h6 { + margin-top: 0.5rem; + margin-bottom: 0.25rem; +} + +.message-content p { + margin: 0.25rem 0; +} + +.message-content ul, +.message-content ol { + margin: 0.25rem 0; + padding-left: 1.5rem; +} + +.message-content code { + background-color: rgba(255, 255, 255, 0.1); + padding: 0.2rem 0.4rem; + border-radius: 3px; + font-family: 'Courier New', monospace; +} + +.message-content pre { + background-color: rgba(0, 0, 0, 0.2); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; +} + +.message-content pre code { + background-color: transparent; + padding: 0; +} + +.thinking-indicator { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.chat-input-form { + display: flex; + gap: 0.5rem; + padding: 1rem; + border-top: 1px solid var(--border-color); + background-color: var(--surface); +} + +.chat-input { + flex: 1; +} + +.chat-input-thinking { + opacity: 0.6; +} + +.empty-chat { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: var(--text-secondary); +} + +/* Button Styles */ +.btn { + padding: 0.5rem 1rem; + border-radius: 4px; + border: none; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--primary-hover); +} + +.btn-danger { + background-color: var(--error-color); + color: white; +} + +.btn-danger:hover:not(:disabled) { + background-color: #ff5252; +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-success:hover:not(:disabled) { + background-color: #45b55a; +} + +.btn-warning { + background-color: #ffa94d; + color: #1a1a1a; +} + +.btn-warning:hover:not(:disabled) { + background-color: #ff922b; +} + +.btn-icon { + padding: 0.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 36px; + height: 36px; +} + +.new-conversation-btn { + background-color: transparent; + border: 1px solid var(--border-color); +} + +.new-conversation-btn:hover { + background-color: var(--surface); +} + +/* MCP Server Styles */ +.component-container { + padding: 1rem; +} + +.component-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.mcp-server-form { + background-color: var(--surface); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-group input { + width: 100%; +} + +.form-help { + display: block; + margin-top: 0.25rem; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.transport-info { + padding: 0.5rem; + background-color: var(--background); + border-radius: 4px; + color: var(--text-secondary); +} + +.mcp-servers-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.mcp-server-item { + display: flex; + justify-content: space-between; + align-items: start; + padding: 1rem; + background-color: var(--surface); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.mcp-server-info { + flex: 1; +} + +.mcp-server-info h3 { + margin: 0 0 0.5rem 0; +} + +.mcp-server-info p { + margin: 0.25rem 0; + color: var(--text-secondary); +} + +.mcp-server-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.mcp-server-tools { + margin-top: 0.5rem; +} + +.mcp-server-tools summary { + cursor: pointer; + font-weight: 500; + color: var(--primary-color); +} + +.mcp-server-tools ul { + margin: 0.5rem 0 0 1rem; + padding: 0; + list-style: none; +} + +.mcp-server-tools li { + margin: 0.25rem 0; + font-size: 0.9rem; +} + +.error-message { + color: var(--error-color); + padding: 0.75rem; + border-radius: 4px; + background-color: rgba(255, 107, 107, 0.1); + border: 1px solid var(--error-color); + margin-bottom: 1rem; +} + +.empty-state { + text-align: center; + color: var(--text-secondary); + padding: 2rem; +} + +/* Tool calls and results */ +.tool-calls, +.tool-results { + margin-top: 0.5rem; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.tool-calls summary, +.tool-results summary { + cursor: pointer; + font-weight: 500; + color: var(--text-secondary); +} + +.tool-calls pre, +.tool-results pre { + margin: 0.5rem 0 0 0; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.3); + border-radius: 3px; + overflow-x: auto; + font-size: 0.85rem; +} + +/* Tool message styles */ +.message-tool { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.15rem 0.75rem; + background-color: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + align-self: flex-start; + max-width: fit-content; +} + +.tool-emoji { + font-size: 1.2rem; +} + +.tool-name { + color: var(--text-primary); +} + +.tool-spinner { + margin-left: 0.5rem; +} + +.tool-link { + background: none; + border: none; + color: var(--primary-color); + cursor: pointer; + padding: 0; + font-size: 1rem; + text-decoration: underline; + transition: opacity 0.2s; +} + +.tool-link:hover { + opacity: 0.8; +} + +/* Modal styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: var(--surface); + border-radius: 8px; + max-width: 600px; + max-height: 80vh; + width: 90%; + display: flex; + flex-direction: column; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + margin: 0; +} + +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-close:hover { + color: var(--text-primary); +} + +.modal-body { + padding: 1rem; + overflow-y: auto; + flex: 1; +} + +.modal-section { + margin-bottom: 1.5rem; +} + +.modal-section:last-child { + margin-bottom: 0; +} + +.modal-section h4 { + margin: 0 0 0.5rem 0; + color: var(--text-secondary); +} + +.json-display { + background-color: rgba(0, 0, 0, 0.2); + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + line-height: 1.4; + color: var(--text-primary); +} + +.copy-btn { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + transition: all 0.2s; +} + +.copy-btn:hover { + color: var(--text-primary); + border-color: var(--primary-color); + background: var(--surface); +} + +/* WebSocket status indicator */ +.ws-status { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 500; +} + +.ws-connected { + background-color: rgba(81, 207, 102, 0.1); + color: var(--success-color); +} + +.ws-disconnected { + background-color: rgba(255, 107, 107, 0.1); + color: var(--error-color); +} + +/* Light mode support */ +@media (prefers-color-scheme: light) { + :root { + --background: #ffffff; + --surface: #f5f5f5; + --text-primary: #213547; + --text-secondary: #646464; + --border-color: rgba(0, 0, 0, 0.1); + color: var(--text-primary); + background-color: var(--background); + } + + button { + background-color: #f9f9f9; + } + + .message-content code { + background-color: rgba(0, 0, 0, 0.05); + } + + .message-content pre { + background-color: rgba(0, 0, 0, 0.03); + } + + .tool-calls, + .tool-results { + background-color: rgba(0, 0, 0, 0.03); + } + + .tool-calls pre, + .tool-results pre { + background-color: rgba(0, 0, 0, 0.05); + } +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/main.tsx b/hyperdrive/packages/spider/ui/src/main.tsx new file mode 100644 index 000000000..a3ad16d4d --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/main.tsx @@ -0,0 +1,12 @@ +// Entry point for the React application +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +// Create root and render the app +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/services/websocket.ts b/hyperdrive/packages/spider/ui/src/services/websocket.ts new file mode 100644 index 000000000..33c275358 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/services/websocket.ts @@ -0,0 +1,174 @@ +import { Message, ConversationMetadata } from '@caller-utils'; +import { + WsClientMessage, + WsServerMessage, + AuthMessage, + ChatMessage, + CancelMessage, + PingMessage +} from '../types/websocket'; + +export type MessageHandler = (message: WsServerMessage) => void; + +class WebSocketService { + private ws: WebSocket | null = null; + private messageHandlers: Set = new Set(); + private reconnectTimeout: NodeJS.Timeout | null = null; + private url: string = ''; + private isAuthenticated: boolean = false; + + connect(url: string): Promise { + return new Promise((resolve, reject) => { + if (this.ws?.readyState === WebSocket.OPEN) { + resolve(); + return; + } + + this.url = url; + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.clearReconnectTimeout(); + resolve(); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + reject(error); + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + this.isAuthenticated = false; + this.scheduleReconnect(); + }; + + this.ws.onmessage = (event) => { + try { + const message = JSON.parse(event.data) as WsServerMessage; + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + }); + } + + private handleMessage(message: WsServerMessage) { + // Notify all handlers + this.messageHandlers.forEach(handler => handler(message)); + } + + authenticate(apiKey: string): Promise { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not connected')); + return; + } + + // Set up one-time handler for auth response + const authHandler = (message: WsServerMessage) => { + if (message.type === 'auth_success') { + this.isAuthenticated = true; + this.removeMessageHandler(authHandler); + resolve(); + } else if (message.type === 'auth_error') { + this.removeMessageHandler(authHandler); + reject(new Error(message.error || 'Authentication failed')); + } + }; + + this.addMessageHandler(authHandler); + + // Send auth message + const authMsg: AuthMessage = { + type: 'auth', + apiKey + }; + this.send(authMsg); + }); + } + + sendChatMessage(messages: Message[], llmProvider?: string, model?: string, mcpServers?: string[], metadata?: ConversationMetadata): void { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const chatMsg: ChatMessage = { + type: 'chat', + payload: { + messages, + llmProvider, + model, + mcpServers, + metadata + } + }; + this.send(chatMsg); + } + + sendCancel(): void { + if (!this.isAuthenticated) { + throw new Error('Not authenticated'); + } + + const cancelMsg: CancelMessage = { + type: 'cancel' + }; + this.send(cancelMsg); + } + + send(data: WsClientMessage): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('WebSocket not connected'); + } + + this.ws.send(JSON.stringify(data)); + } + + addMessageHandler(handler: MessageHandler): void { + this.messageHandlers.add(handler); + } + + removeMessageHandler(handler: MessageHandler): void { + this.messageHandlers.delete(handler); + } + + disconnect(): void { + this.clearReconnectTimeout(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.isAuthenticated = false; + } + + private scheduleReconnect(): void { + if (this.reconnectTimeout) return; + + this.reconnectTimeout = setTimeout(() => { + console.log('Attempting to reconnect WebSocket...'); + this.connect(this.url).catch(error => { + console.error('Reconnection failed:', error); + }); + }, 3000); + } + + private clearReconnectTimeout(): void { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + this.reconnectTimeout = null; + } + } + + get isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + get isReady(): boolean { + return this.isConnected && this.isAuthenticated; + } +} + +export const webSocketService = new WebSocketService(); \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/store/spider.ts b/hyperdrive/packages/spider/ui/src/store/spider.ts new file mode 100644 index 000000000..7c15b96a5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/store/spider.ts @@ -0,0 +1,605 @@ +import { create } from 'zustand'; +import * as api from '../utils/api'; +import { webSocketService } from '../services/websocket'; +import { WsServerMessage } from '../types/websocket'; +import { AuthAnthropic } from '../auth/anthropic'; + +interface ApiKeyInfo { + provider: string; + createdAt: number; + lastUsed?: number; + keyPreview: string; +} + +interface SpiderApiKey { + key: string; + name: string; + permissions: string[]; + createdAt: number; +} + +interface McpServer { + id: string; + name: string; + transport: { + transportType: string; + command?: string; + args?: string[]; + url?: string; + hypergridToken?: string; + hypergridClientId?: string; + hypergridNode?: string; + }; + tools: Array<{ + name: string; + description: string; + parameters: string; + inputSchemaJson?: string; + }>; + connected: boolean; +} + +interface ConversationMetadata { + startTime: string; + client: string; + fromStt: boolean; +} + +interface Conversation { + id: string; + messages: Message[]; + metadata: ConversationMetadata; + llmProvider: string; + model?: string; + mcpServers: string[]; +} + +interface Message { + role: string; + content: string; + toolCallsJson?: string; + toolResultsJson?: string; + timestamp: number; +} + +interface SpiderConfig { + defaultLlmProvider: string; + maxTokens: number; + temperature: number; +} + +interface SpiderStore { + // State + apiKeys: ApiKeyInfo[]; + spiderKeys: SpiderApiKey[]; + mcpServers: McpServer[]; + conversations: Conversation[]; + activeConversation: Conversation | null; + config: SpiderConfig; + isLoading: boolean; + error: string | null; + isConnected: boolean; + nodeId: string; + currentRequestId: string | null; + useWebSocket: boolean; + wsConnected: boolean; + + // Actions + initialize: () => Promise; + setApiKey: (provider: string, key: string) => Promise; + removeApiKey: (provider: string) => Promise; + loadApiKeys: () => Promise; + createSpiderKey: (name: string, permissions: string[]) => Promise; + revokeSpiderKey: (key: string) => Promise; + loadSpiderKeys: () => Promise; + addMcpServer: (name: string, transport: any) => Promise; + connectMcpServer: (serverId: string) => Promise; + disconnectMcpServer: (serverId: string) => Promise; + removeMcpServer: (serverId: string) => Promise; + loadMcpServers: () => Promise; + sendMessage: (message: string, signal?: AbortSignal) => Promise; + cancelRequest: () => Promise; + clearActiveConversation: () => void; + loadConversations: (client?: string, limit?: number) => Promise; + loadConversation: (id: string) => Promise; + loadConfig: () => Promise; + updateConfig: (config: Partial) => Promise; + clearError: () => void; + toggleWebSocket: () => Promise; + connectWebSocket: () => Promise; + disconnectWebSocket: () => void; +} + +// Helper function to fetch admin key using generated binding +async function fetchAdminKey(): Promise { + return api.getAdminKey(); +} + +// Helper function to get API key for chat (checks OAuth tokens) +async function getApiKeyForChat(provider: string): Promise { + // Check if we have an OAuth token for Anthropic + if (provider === 'anthropic' || provider === 'anthropic-oauth') { + const oauthData = localStorage.getItem('claude_oauth'); + if (oauthData) { + try { + const tokens = JSON.parse(oauthData); + + // Check if token is expired + if (tokens.expires && tokens.expires > Date.now()) { + return tokens.access; + } + + // Try to refresh the token + const refreshed = await AuthAnthropic.refresh(tokens.refresh); + + // Update stored tokens + localStorage.setItem('claude_oauth', JSON.stringify({ + refresh: refreshed.refresh, + access: refreshed.access, + expires: refreshed.expires, + })); + + return refreshed.access; + } catch (error) { + console.error('Failed to refresh OAuth token:', error); + // Remove invalid tokens + localStorage.removeItem('claude_oauth'); + } + } + } + + // Fall back to admin key + return (window as any).__spiderAdminKey || null; +} + +export const useSpiderStore = create((set, get) => ({ + // Initial state + apiKeys: [], + spiderKeys: [], + mcpServers: [], + conversations: [], + activeConversation: null, + config: { + defaultLlmProvider: 'anthropic', + maxTokens: 4096, + temperature: 0.7, + }, + isLoading: false, + error: null, + isConnected: false, + nodeId: '', + currentRequestId: null, + useWebSocket: true, // Default to WebSocket for progressive updates + wsConnected: false, + + // Actions + initialize: async () => { + try { + // Fetch admin key if no spider keys are loaded + const state = get(); + if (!state.spiderKeys || state.spiderKeys.length === 0) { + try { + const adminKey = await fetchAdminKey(); + // Store the admin key in a variable accessible to API calls + (window as any).__spiderAdminKey = adminKey; + } catch (error) { + console.error('Failed to fetch admin key:', error); + } + } + set({ isLoading: true }); + + // Check if our.js is loaded + if (typeof window.our === 'undefined') { + set({ + isConnected: false, + error: 'Not connected to Hyperware node. Make sure you are running on a Hyperware node.', + isLoading: false + }); + return; + } + + const nodeId = window.our.node; + set({ isConnected: true, nodeId }); + + // Load initial data + await Promise.all([ + get().loadApiKeys(), + get().loadSpiderKeys(), + get().loadMcpServers(), + get().loadConfig(), + ]); + + // Try to connect WebSocket for progressive updates + if (get().useWebSocket) { + await get().connectWebSocket(); + } + + set({ isLoading: false }); + } catch (error: any) { + set({ + error: error.message || 'Failed to initialize', + isLoading: false, + isConnected: false + }); + } + }, + + setApiKey: async (provider: string, key: string) => { + try { + set({ isLoading: true, error: null }); + await api.setApiKey(provider, key); + await get().loadApiKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to set API key', isLoading: false }); + } + }, + + removeApiKey: async (provider: string) => { + try { + set({ isLoading: true, error: null }); + await api.removeApiKey(provider); + await get().loadApiKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to remove API key', isLoading: false }); + } + }, + + loadApiKeys: async () => { + try { + const keys = await api.listApiKeys(); + set({ apiKeys: keys }); + } catch (error: any) { + set({ error: error.message || 'Failed to load API keys' }); + } + }, + + createSpiderKey: async (name: string, permissions: string[]) => { + try { + set({ isLoading: true, error: null }); + await api.createSpiderKey(name, permissions); + await get().loadSpiderKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to create Spider key', isLoading: false }); + } + }, + + revokeSpiderKey: async (key: string) => { + try { + set({ isLoading: true, error: null }); + await api.revokeSpiderKey(key); + await get().loadSpiderKeys(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to revoke Spider key', isLoading: false }); + } + }, + + loadSpiderKeys: async () => { + try { + // Ensure admin key is fetched first if not already present + if (!(window as any).__spiderAdminKey) { + try { + const adminKey = await fetchAdminKey(); + (window as any).__spiderAdminKey = adminKey; + } catch (error) { + console.error('Failed to fetch admin key:', error); + } + } + + const keys = await api.listSpiderKeys(); + set({ spiderKeys: keys }); + } catch (error: any) { + set({ error: error.message || 'Failed to load Spider keys' }); + } + }, + + addMcpServer: async (name: string, transport: any) => { + try { + set({ isLoading: true, error: null }); + await api.addMcpServer(name, transport); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to add MCP server', isLoading: false }); + } + }, + + connectMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.connectMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to connect MCP server', isLoading: false }); + } + }, + + disconnectMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.disconnectMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to disconnect MCP server', isLoading: false }); + } + }, + + removeMcpServer: async (serverId: string) => { + try { + set({ isLoading: true, error: null }); + await api.removeMcpServer(serverId); + await get().loadMcpServers(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to remove MCP server', isLoading: false }); + } + }, + + loadMcpServers: async () => { + try { + const servers = await api.listMcpServers(); + set({ mcpServers: servers }); + } catch (error: any) { + set({ error: error.message || 'Failed to load MCP servers' }); + } + }, + + sendMessage: async (message: string, signal?: AbortSignal) => { + try { + const requestId = Math.random().toString(36).substring(7); + set({ isLoading: true, error: null, currentRequestId: requestId }); + + // Get current conversation or create new one + let conversation = get().activeConversation; + if (!conversation) { + conversation = { + id: '', + messages: [], + metadata: { + startTime: new Date().toISOString(), + client: 'web-ui', + fromStt: false, + }, + llmProvider: get().config.defaultLlmProvider, + model: get().config.defaultLlmProvider === 'anthropic' ? 'claude-sonnet-4-20250514' : undefined, + mcpServers: get().mcpServers.filter(s => s.connected).map(s => s.id), + }; + } + + // Add user message + const userMessage: Message = { + role: 'user', + content: message, + toolCallsJson: null, + toolResultsJson: null, + timestamp: Date.now(), + }; + + // Update local state immediately for better UX + conversation.messages.push(userMessage); + set({ activeConversation: { ...conversation } }); + + // Check if we should use WebSocket + if (get().useWebSocket && webSocketService.isReady) { + // Send via WebSocket for progressive updates + webSocketService.sendChatMessage( + conversation.messages, + conversation.llmProvider, + conversation.model, + conversation.mcpServers, + conversation.metadata + ); + // WebSocket responses will be handled by the message handler + return; + } + + // Fallback to HTTP + // Get appropriate API key (OAuth or admin) + const apiKey = await getApiKeyForChat(conversation.llmProvider); + if (!apiKey) { + throw new Error('No valid API key available. Please add an API key or login with Claude.'); + } + + // Send to backend with abort signal support + const response = await api.chat( + apiKey, + conversation.messages, + conversation.llmProvider, + conversation.model, + conversation.mcpServers, + conversation.metadata, + signal + ); + + // Only update if this request hasn't been cancelled + if (get().currentRequestId === requestId) { + // Update conversation with response + conversation.id = response.conversationId; + + // Add all messages from the response (includes tool calls and results) + if (response.allMessages && response.allMessages.length > 0) { + conversation.messages.push(...response.allMessages); + } else { + // Fallback to just the final response if allMessages is not available + conversation.messages.push(response.response); + } + + // Update conversations list + const conversations = get().conversations; + const existingIndex = conversations.findIndex(c => c.id === conversation.id); + if (existingIndex >= 0) { + conversations[existingIndex] = conversation; + } else { + conversations.unshift(conversation); + } + + set({ + activeConversation: { ...conversation }, + conversations: [...conversations], + isLoading: false, + currentRequestId: null + }); + } + } catch (error: any) { + if (error.name === 'AbortError') { + set({ isLoading: false, currentRequestId: null }); + } else { + set({ + error: error.message || 'Failed to send message', + isLoading: false, + currentRequestId: null + }); + } + } + }, + + cancelRequest: async () => { + set({ currentRequestId: null, isLoading: false }); + // TODO: Send cancel request to backend if needed + }, + + clearActiveConversation: () => { + set({ activeConversation: null }); + }, + + loadConversations: async (client?: string, limit?: number) => { + try { + const conversations = await api.listConversations(client, limit); + set({ conversations }); + } catch (error: any) { + set({ error: error.message || 'Failed to load conversations' }); + } + }, + + loadConversation: async (id: string) => { + try { + const conversation = await api.getConversation(id); + set({ activeConversation: conversation }); + } catch (error: any) { + set({ error: error.message || 'Failed to load conversation' }); + } + }, + + loadConfig: async () => { + try { + const config = await api.getConfig(); + set({ config }); + } catch (error: any) { + set({ error: error.message || 'Failed to load config' }); + } + }, + + updateConfig: async (config: Partial) => { + try { + set({ isLoading: true, error: null }); + await api.updateConfig(config); + await get().loadConfig(); + set({ isLoading: false }); + } catch (error: any) { + set({ error: error.message || 'Failed to update config', isLoading: false }); + } + }, + + clearError: () => set({ error: null }), + + toggleWebSocket: async () => { + const newState = !get().useWebSocket; + set({ useWebSocket: newState }); + + if (newState) { + await get().connectWebSocket(); + } else { + get().disconnectWebSocket(); + } + }, + + connectWebSocket: async () => { + try { + // Determine WebSocket URL based on current location and BASE_URL + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.host; + const baseUrl = import.meta.env.BASE_URL || '/'; + const wsPath = baseUrl.endsWith('/') ? `${baseUrl}ws` : `${baseUrl}/ws`; + const wsUrl = `${protocol}//${host}${wsPath}`; + + console.log('Connecting to WebSocket at:', wsUrl); + await webSocketService.connect(wsUrl); + + // Set up message handler for progressive updates + webSocketService.addMessageHandler((message: WsServerMessage) => { + const state = get(); + + switch (message.type) { + case 'message': + // Progressive message update from tool loop + if (state.activeConversation && message.message) { + const updatedConversation = { ...state.activeConversation }; + updatedConversation.messages.push(message.message); + set({ activeConversation: updatedConversation }); + } + break; + + case 'chat_complete': + // Final response received + if (state.activeConversation && message.payload) { + const updatedConversation = { ...state.activeConversation }; + updatedConversation.id = message.payload.conversationId; + + // Update conversations list + const conversations = [...state.conversations]; + const existingIndex = conversations.findIndex(c => c.id === updatedConversation.id); + if (existingIndex >= 0) { + conversations[existingIndex] = updatedConversation; + } else { + conversations.unshift(updatedConversation); + } + + set({ + activeConversation: updatedConversation, + conversations, + isLoading: false, + currentRequestId: null + }); + } + break; + + case 'error': + set({ + error: message.error || 'WebSocket error occurred', + isLoading: false, + currentRequestId: null + }); + break; + } + }); + + // Authenticate with appropriate API key (OAuth or admin) + const provider = get().config.defaultLlmProvider; + const authKey = await getApiKeyForChat(provider); + if (!authKey) { + console.error('No API key available for WebSocket auth'); + throw new Error('No valid API key available. Please add an API key or login with Claude.'); + } + await webSocketService.authenticate(authKey); + + set({ wsConnected: true }); + } catch (error: any) { + console.error('Failed to connect WebSocket:', error); + set({ + wsConnected: false, + useWebSocket: false, + error: 'Failed to connect WebSocket. Falling back to HTTP.' + }); + } + }, + + disconnectWebSocket: () => { + webSocketService.disconnect(); + set({ wsConnected: false }); + }, +})); \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts b/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts new file mode 100644 index 000000000..9d01ecbe5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/caller-utils.d.ts @@ -0,0 +1,16 @@ +declare module '../../target/ui/caller-utils' { + export function setApiKey(requestBody: string): Promise; + export function listApiKeys(requestBody: string): Promise; + export function removeApiKey(provider: string): Promise; + export function createSpiderKey(requestBody: string): Promise; + export function listSpiderKeys(requestBody: string): Promise; + export function revokeSpiderKey(keyId: string): Promise; + export function addMcpServer(requestBody: string): Promise; + export function listMcpServers(requestBody: string): Promise; + export function connectMcpServer(serverId: string): Promise; + export function listConversations(requestBody: string): Promise; + export function getConversation(conversationId: string): Promise; + export function getConfig(requestBody: string): Promise; + export function updateConfig(requestBody: string): Promise; + export function chat(requestBody: string): Promise; +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/global.ts b/hyperdrive/packages/spider/ui/src/types/global.ts new file mode 100644 index 000000000..f4b35ac55 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/global.ts @@ -0,0 +1,32 @@ +// Global type definitions for Hyperware environment + +// The window.our object is provided by the /our.js script +// It contains the node and process identity +declare global { + interface Window { + our?: { + node: string; // e.g., "alice.os" + process: string; // e.g., "skeleton-app:skeleton-app:skeleton.os" + }; + } +} + +// Base URL for API calls +// In production, this is empty (same origin) +// In development, you might proxy to your local node +export const BASE_URL = ''; + +// Helper to check if we're in a Hyperware environment +export const isHyperwareEnvironment = (): boolean => { + return typeof window !== 'undefined' && window.our !== undefined; +}; + +// Get the current node identity +export const getNodeId = (): string | null => { + return window.our?.node || null; +}; + +// Get the current process identity +export const getProcessId = (): string | null => { + return window.our?.process || null; +}; \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/types/websocket.ts b/hyperdrive/packages/spider/ui/src/types/websocket.ts new file mode 100644 index 000000000..f7891ebe5 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/types/websocket.ts @@ -0,0 +1,86 @@ +// WebSocket message types for Spider chat + +import { Message, ConversationMetadata, ChatResponse } from '@caller-utils'; + +// Client -> Server messages +export type WsClientMessage = + | AuthMessage + | ChatMessage + | CancelMessage + | PingMessage; + +export interface AuthMessage { + type: 'auth'; + apiKey: string; +} + +export interface ChatMessage { + type: 'chat'; + payload: { + messages: Message[]; + llmProvider?: string; + mcpServers?: string[]; + metadata?: ConversationMetadata; + }; +} + +export interface CancelMessage { + type: 'cancel'; +} + +export interface PingMessage { + type: 'ping'; +} + +// Server -> Client messages +export type WsServerMessage = + | AuthSuccessMessage + | AuthErrorMessage + | StatusMessage + | StreamMessage + | MessageUpdate + | ChatCompleteMessage + | ErrorMessage + | PongMessage; + +export interface AuthSuccessMessage { + type: 'auth_success'; + message: string; +} + +export interface AuthErrorMessage { + type: 'auth_error'; + error: string; +} + +export interface StatusMessage { + type: 'status'; + status: string; + message?: string; +} + +export interface StreamMessage { + type: 'stream'; + iteration: number; + message: string; + tool_calls?: string; +} + +export interface MessageUpdate { + type: 'message'; + message: Message; +} + +export interface ChatCompleteMessage { + type: 'chat_complete'; + payload: ChatResponse; +} + +export interface ErrorMessage { + type: 'error'; + error: string; +} + +export interface PongMessage { + type: 'pong'; +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/src/utils/api.ts b/hyperdrive/packages/spider/ui/src/utils/api.ts new file mode 100644 index 000000000..834aa2fdc --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/utils/api.ts @@ -0,0 +1,175 @@ +// Import the generated functions and types directly +import { + set_api_key as _setApiKey, + list_api_keys as _listApiKeys, + remove_api_key as _removeApiKey, + create_spider_key as _createSpiderKey, + list_spider_keys as _listSpiderKeys, + revoke_spider_key as _revokeSpiderKey, + add_mcp_server as _addMcpServer, + list_mcp_servers as _listMcpServers, + connect_mcp_server as _connectMcpServer, + disconnect_mcp_server as _disconnectMcpServer, + remove_mcp_server as _removeMcpServer, + list_conversations as _listConversations, + get_conversation as _getConversation, + get_config as _getConfig, + update_config as _updateConfig, + chat as _chat, + get_admin_key as _getAdminKey, + type ApiKeyInfo, + type SpiderApiKey, + type McpServer, + type Conversation, + type ConfigResponse, + type ChatResponse, + type Message, + type ConversationMetadata, + type TransportConfig, +} from '@caller-utils'; + +export async function getAdminKey(): Promise { + return _getAdminKey(); +} + +export async function setApiKey(provider: string, key: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _setApiKey({ provider, key, authKey }); +} + +export async function listApiKeys(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listApiKeys({ authKey }); +} + +export async function removeApiKey(provider: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _removeApiKey({ provider, authKey }); +} + +export async function createSpiderKey(name: string, permissions: string[]): Promise { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _createSpiderKey({ name, permissions, adminKey }); +} + +export async function listSpiderKeys(): Promise { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listSpiderKeys({ adminKey }); +} + +export async function revokeSpiderKey(key: string) { + const adminKey = (window as any).__spiderAdminKey; + if (!adminKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _revokeSpiderKey({ keyId: key, adminKey }); +} + +export async function addMcpServer(name: string, transport: TransportConfig): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _addMcpServer({ name, transport, authKey }); +} + +export async function listMcpServers(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listMcpServers({ authKey }); +} + +export async function connectMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _connectMcpServer({ serverId, authKey }); +} + +export async function disconnectMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _disconnectMcpServer({ serverId, authKey }); +} + +export async function removeMcpServer(serverId: string) { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _removeMcpServer({ serverId, authKey }); +} + +export async function listConversations(client?: string, limit?: number, offset?: number): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _listConversations({ + client: client || null, + limit: limit || null, + offset: offset || null, + authKey + }); +} + +export async function getConversation(conversationId: string): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _getConversation({ conversationId, authKey }); +} + +export async function getConfig(): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _getConfig({ authKey }); +} + +export async function updateConfig(config: Partial): Promise { + const authKey = (window as any).__spiderAdminKey; + if (!authKey) { + throw new Error('Admin key not available. Please refresh the page.'); + } + return _updateConfig({ + defaultLlmProvider: config.defaultLlmProvider || null, + maxTokens: config.maxTokens || null, + temperature: config.temperature || null, + authKey + }); +} + +export async function chat(apiKey: string, messages: Message[], llmProvider?: string, model?: string, mcpServers?: string[], metadata?: ConversationMetadata, signal?: AbortSignal): Promise { + // TODO: Pass signal to the underlying API call when supported + return _chat({ + apiKey, + messages, + llmProvider: llmProvider || null, + model: model || null, + mcpServers: mcpServers || null, + metadata: metadata || null + }); +} diff --git a/hyperdrive/packages/spider/ui/src/vite-env.d.ts b/hyperdrive/packages/spider/ui/src/vite-env.d.ts new file mode 100644 index 000000000..151aa6856 --- /dev/null +++ b/hyperdrive/packages/spider/ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/tsconfig.json b/hyperdrive/packages/spider/ui/tsconfig.json new file mode 100644 index 000000000..d07dc20e1 --- /dev/null +++ b/hyperdrive/packages/spider/ui/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + + /* Path mappings */ + "baseUrl": ".", + "paths": { + "@caller-utils": ["../target/ui/caller-utils"] + } + }, + "include": ["src"], + "exclude": ["../target/ui/caller-utils.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/tsconfig.node.json b/hyperdrive/packages/spider/ui/tsconfig.node.json new file mode 100644 index 000000000..4eb43d054 --- /dev/null +++ b/hyperdrive/packages/spider/ui/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/hyperdrive/packages/spider/ui/vite.config.ts b/hyperdrive/packages/spider/ui/vite.config.ts new file mode 100644 index 000000000..1c9b21c6b --- /dev/null +++ b/hyperdrive/packages/spider/ui/vite.config.ts @@ -0,0 +1,74 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +/* +If you are developing a UI outside of a Hyperware project, +comment out the following 2 lines: +*/ +import manifest from '../pkg/manifest.json' +import metadata from '../metadata.json' + +/* +IMPORTANT: +This must match the process name from pkg/manifest.json + pkg/metadata.json +The format is "/" + "process_name:package_name:publisher_node" +*/ +const BASE_URL = `/${manifest[0].process_name}:${metadata.properties.package_name}:${metadata.properties.publisher}`; + +// This is the proxy URL, it must match the node you are developing against +const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080').replace('localhost', '127.0.0.1'); + +console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL); + +export default defineConfig({ + plugins: [react()], + base: BASE_URL, + resolve: { + alias: { + '@caller-utils': path.resolve(__dirname, '../target/ui/caller-utils.ts') + } + }, + build: { + rollupOptions: { + external: ['/our.js'] + } + }, + server: { + open: true, + proxy: { + '/our': { + target: PROXY_URL, + changeOrigin: true, + }, + [`${BASE_URL}/our.js`]: { + target: PROXY_URL, + changeOrigin: true, + rewrite: (path) => path.replace(BASE_URL, ''), + }, + // This route will match all other HTTP requests to the backend + [`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: { + target: PROXY_URL, + changeOrigin: true, + ws: true, // Enable WebSocket proxy + }, + // '/example': { + // target: PROXY_URL, + // changeOrigin: true, + // rewrite: (path) => path.replace(BASE_URL, ''), + // // This is only for debugging purposes + // configure: (proxy, _options) => { + // proxy.on('error', (err, _req, _res) => { + // console.log('proxy error', err); + // }); + // proxy.on('proxyReq', (proxyReq, req, _res) => { + // console.log('Sending Request to the Target:', req.method, req.url); + // }); + // proxy.on('proxyRes', (proxyRes, req, _res) => { + // console.log('Received Response from the Target:', proxyRes.statusCode, req.url); + // }); + // }, + // }, + } + } +}); diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b9c53141d..a3e17ac41 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lib" authors = ["Sybil Technologies AG"] -version = "1.6.1" +version = "1.7.0" edition = "2021" description = "A general-purpose sovereign cloud computing platform" homepage = "https://hyperware.ai" diff --git a/scripts/build-packages/Cargo.toml b/scripts/build-packages/Cargo.toml index cc7bdcb1c..6ea81bb24 100644 --- a/scripts/build-packages/Cargo.toml +++ b/scripts/build-packages/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" anyhow = "1.0.71" clap = "4" fs-err = "2.11" -kit = { git = "https://github.com/hyperware-ai/kit", rev = "79fe678" } +kit = { git = "https://github.com/hyperware-ai/kit", rev = "aac33b6" } serde = "1" serde_json = "1" tokio = "1.28" diff --git a/scripts/build-packages/src/main.rs b/scripts/build-packages/src/main.rs index 980f6166a..b7a439139 100644 --- a/scripts/build-packages/src/main.rs +++ b/scripts/build-packages/src/main.rs @@ -12,11 +12,13 @@ use zip::write::FileOptions; struct PackageBuildParameters { local_dependencies: Option>, is_hyperapp: Option, + features: Option>, } struct PackageBuildParametersPath { local_dependencies: Option>, is_hyperapp: Option, + features: Option>, } fn zip_directory(dir_path: &Path) -> anyhow::Result> { @@ -148,6 +150,7 @@ fn main() -> anyhow::Result<()> { .local_dependencies .map(|bp| bp.iter().map(|f| packages_dir.join(f)).collect()), is_hyperapp: val.is_hyperapp, + features: val.features, }, ) }) @@ -164,7 +167,7 @@ fn main() -> anyhow::Result<()> { // don't run on, e.g., `.DS_Store` return None; } - let (local_dependency_array, is_hyperapp) = + let (local_dependency_array, is_hyperapp, package_specific_features) = if let Some(filename) = entry_path.file_name() { if let Some(maybe_params) = build_parameters.remove(&filename.to_string_lossy().to_string()) @@ -172,18 +175,36 @@ fn main() -> anyhow::Result<()> { ( maybe_params.local_dependencies.unwrap_or_default(), maybe_params.is_hyperapp.unwrap_or_default(), + maybe_params.features.unwrap_or_default(), ) } else { - (vec![], false) + (vec![], false, vec![]) } } else { - (vec![], false) + (vec![], false, vec![]) }; + let package_specific_features = if package_specific_features.is_empty() { + features.clone() + } else if package_specific_features.contains(&"caller-utils".to_string()) { + // build without caller-utils flag, which will fail but will + // also create caller-utils crate (required for succeeding build) + let _ = build_and_zip_package( + entry_path.clone(), + child_pkg_path.to_str().unwrap(), + skip_frontend, + &features, + local_dependency_array.clone(), + is_hyperapp, + ); + format!("{features},{}", package_specific_features.join(",")) + } else { + format!("{features},{}", package_specific_features.join(",")) + }; Some(build_and_zip_package( entry_path.clone(), child_pkg_path.to_str().unwrap(), skip_frontend, - &features, + &package_specific_features, local_dependency_array, is_hyperapp, )) From c2ebb6ba915f539ab7c5f7d597514f8281ad8f88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:02:38 +0000 Subject: [PATCH 64/65] Format Rust code using rustfmt --- .../packages/file-explorer/explorer/src/lib.rs | 6 ++---- hyperdrive/packages/spider/spider/src/lib.rs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/hyperdrive/packages/file-explorer/explorer/src/lib.rs b/hyperdrive/packages/file-explorer/explorer/src/lib.rs index 5aabd4943..ec0b85a60 100644 --- a/hyperdrive/packages/file-explorer/explorer/src/lib.rs +++ b/hyperdrive/packages/file-explorer/explorer/src/lib.rs @@ -1,12 +1,10 @@ use hyperprocess_macro::hyperprocess; -use hyperware_process_lib::logging::{ - debug, error, info, init_logging, Level, -}; +use hyperware_process_lib::hyperapp::{add_response_header, get_path, send, SaveOptions}; +use hyperware_process_lib::logging::{debug, error, info, init_logging, Level}; use hyperware_process_lib::our; use hyperware_process_lib::vfs::{ self, create_drive, vfs_request, FileType, VfsAction, VfsResponse, }; -use hyperware_process_lib::hyperapp::{add_response_header, get_path, send, SaveOptions}; use std::collections::HashMap; const ICON: &str = include_str!("./icon"); diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 485a55e2b..8b6c5ceb8 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -6,8 +6,6 @@ use chrono::Utc; use serde_json::Value; use uuid::Uuid; -#[cfg(not(feature = "simulation-mode"))] -use spider_caller_utils::anthropic_api_key_manager::request_api_key_remote_rpc; use hyperprocess_macro::*; use hyperware_process_lib::{ homepage::add_to_homepage, @@ -18,6 +16,8 @@ use hyperware_process_lib::{ hyperapp::source, our, println, Address, LazyLoadBlob, ProcessId, }; +#[cfg(not(feature = "simulation-mode"))] +use spider_caller_utils::anthropic_api_key_manager::request_api_key_remote_rpc; mod provider; use provider::create_llm_provider; @@ -44,7 +44,7 @@ use utils::{ }; mod tool_providers; -use tool_providers::{ToolProvider, hypergrid::HypergridToolProvider}; +use tool_providers::{hypergrid::HypergridToolProvider, ToolProvider}; const ICON: &str = include_str!("./icon"); @@ -108,7 +108,8 @@ impl SpiderState { let hypergrid_tools = hypergrid_provider.get_tools(self); // Register the provider for later use - self.tool_provider_registry.register(Box::new(hypergrid_provider)); + self.tool_provider_registry + .register(Box::new(hypergrid_provider)); let hypergrid_server = McpServer { id: "hypergrid_default".to_string(), @@ -117,7 +118,9 @@ impl SpiderState { transport_type: "hypergrid".to_string(), command: None, args: None, - url: Some("http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp".to_string()), + url: Some( + "http://localhost:8080/operator:hypergrid:ware.hypr/shim/mcp".to_string(), + ), hypergrid_token: None, hypergrid_client_id: None, hypergrid_node: None, @@ -957,7 +960,8 @@ impl SpiderState { // Register the provider if not already registered if !self.tool_provider_registry.has_provider(&request.server_id) { - self.tool_provider_registry.register(Box::new(hypergrid_provider)); + self.tool_provider_registry + .register(Box::new(hypergrid_provider)); } // Update the server with hypergrid tools and mark as connected From e5d96363a73e4a0eccf76db2bef900a4f113e0c5 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Fri, 5 Sep 2025 16:29:25 -0700 Subject: [PATCH 65/65] spider: fix api key dispenser publisher --- hyperdrive/packages/spider/spider/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyperdrive/packages/spider/spider/src/lib.rs b/hyperdrive/packages/spider/spider/src/lib.rs index 8b6c5ceb8..8ed1d71f0 100644 --- a/hyperdrive/packages/spider/spider/src/lib.rs +++ b/hyperdrive/packages/spider/spider/src/lib.rs @@ -56,7 +56,7 @@ const API_KEY_DISPENSER_NODE: &str = "fake.os"; const API_KEY_DISPENSER_PROCESS_ID: (&str, &str, &str) = ( "anthropic-api-key-manager", "anthropic-api-key-manager", - "sys", + "ware.hypr", ); const HYPERGRID: &str = "operator:hypergrid:ware.hypr";