Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/forty-cows-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pulse-editor/shared-utils": patch
"@pulse-editor/react-api": patch
---

Add app state snapshot during import & export
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"early-pumas-listen",
"few-wasps-beam",
"fluffy-poems-cover",
"forty-cows-smell",
"free-ears-swim",
"fruity-goats-look",
"full-beans-stop",
Expand Down
8 changes: 8 additions & 0 deletions npm-packages/react-api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @pulse-editor/react-api

## 0.1.1-alpha.46

### Patch Changes

- Add app state snapshot during import & export
- Updated dependencies
- @pulse-editor/[email protected]

## 0.1.1-alpha.45

### Patch Changes
Expand Down
4 changes: 2 additions & 2 deletions npm-packages/react-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pulse-editor/react-api",
"version": "0.1.1-alpha.45",
"version": "0.1.1-alpha.46",
"main": "dist/main.js",
"files": [
"dist"
Expand Down Expand Up @@ -38,7 +38,7 @@
"typescript-eslint": "^8.30.1"
},
"peerDependencies": {
"@pulse-editor/shared-utils": "0.1.1-alpha.45",
"@pulse-editor/shared-utils": "0.1.1-alpha.46",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
Expand Down
4 changes: 2 additions & 2 deletions npm-packages/react-api/src/hooks/editor/use-loading.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IMCMessage, IMCMessageTypeEnum } from "@pulse-editor/shared-utils";
import { useEffect, useState } from "react";
import useIMC from "../../lib/use-imc";
import { IMCMessage, IMCMessageTypeEnum } from "@pulse-editor/shared-utils";

export default function useLoading() {
const receiverHandlerMap = new Map<
IMCMessageTypeEnum,
(senderWindow: Window, message: IMCMessage) => Promise<void>
(senderWindow: Window, message: IMCMessage) => Promise<any>
>();

const { imc, isReady } = useIMC(receiverHandlerMap);
Expand Down
64 changes: 64 additions & 0 deletions npm-packages/react-api/src/hooks/editor/use-snapshot-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useContext, useEffect, useState } from "react";
import { SnapshotContext } from "../../providers/snapshot-provider";

export default function useSnapShotState<T>(
key: string,
initialValue?: T,
onRestore?: (value: T) => void
) {
const snapshotContext = useContext(SnapshotContext);

if (!snapshotContext) {
throw new Error("useSnapShotState must be used within a SnapshotProvider");
}

const { states, setStates } = snapshotContext;

// Initialize state with the value from context or the initial value
const [state, setState] = useState<T>(
states[key] !== undefined ? states[key] : initialValue
);

// Update context whenever state changes
const setSnapshotState: React.Dispatch<React.SetStateAction<T>> = (value) => {
setState((prev) => {
const newValue =
typeof value === "function" ? (value as (prev: T) => T)(prev) : value;

// Defer the setStates call to next microtask, outside render phase
Promise.resolve().then(() => {
setStates((prevStates) => ({
...prevStates,
[key]: newValue,
}));
});

return newValue;
});
};

// Set the initial value in context if not already set
useEffect(() => {
// Only set if the key does not exist in the context
if (states[key] === undefined && initialValue !== undefined) {
setStates((prevStates) => ({
...prevStates,
[key]: initialValue,
}));
}
}, []);

// Restore state from context when key or states change
useEffect(() => {
console.log("Restoring state for key:", key, states[key]);

if (states[key] !== undefined && states[key] !== state) {
setState(states[key]);
if (onRestore) {
onRestore(states[key]);
}
}
}, [states[key]]);

return [state, setSnapshotState] as const;
}
7 changes: 3 additions & 4 deletions npm-packages/react-api/src/lib/use-imc.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InterModuleCommunication } from "@pulse-editor/shared-utils";
import {
IMCMessageTypeEnum,
InterModuleCommunication,
ReceiverHandlerMap,
} from "@pulse-editor/shared-utils";
import { useEffect, useState } from "react";
Expand Down Expand Up @@ -35,9 +35,8 @@ export default function useIMC(handlerMap: ReceiverHandlerMap) {
await newImc.initOtherWindow(targetWindow);
setImc(newImc);

newImc.sendMessage(IMCMessageTypeEnum.AppReady).then(() => {
setIsReady(true);
});
await newImc.sendMessage(IMCMessageTypeEnum.AppReady);
setIsReady(true);
}

