@sero-ai/app-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @sero-ai/app-runtime
2
+
3
+ Shared runtime hooks for Sero federated app modules.
4
+
5
+ This package provides the React hooks and context used by Sero apps:
6
+
7
+ - `useAppState`
8
+ - `useAppInfo`
9
+ - `useAgentPrompt`
10
+ - `useAI`
11
+ - `useAvailableModels`
12
+ - `useTheme`
13
+ - `AppProvider`
14
+
15
+ ## Development
16
+
17
+ Inside the Sero monorepo, workspace packages consume the source entrypoint.
18
+
19
+ ## Publishing
20
+
21
+ This package intentionally publishes its TypeScript source directly under the
22
+ `@sero-ai` npm scope.
23
+
24
+ ```bash
25
+ cd packages/app-runtime
26
+ npm publish --access public
27
+ ```
28
+
29
+ ## Consumption
30
+
31
+ Inside the Sero monorepo and exported plugin source repos, consumers keep
32
+ importing `@sero-ai/app-runtime`. Package manifests map that import name to the
33
+ published package via npm aliasing (`npm:@sero-ai/app-runtime@<version>`).
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@sero-ai/app-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Hooks for Sero federated app modules — useAppState, useAppInfo, useAgentPrompt",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=19.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^19.2.13",
26
+ "typescript": "^5.9.3"
27
+ }
28
+ }
package/src/context.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * App context — provided by Sero's shell when mounting a federated app.
3
+ *
4
+ * Uses a globalThis singleton so the SAME context object is shared
5
+ * between host and remote even if @sero-ai/app-runtime is instantiated
6
+ * multiple times (which happens in Vite dev mode with MF).
7
+ */
8
+
9
+ import { createContext } from 'react';
10
+
11
+ export interface AppContextValue {
12
+ /** App identifier (e.g. "todo"). */
13
+ appId: string;
14
+ /** Workspace identifier (e.g. "global"). */
15
+ workspaceId: string;
16
+ /** Absolute path to the workspace root. */
17
+ workspacePath: string;
18
+ /** Absolute path to the state file on disk. */
19
+ stateFilePath: string;
20
+ /**
21
+ * Send a prompt to the active agent session.
22
+ * Injected by the shell — apps don't need to know about session IDs.
23
+ * Returns undefined if no session is active.
24
+ */
25
+ promptAgent?: (text: string) => void;
26
+ /** Current effective theme mode ('light' or 'dark'). */
27
+ themeMode?: 'light' | 'dark';
28
+ /** Active theme preset ID. */
29
+ themePresetId?: string;
30
+ }
31
+
32
+ const CONTEXT_KEY = '__sero_app_context__';
33
+
34
+ // Ensure a single context instance across all module copies
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const g = globalThis as any;
37
+ if (!g[CONTEXT_KEY]) {
38
+ g[CONTEXT_KEY] = createContext<AppContextValue | null>(null);
39
+ }
40
+
41
+ export const AppContext: React.Context<AppContextValue | null> = g[CONTEXT_KEY];
42
+
43
+ export const AppProvider = AppContext.Provider;
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @sero-ai/app-runtime — hooks for Sero federated app modules.
3
+ *
4
+ * Shared via module federation so every app gets the host's singleton.
5
+ * Hooks communicate with the Electron main process via window.sero IPC.
6
+ */
7
+
8
+ export { AppContext, AppProvider, type AppContextValue } from './context';
9
+ export { useAppState } from './use-app-state';
10
+ export { useAppInfo } from './use-app-info';
11
+ export { useAgentPrompt } from './use-agent-prompt';
12
+ export { useAI, type AppAI } from './use-ai';
13
+ export { useAvailableModels, type UseAvailableModelsResult } from './use-available-models';
14
+ export { useTheme, type UseThemeResult } from './use-theme';
15
+ export { getSeroApi } from './sero-bridge';
16
+ export type { AppModelInfo, AppModelGroup } from './sero-bridge';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Typed access to the `window.sero` preload API.
3
+ *
4
+ * The full SeroAPI lives in apps/desktop/src/types/electron.d.ts. This
5
+ * module declares only the subset app-runtime hooks need, keeping the
6
+ * package decoupled from the desktop app's types while providing type
7
+ * safety for all IPC calls.
8
+ *
9
+ * The single `(window as ...)` cast lives here — every other module
10
+ * imports the typed getter.
11
+ */
12
+
13
+ export interface SeroAppStateBridge {
14
+ read(filePath: string): Promise<unknown>;
15
+ write(filePath: string, data: unknown): Promise<void>;
16
+ watch(filePath: string): Promise<unknown>;
17
+ unwatch(filePath: string): Promise<void>;
18
+ onChange(cb: (filePath: string, data: unknown) => void): () => void;
19
+ }
20
+
21
+ export interface SeroAppAgentBridge {
22
+ prompt(appId: string, workspaceId: string, text: string): Promise<string>;
23
+ promptStream?(
24
+ appId: string,
25
+ workspaceId: string,
26
+ text: string,
27
+ onDelta: (delta: string) => void,
28
+ ): Promise<string>;
29
+ }
30
+
31
+ export interface SeroGitAppActionParams {
32
+ action: string;
33
+ file?: string;
34
+ message?: string;
35
+ branch?: string;
36
+ hash?: string;
37
+ staged?: boolean;
38
+ all?: boolean;
39
+ stashIndex?: number;
40
+ }
41
+
42
+ export interface SeroGitAppActionResult {
43
+ ok: boolean;
44
+ message: string;
45
+ }
46
+
47
+ export interface SeroGitAppBridge {
48
+ run(workspaceId: string, params: SeroGitAppActionParams): Promise<SeroGitAppActionResult>;
49
+ }
50
+
51
+ export interface SeroEditorExecResult {
52
+ exitCode: number;
53
+ stdout: string;
54
+ stderr: string;
55
+ }
56
+
57
+ export interface SeroEditorBridge {
58
+ exec(workspaceId: string, command: string): Promise<SeroEditorExecResult>;
59
+ }
60
+
61
+ // ── Model types (subset of desktop's ipc types) ──────────────
62
+
63
+ /** Serialisable model info for app modules. */
64
+ export interface AppModelInfo {
65
+ provider: string;
66
+ modelId: string;
67
+ name: string;
68
+ reasoning: boolean;
69
+ }
70
+
71
+ /** A group of models under a single provider. */
72
+ export interface AppModelGroup {
73
+ provider: string;
74
+ displayName: string;
75
+ logo: string;
76
+ models: AppModelInfo[];
77
+ }
78
+
79
+ export interface SeroModelsBridge {
80
+ list(): Promise<AppModelGroup[]>;
81
+ }
82
+
83
+ export interface SeroBridge {
84
+ appState: SeroAppStateBridge;
85
+ appAgent: SeroAppAgentBridge;
86
+ gitApp?: SeroGitAppBridge;
87
+ editor?: SeroEditorBridge;
88
+ models?: SeroModelsBridge;
89
+ }
90
+
91
+ /**
92
+ * Get the Sero preload bridge. Throws if not running inside the Sero shell.
93
+ */
94
+ export function getSeroApi(): SeroBridge {
95
+ const sero = (window as unknown as { sero?: SeroBridge }).sero;
96
+ if (!sero) {
97
+ throw new Error('[app-runtime] window.sero not available — must run inside Sero shell');
98
+ }
99
+ return sero;
100
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * useAgentPrompt — send a message to the agent from an app UI.
3
+ *
4
+ * Returns a function that, when called, sends a text prompt to the
5
+ * currently active agent session. The shell injects the actual
6
+ * prompt function via AppContext — apps never need to know about
7
+ * session IDs or storage details.
8
+ */
9
+
10
+ import { useContext, useCallback } from 'react';
11
+ import { AppContext } from './context';
12
+
13
+ /**
14
+ * Returns a function that sends a prompt to the active agent session.
15
+ *
16
+ * Usage:
17
+ * const prompt = useAgentPrompt();
18
+ * prompt("Add a todo: buy milk");
19
+ */
20
+ export function useAgentPrompt(): (text: string) => void {
21
+ const ctx = useContext(AppContext);
22
+
23
+ return useCallback(
24
+ (text: string) => {
25
+ if (!ctx?.promptAgent) {
26
+ console.warn('[useAgentPrompt] No promptAgent in context — prompt dropped');
27
+ return;
28
+ }
29
+ ctx.promptAgent(text);
30
+ },
31
+ [ctx],
32
+ );
33
+ }
package/src/use-ai.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * useAI — make ad-hoc LLM calls from an app UI.
3
+ *
4
+ * Each app gets a dedicated agent session managed by the main process.
5
+ * Calls go through `window.sero.appAgent.prompt()` — no active chat
6
+ * session required. The session persists for the app × workspace pair,
7
+ * so the LLM accumulates context across calls.
8
+ *
9
+ * Usage:
10
+ * const ai = useAI();
11
+ * const response = await ai.prompt("Generate an inspirational quote.");
12
+ */
13
+
14
+ import { useContext, useCallback, useMemo } from 'react';
15
+ import { AppContext } from './context';
16
+ import { getSeroApi } from './sero-bridge';
17
+
18
+ export interface AppAI {
19
+ /** Send a prompt to the app's dedicated agent session. Returns the LLM's text response. */
20
+ prompt: (text: string) => Promise<string>;
21
+ /** Send a prompt and receive text deltas as they stream in. Returns the final full text. */
22
+ promptStream: (text: string, onDelta: (delta: string) => void) => Promise<string>;
23
+ }
24
+
25
+ export function useAI(): AppAI {
26
+ const ctx = useContext(AppContext);
27
+
28
+ const prompt = useCallback(
29
+ async (text: string): Promise<string> => {
30
+ if (!ctx?.appId || !ctx?.workspaceId) {
31
+ throw new Error('[useAI] No app context — must be used inside a Sero app');
32
+ }
33
+
34
+ const { appAgent } = getSeroApi();
35
+ return appAgent.prompt(ctx.appId, ctx.workspaceId, text);
36
+ },
37
+ [ctx],
38
+ );
39
+
40
+ const promptStream = useCallback(
41
+ async (text: string, onDelta: (delta: string) => void): Promise<string> => {
42
+ if (!ctx?.appId || !ctx?.workspaceId) {
43
+ throw new Error('[useAI] No app context — must be used inside a Sero app');
44
+ }
45
+
46
+ const { appAgent } = getSeroApi();
47
+
48
+ // Fall back to non-streaming if bridge doesn't support it
49
+ if (!appAgent.promptStream) {
50
+ return appAgent.prompt(ctx.appId, ctx.workspaceId, text);
51
+ }
52
+
53
+ return appAgent.promptStream(ctx.appId, ctx.workspaceId, text, onDelta);
54
+ },
55
+ [ctx],
56
+ );
57
+
58
+ return useMemo(() => ({ prompt, promptStream }), [prompt, promptStream]);
59
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * useAppInfo — read-only context about the current app and workspace.
3
+ */
4
+
5
+ import { useContext } from 'react';
6
+ import { AppContext } from './context';
7
+
8
+ export interface AppInfo {
9
+ appId: string;
10
+ workspaceId: string;
11
+ workspacePath: string;
12
+ }
13
+
14
+ export function useAppInfo(): AppInfo {
15
+ const ctx = useContext(AppContext);
16
+ if (!ctx) {
17
+ throw new Error('useAppInfo must be used inside an <AppProvider>');
18
+ }
19
+
20
+ return {
21
+ appId: ctx.appId,
22
+ workspaceId: ctx.workspaceId,
23
+ workspacePath: ctx.workspacePath,
24
+ };
25
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * useAppState — file-backed reactive state for Sero apps.
3
+ *
4
+ * 1. Initial read via IPC
5
+ * 2. File watching via main process (fs.watch → IPC push)
6
+ * 3. Writes via IPC (atomic file write → watcher fires → all consumers update)
7
+ */
8
+
9
+ import { useState, useEffect, useCallback, useContext, useRef } from 'react';
10
+ import { AppContext } from './context';
11
+ import { getSeroApi } from './sero-bridge';
12
+
13
+ /**
14
+ * File-backed reactive state hook.
15
+ *
16
+ * @param defaultState — returned while the file is being read (or if missing)
17
+ * @returns [state, updateState] — updateState accepts an updater function
18
+ */
19
+ export function useAppState<T>(defaultState: T): [T, (updater: (prev: T) => T) => void] {
20
+ const ctx = useContext(AppContext);
21
+ if (!ctx) {
22
+ throw new Error('useAppState must be used inside an <AppProvider>');
23
+ }
24
+
25
+ const { stateFilePath } = ctx;
26
+ const [state, setState] = useState<T>(defaultState);
27
+ const stateRef = useRef<T>(defaultState);
28
+
29
+ // Keep ref in sync for the updater closure
30
+ stateRef.current = state;
31
+
32
+ // Initial read + start watching
33
+ useEffect(() => {
34
+ const api = getSeroApi();
35
+ let unsubChange: (() => void) | null = null;
36
+
37
+ stateRef.current = defaultState;
38
+ setState(defaultState);
39
+
40
+ // Subscribe to file changes from main process
41
+ unsubChange = api.appState.onChange((fp: string, data: unknown) => {
42
+ if (fp === stateFilePath && data != null) {
43
+ const parsed = data as T;
44
+ stateRef.current = parsed;
45
+ setState(parsed);
46
+ }
47
+ });
48
+
49
+ // Start watching (also returns current state)
50
+ api.appState.watch(stateFilePath).then((current) => {
51
+ if (current != null) {
52
+ const parsed = current as T;
53
+ stateRef.current = parsed;
54
+ setState(parsed);
55
+ }
56
+ });
57
+
58
+ return () => {
59
+ unsubChange?.();
60
+ api.appState.unwatch(stateFilePath);
61
+ };
62
+ }, [stateFilePath]);
63
+
64
+ // Write updater
65
+ const updateState = useCallback(
66
+ (updater: (prev: T) => T) => {
67
+ const next = updater(stateRef.current);
68
+ stateRef.current = next;
69
+ setState(next);
70
+
71
+ // Persist to disk (async, fire-and-forget — watcher will confirm)
72
+ const api = getSeroApi();
73
+ api.appState.write(stateFilePath, next);
74
+ },
75
+ [stateFilePath],
76
+ );
77
+
78
+ return [state, updateState];
79
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * useAvailableModels — fetches the list of available models from the
3
+ * Sero host's ModelRegistry. Session-independent: apps don't need an
4
+ * active agent session to enumerate models.
5
+ *
6
+ * Returns { groups, loading, error, refresh }.
7
+ */
8
+
9
+ import { useState, useEffect, useCallback } from 'react';
10
+ import { getSeroApi, type AppModelGroup } from './sero-bridge';
11
+
12
+ export interface UseAvailableModelsResult {
13
+ /** Model groups, grouped by provider. */
14
+ groups: AppModelGroup[];
15
+ /** True while the initial fetch is in flight. */
16
+ loading: boolean;
17
+ /** Error message if the fetch failed. */
18
+ error: string | null;
19
+ /** Manually re-fetch the list (e.g. after adding auth). */
20
+ refresh: () => void;
21
+ }
22
+
23
+ export function useAvailableModels(): UseAvailableModelsResult {
24
+ const [groups, setGroups] = useState<AppModelGroup[]>([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ const fetch = useCallback(() => {
29
+ setLoading(true);
30
+ setError(null);
31
+
32
+ const api = getSeroApi();
33
+ if (!api.models) {
34
+ setError('Model listing not available in this Sero version');
35
+ setLoading(false);
36
+ return;
37
+ }
38
+
39
+ api.models
40
+ .list()
41
+ .then((result) => {
42
+ setGroups(result);
43
+ setError(null);
44
+ })
45
+ .catch((err: unknown) => {
46
+ setError(err instanceof Error ? err.message : 'Failed to fetch models');
47
+ })
48
+ .finally(() => {
49
+ setLoading(false);
50
+ });
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ fetch();
55
+ }, [fetch]);
56
+
57
+ return { groups, loading, error, refresh: fetch };
58
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * useTheme — read the current theme mode and preset ID from app context.
3
+ *
4
+ * Most federated apps don't need this — CSS variables update automatically.
5
+ * Use this hook when you need programmatic access to the theme (e.g. for
6
+ * canvas rendering, charts, or conditional logic).
7
+ */
8
+
9
+ import { useContext } from 'react';
10
+ import { AppContext } from './context';
11
+
12
+ export interface UseThemeResult {
13
+ /** Current effective mode ('light' or 'dark'). */
14
+ mode: 'light' | 'dark';
15
+ /** Active theme preset ID. */
16
+ presetId: string;
17
+ }
18
+
19
+ export function useTheme(): UseThemeResult {
20
+ const ctx = useContext(AppContext);
21
+ return {
22
+ mode: ctx?.themeMode ?? 'dark',
23
+ presetId: ctx?.themePresetId ?? 'default',
24
+ };
25
+ }