import type { OSApi, Size } from "./types"; import { fs } from "./fs"; import { db } from "./db"; import { bus } from "./bus"; import { chat, chatStream, aiAvailable } from "./ai"; import { useOS } from "./store"; import { tokensFor, tokensAsCss } from "../design/tokens"; // Builds the sandboxed OS API handed to a single app window. // Apps may ONLY reach the outside world through this object. export function makeOSApi(appId: string, windowId: string): OSApi { const store = () => useOS.getState(); return { fs: { read: (p) => fs.read(p), write: (p, c) => fs.write(p, c), list: (p) => fs.list(p), delete: (p) => fs.delete(p), mkdir: (p) => fs.mkdir(p), exists: (p) => fs.exists(p), }, window: { setTitle: (title) => store().setWindowTitle(windowId, title), close: () => store().closeWindow(windowId), setSize: (size: Size) => store().resizeWindow(windowId, size), minimize: () => store().minimizeWindow(windowId), }, ai: { // Getter, not a snapshot: the API key can be configured in Settings after // an app window is already open, and the app must see the live value. get available() { return aiAvailable(); }, ask: (prompt, opts) => chat([{ role: "user", content: prompt }], { system: opts?.system }), stream: (prompt, onToken, opts) => chatStream([{ role: "user", content: prompt }], onToken, { system: opts?.system }), }, notify: (title, body, icon) => store().notify({ title, body, icon, appId }), net: { proxyUrl: (url: string) => { const u = (url || "").trim(); if (!u) return u; // In dev we have the /__proxy middleware; in a prod build there is none, // so fall back to the raw URL (which the target may still block). if (import.meta.env.DEV && /^https?:\/\//i.test(u)) { return `/__proxy?url=${encodeURIComponent(u)}`; } return u; }, }, storage: { async get(key: string): Promise { const d = await db(); const row = await d.get("appstore", `${appId}:${key}`); return row ? (row.value as T) : null; }, async set(key, value) { const d = await db(); await d.put("appstore", { k: `${appId}:${key}`, appId, value }); }, async remove(key) { const d = await db(); await d.delete("appstore", `${appId}:${key}`); }, async keys() { const d = await db(); const rows = await d.getAllFromIndex("appstore", "byApp", appId); return rows.map((r) => r.k.slice(appId.length + 1)); }, }, theme: { get tokens() { return tokensAsCss(tokensFor(store().themeMode)); }, get mode() { return store().themeMode; }, }, bus: { emit: (channel, payload) => bus.emit(`app:${channel}`, payload), on: (channel, handler) => bus.on(`app:${channel}`, handler), }, }; }