initIMC();
Expand Down
8 changes: 6 additions & 2 deletions npm-packages/react-api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import useAgentTools from "./hooks/agent/use-agent-tools";
import useAgents from "./hooks/agent/use-agents";
import useRegisterAction from "./hooks/editor/use-register-action";
import useFileView from "./hooks/editor/use-file-view";
import useLoading from "./hooks/editor/use-loading";
import useNotification from "./hooks/editor/use-notification";
import useRegisterAction from "./hooks/editor/use-register-action";
import useTheme from "./hooks/editor/use-theme";
import useToolbar from "./hooks/editor/use-toolbar";

Expand All @@ -14,20 +14,24 @@ import useSTT from "./hooks/ai-modality/use-stt";
import useTTS from "./hooks/ai-modality/use-tts";
import useVideoGen from "./hooks/ai-modality/use-video-gen";
import usePulseEnv from "./hooks/editor/use-env";
import useSnapshotState from "./hooks/editor/use-snapshot-state";
import useTerminal from "./hooks/terminal/use-terminal";
import SnapshotProvider from "./providers/snapshot-provider";

export {
SnapshotProvider,
useAgentTools,
useAgents,
useRegisterAction,
useFileView,
useImageGen,
useLLM,
useLoading,
useNotification,
useOCR,
usePulseEnv,
useRegisterAction,
useSTT,
useSnapshotState,
useTTS,
useTerminal,
useTheme,
Expand Down
63 changes: 63 additions & 0 deletions npm-packages/react-api/src/providers/snapshot-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { IMCMessage, IMCMessageTypeEnum } from "@pulse-editor/shared-utils";
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useEffect,
useState,
} from "react";
import useIMC from "../lib/use-imc";

export const SnapshotContext = createContext<SnapshotContextType | undefined>(
undefined
);

export type SnapshotContextType = {
states: { [key: string]: any };
setStates: Dispatch<SetStateAction<{ [key: string]: any }>>;
};

export default function SnapshotProvider({
children,
}: {
children: ReactNode;
}) {
const receiverHandlerMap = new Map<
IMCMessageTypeEnum,
(senderWindow: Window, message: IMCMessage) => Promise<any>
>([
[
IMCMessageTypeEnum.EditorAppStateSnapshotRestore,
async (senderWindow: Window, message: IMCMessage) => {
const { states } = message.payload;

// Update all states in the context
setStates((prev) => ({ ...states }));
},
],
[
IMCMessageTypeEnum.EditorAppStateSnapshotSave,
async (senderWindow: Window, message: IMCMessage) => {
// Return current states in the context
return { states };
},
],
]);

const { imc, isReady } = useIMC(receiverHandlerMap);

const [states, setStates] = useState<{ [key: string]: any }>({});

useEffect(() => {
if (isReady) {
imc?.updateReceiverHandlerMap(receiverHandlerMap);
}
}, [isReady, states]);

return (
<SnapshotContext.Provider value={{ states, setStates }}>
{children}
</SnapshotContext.Provider>
);
}
6 changes: 6 additions & 0 deletions npm-packages/shared-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @pulse-editor/shared-utils

## 0.1.1-alpha.46

### Patch Changes

- Add app state snapshot during import & export

