import { createContext, useCallback, useContext, useEffect, useState, useSyncExternalStore, } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { _getPluginId, invokeHost } from "./ipc";
export const PluginRuntimeContext = createContext({
pluginId: "",
slotId: "",
slotContext: {},
});
export function useSlotContext() {
return useContext(PluginRuntimeContext).slotContext;
}
export function usePluginInfo() {
const ctx = useContext(PluginRuntimeContext);
return { id: ctx.pluginId, slotId: ctx.slotId };
}
export function usePluginSettings() {
const { pluginId } = useContext(PluginRuntimeContext);
const [settings, setSettings] = useState({});
useEffect(() => {
if (!pluginId)
return;
invoke("plugin_get_settings", { pluginId })
.then(setSettings)
.catch(console.error);
}, [pluginId]);
const updateSettings = useCallback(async (patch) => {
const next = { ...settings, ...patch };
await invoke("plugin_save_settings", { pluginId, settings: next });
setSettings(next);
}, [pluginId, settings]);
return { settings, updateSettings };
}
export function useAppTheme() {
const [theme, setTheme] = useState({
id: "forge-dark",
name: "Forge Dark",
type: "dark",
colors: {},
});
useEffect(() => {
const readCssVars = () => {
const style = getComputedStyle(document.documentElement);
const varNames = [
"--color-primary",
"--color-background",
"--color-surface",
"--color-foreground",
"--color-border",
"--color-sidebar",
];
const colors = {};
for (const name of varNames) {
colors[name] = style.getPropertyValue(name).trim();
}
setTheme((current) => ({ ...current, colors }));
};
readCssVars();
const unlisten = listen("theme:changed", readCssVars);
return () => {
unlisten.then((fn) => fn());
};
}, []);
return {
theme,
cssVars: theme.colors,
};
}
export function useHostTheme() {
return useAppTheme();
}
function getHostBridge() {
return window.__HF_HOST ?? {};
}
function normalizeFileIntent(snapshot) {
if (snapshot.pendingFileIntent && typeof snapshot.pendingFileIntent.path === "string") {
return snapshot.pendingFileIntent;
}
if (typeof snapshot.pendingMarkdownOpenPath === "string" && snapshot.pendingMarkdownOpenPath) {
return {
kind: "open",
path: snapshot.pendingMarkdownOpenPath,
source: "legacy-markdown-open",
};
}
return null;
}
const hostAppListeners = new Set();
let hostAppTimer = null;
const hostAppState = {
activeModule: "devkit",
activeSettingsTab: null,
pendingFileIntent: null,
};
function emitHostAppChange() {
hostAppListeners.forEach((listener) => listener());
}
function syncHostAppFromBridge() {
const snapshot = getHostBridge().app?.getSnapshot?.();
if (!snapshot) {
return;
}
let changed = false;
if (typeof snapshot.activeModule === "string" && snapshot.activeModule !== hostAppState.activeModule) {
hostAppState.activeModule = snapshot.activeModule;
changed = true;
}
if ((typeof snapshot.activeSettingsTab === "string" || snapshot.activeSettingsTab === null)
&& snapshot.activeSettingsTab !== hostAppState.activeSettingsTab) {
hostAppState.activeSettingsTab = snapshot.activeSettingsTab ?? null;
changed = true;
}
const nextIntent = normalizeFileIntent(snapshot);
if (JSON.stringify(nextIntent) !== JSON.stringify(hostAppState.pendingFileIntent)) {
hostAppState.pendingFileIntent = nextIntent;
changed = true;
}
if (changed) {
emitHostAppChange();
}
}
function startHostAppPolling() {
if (hostAppTimer !== null) {
return;
}
hostAppTimer = window.setInterval(syncHostAppFromBridge, 300);
}
function stopHostAppPolling() {
if (hostAppTimer === null || hostAppListeners.size > 0) {
return;
}
window.clearInterval(hostAppTimer);
hostAppTimer = null;
}
function subscribeHostApp(listener) {
hostAppListeners.add(listener);
syncHostAppFromBridge();
startHostAppPolling();
return () => {
hostAppListeners.delete(listener);
stopHostAppPolling();
};
}
function getHostAppSnapshot() {
syncHostAppFromBridge();
return hostAppState;
}
function setPendingFileIntent(intent) {
const hostApp = getHostBridge().app;
if (hostApp?.setPendingFileIntent) {
hostApp.setPendingFileIntent(intent);
}
else if (intent && hostApp?.setPendingMarkdownOpenPath) {
hostApp.setPendingMarkdownOpenPath(intent.path);
}
else if (!intent && hostApp?.clearPendingFileIntent) {
hostApp.clearPendingFileIntent();
}
else if (!intent && hostApp?.clearPendingMarkdownOpenPath) {
hostApp.clearPendingMarkdownOpenPath();
}
hostAppState.pendingFileIntent = intent;
emitHostAppChange();
}
function clearPendingFileIntent() {
setPendingFileIntent(null);
}
const hostAIListeners = new Set();
let hostAITimer = null;
const hostAIState = {
modelConfigs: [],
selectedModelId: null,
};
function emitHostAIChange() {
hostAIListeners.forEach((listener) => listener());
}
function syncHostAIFromBridge() {
const snapshot = getHostBridge().aichat?.getSnapshot?.();
if (!snapshot) {
return;
}
let changed = false;
if (Array.isArray(snapshot.modelConfigs) && snapshot.modelConfigs !== hostAIState.modelConfigs) {
hostAIState.modelConfigs = snapshot.modelConfigs;
changed = true;
}
if ((typeof snapshot.selectedModelId === "string" || snapshot.selectedModelId === null)
&& snapshot.selectedModelId !== hostAIState.selectedModelId) {
hostAIState.selectedModelId = snapshot.selectedModelId ?? null;
changed = true;
}
if (changed) {
emitHostAIChange();
}
}
function startHostAIPolling() {
if (hostAITimer !== null) {
return;
}
hostAITimer = window.setInterval(syncHostAIFromBridge, 300);
}
function stopHostAIPolling() {
if (hostAITimer === null || hostAIListeners.size > 0) {
return;
}
window.clearInterval(hostAITimer);
hostAITimer = null;
}
function subscribeHostAI(listener) {
hostAIListeners.add(listener);
syncHostAIFromBridge();
startHostAIPolling();
return () => {
hostAIListeners.delete(listener);
stopHostAIPolling();
};
}
function getHostAISnapshot() {
syncHostAIFromBridge();
return hostAIState;
}
export function useHostAppState() {
const snapshot = useSyncExternalStore(subscribeHostApp, getHostAppSnapshot, getHostAppSnapshot);
return {
activeModule: snapshot.activeModule,
activeSettingsTab: snapshot.activeSettingsTab,
};
}
export function useHostNavigation() {
const snapshot = useSyncExternalStore(subscribeHostApp, getHostAppSnapshot, getHostAppSnapshot);
const navigateToModule = useCallback((moduleId) => {
getHostBridge().app?.setActiveModule?.(moduleId);
hostAppState.activeModule = moduleId;
emitHostAppChange();
}, []);
const openSettingsTab = useCallback((tabId) => {
const hostApp = getHostBridge().app;
if (hostApp?.openSettingsTab) {
hostApp.openSettingsTab(tabId);
}
else {
hostApp?.setActiveModule?.("settings");
}
hostAppState.activeModule = "settings";
hostAppState.activeSettingsTab = tabId;
emitHostAppChange();
}, []);
return {
activeModule: snapshot.activeModule,
activeSettingsTab: snapshot.activeSettingsTab,
navigateToModule,
openSettingsTab,
};
}
export function useHostFileIntent() {
const snapshot = useSyncExternalStore(subscribeHostApp, getHostAppSnapshot, getHostAppSnapshot);
return {
intent: snapshot.pendingFileIntent,
setIntent: setPendingFileIntent,
consume: clearPendingFileIntent,
};
}
export function useHostModels() {
const snapshot = useSyncExternalStore(subscribeHostAI, getHostAISnapshot, getHostAISnapshot);
const refresh = useCallback(async () => {
await getHostBridge().aichat?.fetchModelConfigs?.();
syncHostAIFromBridge();
}, []);
const selectModel = useCallback((id) => {
getHostBridge().aichat?.setSelectedModelId?.(id);
hostAIState.selectedModelId = id;
emitHostAIChange();
}, []);
return {
models: snapshot.modelConfigs,
selectedModelId: snapshot.selectedModelId,
selectModel,
refresh,
};
}
export function useAvailableModels() {
return useHostModels();
}
export function useHostAI() {
const { models, selectedModelId, selectModel, refresh } = useHostModels();
const sendMessage = useCallback(async (request) => {
const normalizedRequest = typeof request === "string" ? { content: request } : request;
return invoke("aichat_send_message", { request: normalizedRequest });
}, []);
const stopGeneration = useCallback(async () => {
return invoke("aichat_stop_generation");
}, []);
const createSession = useCallback(async (session) => {
return invoke("aichat_create_session", { session });
}, []);
const getStreamState = useCallback(async (sessionId) => {
return invoke("aichat_get_stream_state", { sessionId });
}, []);
return {
models,
selectedModelId,
selectModel,
refresh,
sendMessage,
stopGeneration,
createSession,
getStreamState,
};
}
export async function pickHostFile(options = {}) {
return invokeHost("devkit_pick_file", {
title: options.title ?? null,
directory: options.directory ?? null,
filters: options.filters ?? null,
});
}
export async function pickHostDirectory(options = {}) {
return invokeHost("devkit_pick_directory", {
title: options.title ?? null,
directory: options.directory ?? null,
});
}
export async function saveHostFile(options = {}) {
return invokeHost("devkit_save_file", {
title: options.title ?? null,
directory: options.directory ?? null,
defaultName: options.defaultName ?? null,
filters: options.filters ?? null,
});
}
const HOST_DATA_COMMANDS = {
"devkit.profiles": "devkit_get_profiles",
"devkit.workflows": "devkit_get_workflows",
"devkit.snippets": "devkit_get_snippets",
"devkit.directories": "devkit_get_directories",
"aichat.sessions": "aichat_get_sessions",
"aichat.models": "aichat_get_model_configs",
};
export function useHostData(resource) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetch = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await invoke(HOST_DATA_COMMANDS[resource]);
setData(result);
}
catch (e) {
setError(String(e));
}
finally {
setLoading(false);
}
}, [resource]);
useEffect(() => {
void fetch();
}, [fetch]);
return { data, loading, error, refetch: () => void fetch() };
}
const storage = new Map();
export function usePluginStorage() {
return {
get: (key) => storage.get(key),
set: (key, value) => storage.set(key, value),
remove: (key) => storage.delete(key),
clear: () => storage.clear(),
};
}
let toastEmitter = null;
export function _setToastEmitter(fn) {
toastEmitter = fn;
}
export function notify(options) {
if (toastEmitter) {
toastEmitter(options);
}
else {
console.info(`[plugin toast] ${options.title}: ${options.message}`);
}
}
export function useAppEvent(event, handler) {
useEffect(() => {
const unlisten = listen(event, (e) => handler(e.payload));
return () => {
unlisten.then((fn) => fn());
};
}, [event, handler]);
}
export function useHostEvent(event, handler) {
useAppEvent(event, handler);
}
export async function emitPluginEvent(event, payload) {
const pluginId = _getPluginId();
if (!pluginId) {
throw new Error("[plugin-sdk] emitPluginEvent: plugin ID not set. Did you call registerPlugin()?");
}
await invoke("plugin_invoke", {
args: {
wire_name: `plugin_${pluginId.replace(/[.\-]/g, "_")}_emit_event`,
args: { event, payload },
},
});
}