## 0.1.1-alpha.45

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion npm-packages/shared-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pulse-editor/shared-utils",
"version": "0.1.1-alpha.45",
"version": "0.1.1-alpha.46",
"main": "dist/main.js",
"files": [
"dist"
Expand Down
12 changes: 10 additions & 2 deletions npm-packages/shared-utils/src/imc/poly-imc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,26 @@ export class ConnectionListener {

private listener: InterModuleCommunication;

/**
*
* @param polyIMC The polyIMC instance.
* @param newConnectionReceiverHandlerMap Receiver handler map for newly established poly-IMC channel.
* @param onConnection Callback function to be called when a new connection is established.
* @param expectedOtherWindowId Optional expected other window ID to validate incoming connections.
*/
constructor(
polyIMC: PolyIMC,
newConnectionReceiverHandlerMap: ReceiverHandlerMap,
onConnection?: (senderWindow: Window, message: IMCMessage) => void
onConnection?: (senderWindow: Window, message: IMCMessage) => void,
expectedOtherWindowId?: string
) {
this.polyIMC = polyIMC;
this.newConnectionReceiverHandlerMap = newConnectionReceiverHandlerMap;
this.onConnection = onConnection;

const listener = new InterModuleCommunication();
this.listener = listener;
listener.initThisWindow(window);
listener.initThisWindow(window, expectedOtherWindowId);

listener.updateReceiverHandlerMap(
new Map<IMCMessageTypeEnum, ReceiverHandler>([
Expand Down
3 changes: 3 additions & 0 deletions npm-packages/shared-utils/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export enum IMCMessageTypeEnum {
EditorShowNotification = "editor-show-notification",
// Get environment variables
EditorGetEnv = "editor-get-env",
// App state snapshot upon importing & exporting
EditorAppStateSnapshotRestore = "editor-app-state-snapshot-restore",
EditorAppStateSnapshotSave = "editor-app-state-snapshot-save",
// #endregion

// #region Platform API interaction messages (require OS-like environment)
Expand Down
6 changes: 5 additions & 1 deletion web/components/app-loaders/sandbox-app-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function SandboxAppLoader({

if (currentViewId) {
// Listen for an incoming extension connection
console.log("Listening for extension connection...");
console.log(`[${currentViewId}]: Listening for app connection...`);
listenForExtensionConnection();

setIsLookingForExtension(true);
Expand All @@ -136,6 +136,8 @@ export default function SandboxAppLoader({
// When IMC is connected, remove the connection listener
useEffect(() => {
if (isConnected && clRef.current) {
console.log(`[${currentViewId}]: App connected.`);
// Close the connection listener
clRef.current.close();
clRef.current = null;
}
Expand Down Expand Up @@ -184,6 +186,7 @@ export default function SandboxAppLoader({
}: {
isLoading: boolean;
} = message.payload;
console.log(`[${model.viewId}]: App is loading: `, isLoading);
setIsLoadingExtension((prev) => isLoading);
if (onInitialLoaded) {
onInitialLoaded();
Expand Down Expand Up @@ -296,6 +299,7 @@ export default function SandboxAppLoader({
(senderWindow: Window, message: IMCMessage) => {
setIsConnected((prev) => true);
},
viewModel.viewId,
);
clRef.current = cl;
}
Expand Down
24 changes: 13 additions & 11 deletions web/components/explorer/app/app-explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,19 @@ export default function AppExplorer() {
<div className="grid grid-cols-2 gap-2 h-full w-full overflow-y-auto overflow-x-hidden px-4">
{previews}
</div>
<Button
className="mx-4 mb-2"
onPress={() => {
editorContext?.setEditorStates((prev) => ({
...prev,
isMarketplaceOpen: true,
}));
}}
>
Browse More Apps
</Button>
<div className="flex flex-col gap-y-1 px-4 pb-2">
<Button
color="secondary"
onPress={() => {
editorContext?.setEditorStates((prev) => ({
...prev,
isMarketplaceOpen: true,
}));
}}
>
Explorer Community Workflows/Apps
</Button>
</div>
</div>
);
}
Loading